From f58b6dd3a1f86e4b01fbbae6a7217f694c6404f6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 6 May 2026 19:35:12 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=84=20Sync=202026-05-06=2019:35:12?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gowebdav/Makefile | 4 +- hickory-dns/Makefile | 2 +- hickory-dns/files/etc/hickory-dns/0.zone | 8 + .../files/etc/hickory-dns/127.0.0.1.zone | 10 + hickory-dns/files/etc/hickory-dns/255.zone | 8 + .../files/etc/hickory-dns/forwarder.toml | 27 +- hickory-dns/files/etc/hickory-dns/ipv6_1.zone | 10 + .../files/etc/hickory-dns/localhost.zone | 11 + hickory-dns/files/etc/init.d/hickory-dns | 9 +- ipv6-neigh/Cargo.lock | 17 +- ipv6-neigh/Cargo.toml | 1 + ipv6-neigh/Makefile | 9 +- ipv6-neigh/files/etc/init.d/ipv6-neigh | 4 - ipv6-neigh/src/main.rs | 242 +++++---- .../homeproxy/scripts/update_subscriptions.uc | 6 +- luci-app-passwall/Makefile | 4 +- .../model/cbi/passwall/client/global.lua | 2 +- .../passwall/client/node_subscribe_config.lua | 2 + .../luasrc/passwall/util_sing-box.lua | 13 +- .../luasrc/passwall/util_xray.lua | 58 ++- .../passwall/node_config/link_share_man.htm | 2 +- .../usr/share/passwall/clash_subconverter.lua | 486 ++++++++++++++++++ .../root/usr/share/passwall/subscribe.lua | 19 +- luci-app-passwall2/Makefile | 44 +- .../model/cbi/passwall2/client/type/ray.lua | 7 - .../luasrc/passwall2/util_hysteria2.lua | 2 +- .../luasrc/passwall2/util_naiveproxy.lua | 2 +- .../luasrc/passwall2/util_shadowsocks.lua | 2 +- .../luasrc/passwall2/util_sing-box.lua | 6 +- .../luasrc/passwall2/util_tuic.lua | 2 +- .../luasrc/passwall2/util_xray.lua | 52 +- .../passwall2/node_config/link_share_man.htm | 19 +- luci-app-passwall2/po/zh-cn/passwall2.po | 6 - .../usr/share/passwall2/helper_dnsmasq.lua | 1 + .../root/usr/share/passwall2/subscribe.lua | 354 ++++++++++++- luci-app-socat/Makefile | 2 +- .../luci-static/resources/view/socat.js | 110 ++-- luci-app-socat/po/templates/socat.pot | 175 +++---- luci-app-socat/po/zh_Hans/socat.po | 158 +++--- .../usr/share/rpcd/acl.d/luci-app-socat.json | 4 +- luci-app-wechatpush/Makefile | 2 +- luci-app-wechatpush/po/zh_Hans/wechatpush.po | 4 +- .../root/usr/share/wechatpush/wechatpush | 4 +- luci-theme-material3/Readme.md | 65 ++- .../htdocs/luci-static/material3/cascade.css | 270 +++++----- .../htdocs/luci-static/material3/mobile.css | 57 +- .../luci-static/resources/menu-material3.js | 102 +++- .../ucode/template/themes/material3/footer.ut | 3 + .../ucode/template/themes/material3/header.ut | 2 +- quickstart/Makefile | 4 +- 50 files changed, 1787 insertions(+), 626 deletions(-) create mode 100644 hickory-dns/files/etc/hickory-dns/0.zone create mode 100644 hickory-dns/files/etc/hickory-dns/127.0.0.1.zone create mode 100644 hickory-dns/files/etc/hickory-dns/255.zone create mode 100644 hickory-dns/files/etc/hickory-dns/ipv6_1.zone create mode 100644 hickory-dns/files/etc/hickory-dns/localhost.zone create mode 100644 luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua diff --git a/gowebdav/Makefile b/gowebdav/Makefile index ac07b102..cee9a6f6 100644 --- a/gowebdav/Makefile +++ b/gowebdav/Makefile @@ -6,8 +6,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=gowebdav -PKG_VERSION:=5.11.7 -PKG_RELEASE:=7 +PKG_VERSION:=5.11.8 +PKG_RELEASE:=8 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/hacdias/webdav/tar.gz/v$(PKG_VERSION)? diff --git a/hickory-dns/Makefile b/hickory-dns/Makefile index 20fc745b..93e623d0 100644 --- a/hickory-dns/Makefile +++ b/hickory-dns/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=hickory-dns PKG_VERSION:=0.26.1 -PKG_RELEASE:=19 +PKG_RELEASE:=20 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/hickory-dns/hickory-dns/tar.gz/v$(PKG_VERSION)? diff --git a/hickory-dns/files/etc/hickory-dns/0.zone b/hickory-dns/files/etc/hickory-dns/0.zone new file mode 100644 index 00000000..01c795c8 --- /dev/null +++ b/hickory-dns/files/etc/hickory-dns/0.zone @@ -0,0 +1,8 @@ +$TTL 3D +@ IN SOA ns.lan. admin.lan. ( + 199609203 ; Serial + 28800 ; Refresh + 7200 ; Retry + 604800 ; Expire + 86400) ; Minimum TTL + NS ns.lan. diff --git a/hickory-dns/files/etc/hickory-dns/127.0.0.1.zone b/hickory-dns/files/etc/hickory-dns/127.0.0.1.zone new file mode 100644 index 00000000..b6551760 --- /dev/null +++ b/hickory-dns/files/etc/hickory-dns/127.0.0.1.zone @@ -0,0 +1,10 @@ +$TTL 3D +@ IN SOA ns.lan. admin.lan. ( + 199609203 ; Serial + 28800 ; Refresh + 7200 ; Retry + 604800 ; Expire + 86400) ; Minimum TTL + NS ns.lan. + +1 PTR localhost. diff --git a/hickory-dns/files/etc/hickory-dns/255.zone b/hickory-dns/files/etc/hickory-dns/255.zone new file mode 100644 index 00000000..01c795c8 --- /dev/null +++ b/hickory-dns/files/etc/hickory-dns/255.zone @@ -0,0 +1,8 @@ +$TTL 3D +@ IN SOA ns.lan. admin.lan. ( + 199609203 ; Serial + 28800 ; Refresh + 7200 ; Retry + 604800 ; Expire + 86400) ; Minimum TTL + NS ns.lan. diff --git a/hickory-dns/files/etc/hickory-dns/forwarder.toml b/hickory-dns/files/etc/hickory-dns/forwarder.toml index fab22508..6d147dae 100644 --- a/hickory-dns/files/etc/hickory-dns/forwarder.toml +++ b/hickory-dns/files/etc/hickory-dns/forwarder.toml @@ -5,6 +5,31 @@ prometheus_listen_addr = "[::]:9000" user = "nobody" group = "nogroup" +[[zones]] +zone = "localhost" +zone_type = "Primary" +file = "/etc/hickory-dns/localhost.zone" + +[[zones]] +zone = "0.0.127.in-addr.arpa" +zone_type = "Secondary" +file = "/etc/hickory-dns/127.0.0.1.zone" + +[[zones]] +zone = "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa" +zone_type = "Primary" +file = "/etc/hickory-dns/ipv6_1.zone" + +[[zones]] +zone = "255.in-addr.arpa" +zone_type = "Primary" +file = "/etc/hickory-dns/255.zone" + +[[zones]] +zone = "0.in-addr.arpa" +zone_type = "Primary" +file = "/etc/hickory-dns/0.zone" + [[zones]] zone = "lan" zone_type = "Primary" @@ -27,7 +52,7 @@ zone_type = "External" [[zones.stores]] type = "forward" -options = { edns0 = true, ip_strategy = "Ipv6AndIpv4", cache_size = 10240, use_hosts_file = "Never", num_concurrent_reqs = 1, server_ordering_strategy = "RoundRobin" } +options = { edns0 = true, ip_strategy = "Ipv6AndIpv4", cache_size = 8192, use_hosts_file = "Always", num_concurrent_reqs = 1, server_ordering_strategy = "RoundRobin" } [[zones.stores.name_servers]] ip = "2400:3200:baba::1" diff --git a/hickory-dns/files/etc/hickory-dns/ipv6_1.zone b/hickory-dns/files/etc/hickory-dns/ipv6_1.zone new file mode 100644 index 00000000..b6551760 --- /dev/null +++ b/hickory-dns/files/etc/hickory-dns/ipv6_1.zone @@ -0,0 +1,10 @@ +$TTL 3D +@ IN SOA ns.lan. admin.lan. ( + 199609203 ; Serial + 28800 ; Refresh + 7200 ; Retry + 604800 ; Expire + 86400) ; Minimum TTL + NS ns.lan. + +1 PTR localhost. diff --git a/hickory-dns/files/etc/hickory-dns/localhost.zone b/hickory-dns/files/etc/hickory-dns/localhost.zone new file mode 100644 index 00000000..39966e07 --- /dev/null +++ b/hickory-dns/files/etc/hickory-dns/localhost.zone @@ -0,0 +1,11 @@ +$TTL 3D +@ IN SOA ns.lan. admin.lan. ( + 199609203 ; Serial + 28800 ; Refresh + 7200 ; Retry + 604800 ; Expire + 86400) ; Minimum TTL + NS ns.lan. + +localhost. A 127.0.0.1 + AAAA ::1 diff --git a/hickory-dns/files/etc/init.d/hickory-dns b/hickory-dns/files/etc/init.d/hickory-dns index b1f26778..01a26e41 100755 --- a/hickory-dns/files/etc/init.d/hickory-dns +++ b/hickory-dns/files/etc/init.d/hickory-dns @@ -17,15 +17,22 @@ generate_tsig_key() { start_service() { generate_tsig_key + # Remove stale journal so hickory-dns always starts from the zone file. + # ipv6-neigh re-registers all dynamic entries on startup, so the journal + # has no durable value and may contain a corrupt state after unclean shutdown. + rm -f /var/lib/hickory-dns/lan.jrnl procd_open_instance hickory-dns procd_set_param command $PROG -c $CONF -q procd_set_param stdout 1 procd_set_param stderr 1 procd_set_param respawn "${respawn_threshold:-3600}" "${respawn_timeout:-5}" "${respawn_retry:-5}" procd_close_instance hickory-dns + # Journal was wiped above; restart ipv6-neigh so it re-registers all + # dynamic DNS entries from the current neighbour table. + /etc/init.d/ipv6-neigh restart } - service_triggers() { procd_add_reload_trigger "dhcp" "network" "system" + procd_add_reload_interface_trigger "wan_6" } diff --git a/ipv6-neigh/Cargo.lock b/ipv6-neigh/Cargo.lock index 4b8b3216..7a232d7e 100644 --- a/ipv6-neigh/Cargo.lock +++ b/ipv6-neigh/Cargo.lock @@ -564,6 +564,7 @@ dependencies = [ "netlink-sys", "rtnetlink", "serde_json", + "socket2 0.5.10", "tokio", "ubus", ] @@ -1125,6 +1126,16 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.3" @@ -1261,9 +1272,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.1" +version = "1.52.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" +checksum = "110a78583f19d5cdb2c5ccf321d1290344e71313c6c37d43520d386027d18386" dependencies = [ "bytes", "libc", @@ -1271,7 +1282,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] diff --git a/ipv6-neigh/Cargo.toml b/ipv6-neigh/Cargo.toml index 908d293b..bc7294fc 100644 --- a/ipv6-neigh/Cargo.toml +++ b/ipv6-neigh/Cargo.toml @@ -18,6 +18,7 @@ ubus = "0.1.6" serde_json = "1.0.108" log = "0.4" env_logger = "0.11" +socket2 = "0.5" [profile.release] opt-level = 3 diff --git a/ipv6-neigh/Makefile b/ipv6-neigh/Makefile index dbdd198c..2e5d74e8 100644 --- a/ipv6-neigh/Makefile +++ b/ipv6-neigh/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ipv6-neigh PKG_VERSION:=0.1.0 -PKG_RELEASE:=4 +PKG_RELEASE:=5 PKG_BUILD_DEPENDS:=rust/host PKG_BUILD_PARALLEL:=1 @@ -28,6 +28,13 @@ define Package/ipv6-neigh/description DHCP leases (ubus). endef +define Build/Prepare + mkdir -p $(PKG_BUILD_DIR)/src + $(CP) $(CURDIR)/src/. $(PKG_BUILD_DIR)/src/ + $(CP) $(CURDIR)/Cargo.toml $(PKG_BUILD_DIR)/ + $(CP) $(CURDIR)/Cargo.lock $(PKG_BUILD_DIR)/ +endef + define Build/Compile +$(CARGO_PKG_VARS) \ cargo build -v --profile $(CARGO_PKG_PROFILE) \ diff --git a/ipv6-neigh/files/etc/init.d/ipv6-neigh b/ipv6-neigh/files/etc/init.d/ipv6-neigh index f50a996f..deacd301 100644 --- a/ipv6-neigh/files/etc/init.d/ipv6-neigh +++ b/ipv6-neigh/files/etc/init.d/ipv6-neigh @@ -14,7 +14,3 @@ start_service() { procd_set_param respawn "${respawn_threshold:-3600}" "${respawn_timeout:-5}" "${respawn_retry:-5}" procd_close_instance ipv6-neigh } - -service_triggers() { - procd_add_reload_trigger "dhcp" "network" -} diff --git a/ipv6-neigh/src/main.rs b/ipv6-neigh/src/main.rs index 61348729..6b3a130a 100644 --- a/ipv6-neigh/src/main.rs +++ b/ipv6-neigh/src/main.rs @@ -1,15 +1,18 @@ -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr as StdSocketAddr}; +use std::time::Instant; use futures::stream::StreamExt; use futures::stream::TryStreamExt; -use log::{debug, error, info, warn}; +use log::{debug, error, info}; use netlink_packet_core::NetlinkPayload; use netlink_packet_route::RouteNetlinkMessage; use netlink_packet_route::neighbour::NeighbourMessage; use netlink_packet_route::neighbour::{NeighbourAddress, NeighbourAttribute, NeighbourState}; use netlink_packet_route::route::RouteType; use rtnetlink::{Error, Handle, new_connection}; +use socket2::{Domain, Protocol, SockAddr, Socket, Type}; +use tokio::time::{self, Duration}; use clap::Parser; use hickory_proto::rr::Name; @@ -32,8 +35,6 @@ const fn nl_mgrp(group: u32) -> u32 { #[derive(Debug, Parser)] #[clap()] struct Cli { - #[clap(short, long)] - iface: Option, #[clap(short, long)] private_subnet: bool, /// hickory-dns server address for DNS updates (e.g. "[::1]:5335") @@ -51,6 +52,9 @@ struct Cli { /// Log level (error, warn, info, debug, trace) #[clap(short = 'l', long, default_value = "info")] log_level: String, + /// Probe interval in seconds for active reachability checks (0 to disable) + #[clap(long, default_value = "120")] + probe_interval: u64, } #[derive(Debug)] @@ -150,6 +154,16 @@ fn should_skip_neigh(neigh: &Neigh) -> bool { should_skip_route_type(neigh.kind) } +/// Whether this NUD state indicates the neighbour is likely reachable (register in DNS). +fn is_reachable_state(state: NeighbourState) -> bool { + matches!(state, NeighbourState::Reachable | NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) +} + +/// Whether this NUD state indicates the neighbour is definitely gone (remove from DNS). +fn is_failed_state(state: NeighbourState) -> bool { + matches!(state, NeighbourState::Failed) +} + #[tokio::main] async fn main() -> Result<(), ()> { let args = Cli::parse(); @@ -176,30 +190,26 @@ async fn main() -> Result<(), ()> { let (connection, handle, _) = new_connection().unwrap(); tokio::spawn(connection); - if let Some(ref iface) = args.iface { - if let Err(e) = dump_addresses(handle.clone(), iface.clone()).await { - warn!("failed to dump addresses for iface {}: {}", iface, e); - } - } // Load DHCP leases (mac -> hostname) from ubus let mut leases = op::get_lease().unwrap_or_default(); info!("loaded {} DHCP leases", leases.len()); - // State cache: tracks (mac, ip_string) pairs already registered - let mut registered: HashSet<(String, String)> = HashSet::new(); + // State cache: tracks (mac, ip_string) -> last confirmed time + let mut registered: HashMap<(String, String), Instant> = HashMap::new(); - // Dump existing neighbours and register them + // Dump existing neighbours and register only reachable/stale ones debug!("dumping neighbours"); if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet).await { for neigh in &neighbours { debug!("{:?}", neigh); - if !should_skip_neigh(neigh) { - let key = (neigh.mac.clone(), inet_to_string(&neigh.inet)); - if registered.insert(key.clone()) { - if !process_new_neigh(neigh, &updater, &leases).await { - registered.remove(&key); - } + if should_skip_neigh(neigh) || !is_reachable_state(neigh.state) { + continue; + } + let key = (neigh.mac.clone(), inet_to_string(&neigh.inet)); + if !registered.contains_key(&key) { + if process_new_neigh(neigh, &updater, &leases).await { + registered.insert(key, Instant::now()); } } } @@ -218,69 +228,144 @@ async fn main() -> Result<(), ()> { tokio::spawn(conn); + // Start periodic probing task + let probe_interval = args.probe_interval; + let mut probe_timer = if probe_interval > 0 { + time::interval(Duration::from_secs(probe_interval)) + } else { + // Create a very long interval that effectively disables probing + time::interval(Duration::from_secs(u64::MAX / 2)) + }; + probe_timer.tick().await; // consume the immediate first tick + let mut event_count: u64 = 0; - // Start receiving events through `messages` channel. - while let Some((message, _)) = messages.next().await { - // Refresh leases periodically (every 50 events) - event_count += 1; - if event_count % 50 == 0 { - if let Ok(new_leases) = op::get_lease() { - leases = new_leases; - info!("refreshed {} DHCP leases", leases.len()); - } - } + loop { + tokio::select! { + msg_opt = messages.next() => { + let Some((message, _)) = msg_opt else { + break; + }; - let payload = message.payload; - if let NetlinkPayload::InnerMessage(msg) = payload { - match msg { - RouteNetlinkMessage::NewNeighbour(new_neigh) => { - let Some(neigh) = parse_neighbour_message(new_neigh, private_subnet) else { - continue; - }; - if should_skip_neigh(&neigh) { - continue; - } - // Only act on Reachable state to avoid redundant updates - if neigh.state != NeighbourState::Reachable { - continue; - } - let key = (neigh.mac.clone(), inet_to_string(&neigh.inet)); - if !registered.insert(key.clone()) { - // Already registered, skip - continue; - } - debug!("New neighbour: {:?}", neigh); - if !process_new_neigh(&neigh, &updater, &leases).await { - // DNS update failed, remove from cache so we retry next time - registered.remove(&key); + // Refresh leases periodically (every 50 events) + event_count += 1; + if event_count % 50 == 0 { + if let Ok(new_leases) = op::get_lease() { + leases = new_leases; + debug!("refreshed {} DHCP leases", leases.len()); } } - RouteNetlinkMessage::DelNeighbour(del_neigh) => { - let Some(neigh) = parse_neighbour_message(del_neigh, private_subnet) else { - continue; - }; - if should_skip_neigh(&neigh) { - continue; - } - let key = (neigh.mac.clone(), inet_to_string(&neigh.inet)); - if !registered.remove(&key) { - // Was not registered, skip - continue; - } - debug!("Del neighbour: {:?}", neigh); - if !process_del_neigh(&neigh, &updater, &leases).await { - // DNS delete failed, put back so we retry delete next time - registered.insert(key); + + let payload = message.payload; + if let NetlinkPayload::InnerMessage(msg) = payload { + match msg { + RouteNetlinkMessage::NewNeighbour(new_neigh) => { + let Some(neigh) = parse_neighbour_message(new_neigh, private_subnet) else { + continue; + }; + if should_skip_neigh(&neigh) { + continue; + } + let key = (neigh.mac.clone(), inet_to_string(&neigh.inet)); + + if is_failed_state(neigh.state) { + // Neighbour confirmed unreachable — remove DNS record + if registered.remove(&key).is_some() { + debug!("Neighbour failed: {:?}", neigh); + if !process_del_neigh(&neigh, &updater, &leases).await { + // DNS delete failed, put back so we retry + registered.insert(key, Instant::now()); + } + } + } else if is_reachable_state(neigh.state) { + if registered.contains_key(&key) { + // Already registered, just update timestamp + registered.insert(key, Instant::now()); + } else { + // New reachable neighbour — register in DNS + debug!("New neighbour: {:?}", neigh); + if process_new_neigh(&neigh, &updater, &leases).await { + registered.insert(key, Instant::now()); + } + } + } + } + RouteNetlinkMessage::DelNeighbour(del_neigh) => { + let Some(neigh) = parse_neighbour_message(del_neigh, private_subnet) else { + continue; + }; + if should_skip_neigh(&neigh) { + continue; + } + let key = (neigh.mac.clone(), inet_to_string(&neigh.inet)); + if registered.remove(&key).is_none() { + // Was not registered, skip + continue; + } + debug!("Del neighbour: {:?}", neigh); + if !process_del_neigh(&neigh, &updater, &leases).await { + // DNS delete failed, put back so we retry delete next time + registered.insert(key, Instant::now()); + } + } + _ => {} } } - _ => {} + } + _ = probe_timer.tick() => { + if probe_interval == 0 { + continue; + } + probe_registered_neighbours(®istered).await; } } } Ok(()) } +/// Send ICMPv6 Echo Request / ICMPv4 Echo Request to all registered neighbours. +/// This forces the kernel NUD state machine to verify reachability, generating +/// NewNeighbour events with the resulting state (Reachable or Failed). +async fn probe_registered_neighbours(registered: &HashMap<(String, String), Instant>) { + let now = Instant::now(); + for ((_, ip_str), last_confirmed) in registered.iter() { + // Only probe entries not confirmed recently (older than 30s) + if now.duration_since(*last_confirmed) < Duration::from_secs(30) { + continue; + } + if let Ok(addr) = ip_str.parse::() { + if let Err(e) = send_icmpv6_echo(addr) { + debug!("probe failed for {}: {}", ip_str, e); + } + } else if let Ok(addr) = ip_str.parse::() { + if let Err(e) = send_icmpv4_echo(addr) { + debug!("probe failed for {}: {}", ip_str, e); + } + } + } +} + +fn send_icmpv6_echo(addr: Ipv6Addr) -> std::io::Result<()> { + let socket = Socket::new(Domain::IPV6, Type::DGRAM, Some(Protocol::ICMPV6))?; + socket.set_nonblocking(true)?; + // ICMPv6 Echo Request: type=128, code=0, checksum=0 (kernel computes), id=0, seq=1 + let packet: [u8; 8] = [128, 0, 0, 0, 0, 0, 0, 1]; + let dest = SockAddr::from(std::net::SocketAddrV6::new(addr, 0, 0, 0)); + let _ = socket.send_to(&packet, &dest); + Ok(()) +} + +fn send_icmpv4_echo(addr: Ipv4Addr) -> std::io::Result<()> { + let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::ICMPV4))?; + socket.set_nonblocking(true)?; + // ICMPv4 Echo Request: type=8, code=0, checksum (simple for 8 bytes), id=0, seq=1 + // Checksum for [08,00,00,00,00,00,00,01]: ~(0x0800 + 0x0001) = 0xf7fe + let packet: [u8; 8] = [8, 0, 0xf7, 0xfe, 0, 0, 0, 1]; + let dest = SockAddr::from(std::net::SocketAddrV4::new(addr, 0)); + let _ = socket.send_to(&packet, &dest); + Ok(()) +} + fn format_mac(mac: Vec) -> String { let mut mac_str = String::new(); for byte in mac { @@ -298,27 +383,6 @@ fn inet_to_string(addr: &NeighbourAddress) -> String { } } -async fn dump_addresses(handle: Handle, link: String) -> Result<(), Error> { - let mut request = handle.link().get(); - request = request.match_name(link); - - let mut links = request.execute(); - if let Some(link) = links.try_next().await? { - let mut addresses = handle - .address() - .get() - .set_link_index_filter(link.header.index) - .execute(); - while let Some(msg) = addresses.try_next().await? { - debug!("{msg:?}"); - } - Ok(()) - } else { - warn!("link not found"); - Ok(()) - } -} - fn is_link_local_ipv6(addr: &NeighbourAddress) -> bool { matches!(addr, NeighbourAddress::Inet6(ip) if (ip.segments()[0] & 0xffc0) == 0xfe80) } diff --git a/luci-app-homeproxy/root/etc/homeproxy/scripts/update_subscriptions.uc b/luci-app-homeproxy/root/etc/homeproxy/scripts/update_subscriptions.uc index bfb902f7..020a9130 100755 --- a/luci-app-homeproxy/root/etc/homeproxy/scripts/update_subscriptions.uc +++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/update_subscriptions.uc @@ -136,7 +136,7 @@ function parse_uri(uri) { case 'hysteria': /* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */ url = parseURL('http://' + uri[1]) || {}; - params = url.searchParams; + params = url.searchParams || {}; if (!sing_features.with_quic || (params.protocol && params.protocol !== 'udp')) { log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname)); @@ -168,7 +168,7 @@ function parse_uri(uri) { case 'hy2': /* https://v2.hysteria.network/docs/developers/URI-Scheme/ */ url = parseURL('http://' + uri[1]) || {}; - params = url.searchParams; + params = url.searchParams || {}; if (!sing_features.with_quic) { log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname)); @@ -318,7 +318,7 @@ function parse_uri(uri) { case 'vless': /* https://github.com/XTLS/Xray-core/discussions/716 */ url = parseURL('http://' + uri[1]) || {}; - params = url.searchParams; + params = url.searchParams || {}; /* Unsupported protocol */ if (params.type === 'kcp') { diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 56d551d0..bf91dc24 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.5.3 -PKG_RELEASE:=112 +PKG_RELEASE:=113 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ @@ -39,7 +39,7 @@ LUCI_PKGARCH:=all LUCI_DEPENDS:=+coreutils +coreutils-base64 +coreutils-nohup +curl \ +chinadns-ng +dns2socks +dnsmasq-full +ip-full \ +libuci-lua +lua +luci-compat +luci-lib-jsonc \ - +microsocks +resolveip +tcping \ + +microsocks +resolveip +tcping +lyaml \ +ipt2socks \ +PACKAGE_$(PKG_NAME)_INCLUDE_Geoview:geoview \ +PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy:haproxy \ diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index dfa7994a..78d8dd63 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -253,7 +253,7 @@ o:value("119.28.28.28") o:depends("direct_dns_mode", "udp") o:depends("direct_dns_mode", "tcp") -o = s:taboption("DNS", Flag, "filter_proxy_ipv6", translate("Filter Proxy Host IPv6"), translate("Experimental feature.")) +o = s:taboption("DNS", Flag, "filter_proxy_ipv6", translate("Filter Proxy Host IPv6")) o.default = "0" -- TCP分流时dns过滤模式保存逻辑 diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua index fb633b8f..a779000f 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua @@ -288,6 +288,8 @@ o = s:option(Value, "user_agent", translate("User-Agent")) o.default = "passwall" o:value("passwall", "PassWall") o:value("v2rayN/9.99", "v2rayN") +o:value("clash.meta", "Clash.Meta") +o:value("Clash", "Clash") o:value("curl", "Curl") o:value("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", "Edge for Linux") o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", "Edge for Windows") diff --git a/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/luci-app-passwall/luasrc/passwall/util_sing-box.lua index e9015773..ab75bc64 100644 --- a/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -511,13 +511,12 @@ function gen_outbound(flag, node, tag, proxy_table) heartbeat = (tonumber(node.tuic_heartbeat) or 3) .. "s", tls = tls } - if node.tuic_alpn and node.tuic_alpn ~= "default" then - local alpn = {} - string.gsub(node.tuic_alpn, '[^,]+', function(w) - table.insert(alpn, w) - end) - if #alpn > 0 then protocol_table.tls.alpn = alpn end - end + node.tuic_alpn = (node.tuic_alpn and node.tuic_alpn ~= "default") and node.tuic_alpn or "h3" + local alpn = {} + string.gsub(node.tuic_alpn, '[^,]+', function(w) + table.insert(alpn, w) + end) + if #alpn > 0 then protocol_table.tls.alpn = alpn end end if node.protocol == "hysteria2" then diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index 074cfb51..b4d9dbfe 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -1643,14 +1643,13 @@ function gen_config(var) } }) else - if COMMON.default_balancer_tag then - dns_outbound_tag = nil - elseif COMMON.default_outbound_tag then + if COMMON.default_outbound_tag then dns_outbound_tag = COMMON.default_outbound_tag end end local dns_rule_position = 1 + local remote_dns_outbound if dns_listen_port then table.insert(inbounds, { listen = "127.0.0.1", @@ -1663,22 +1662,22 @@ function gen_config(var) } }) - table.insert(outbounds, { + -- remote dns outbound + local chn_list = uci:get(appname, "@global[0]", "chn_list") or "direct" + remote_dns_outbound = { tag = "dns-out", protocol = "dns", + proxySettings = dns_outbound_tag and { + tag = (dns_outbound_tag ~= "blackhole") and dns_outbound_tag or "direct" + } or nil, settings = { + address = (chn_list ~= "proxy") and "8.8.8.8" or "223.5.5.5", + port = 53, + network = "tcp", nonIPQuery = (api.compare_versions(xray_version, "<", "26.4.25")) and "reject" or nil, -- Todo is to remove it - rules = (api.compare_versions(xray_version, ">", "26.4.17")) and { - { - action = "hijack", - qtype = "1,28" - }, - { - action = "reject" - } - } or nil + rules = (api.compare_versions(xray_version, ">", "26.4.17")) and {} or nil } - }) + } table.insert(routing.rules, 1, { inboundTag = { @@ -1702,6 +1701,7 @@ function gen_config(var) end --按分流顺序DNS + local remote_dns_out_rules = {} if dns_domain_rules and #dns_domain_rules > 0 then for index, value in ipairs(dns_domain_rules) do if value.domain and value.outboundTag then @@ -1732,6 +1732,21 @@ function gen_config(var) dns_server = nil end ]]-- + + -- remote dns outbound rules + if value.outboundTag == "blackhole" then + table.insert(remote_dns_out_rules, { + action = "reject", + domain = api.clone(value.domain) + }) + else + table.insert(remote_dns_out_rules, { + action = "hijack", + qtype = "1,28", + domain = api.clone(value.domain) + }) + end + if dns_server then --dns_server.finalQuery = true dns_server.domains = value.domain @@ -1789,6 +1804,21 @@ function gen_config(var) dns.hosts = nil end + -- remote dns outbound + if remote_dns_outbound then + if remote_dns_outbound.settings.rules then + table.insert(remote_dns_out_rules, { + action = "hijack", + qtype = "1,28" + }) + table.insert(remote_dns_out_rules, { + action = "direct" + }) + remote_dns_outbound.settings.rules = remote_dns_out_rules + end + table.insert(outbounds, remote_dns_outbound) + end + -- 自定义节点 DNS if #node_dns > 0 and #dns.servers < 1 then dns.servers = { "localhost" } diff --git a/luci-app-passwall/luasrc/view/passwall/node_config/link_share_man.htm b/luci-app-passwall/luasrc/view/passwall/node_config/link_share_man.htm index 02dd00c1..f3d33278 100644 --- a/luci-app-passwall/luasrc/view/passwall/node_config/link_share_man.htm +++ b/luci-app-passwall/luasrc/view/passwall/node_config/link_share_man.htm @@ -1684,7 +1684,7 @@ local current_node = map:get(section) } opt.set(dom_prefix + 'tuic_congestion_control', queryParam.congestion_control || 'cubic'); opt.set(dom_prefix + 'tuic_udp_relay_mode', queryParam.udp_relay_mode || 'native'); - opt.set(dom_prefix + 'tuic_alpn', queryParam.alpn || 'default'); + opt.set(dom_prefix + 'tuic_alpn', queryParam.alpn || 'h3'); opt.set(dom_prefix + 'tls_serverName', queryParam.sni || ''); opt.set(dom_prefix + 'tls_disable_sni', queryParam.disable_sni === "1"); opt.set( diff --git a/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua b/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua new file mode 100644 index 00000000..526ac3ad --- /dev/null +++ b/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua @@ -0,0 +1,486 @@ +#!/usr/bin/lua + +-- Copyright(c): lwb1978 2025–2027 + +local api = require("luci.passwall.api") +local urlencode = api.UrlEncode +local base64 = api.base64Encode +local json = api.jsonc + +local function host_format(host) + if not host then return "" end + local str = host:match("%[(.-)%]") or host + if api.datatypes.ip6addr(str) then + return "[" .. str .. "]" + end + return host +end + +local function build_alpn(alpn) -- 排序+去重 + if not alpn then return nil end + + local seen = {} + local order = { "h3", "h2", "http/1.1" } + + if type(alpn) == "table" then + for _, v in ipairs(alpn) do + if v then seen[v] = true end + end + else + seen[tostring(alpn)] = true + end + + local t = {} + for _, v in ipairs(order) do + if seen[v] then table.insert(t, v) end + end + + if #t == 0 then return nil end + return table.concat(t, ",") +end + +local function build_common(node) + local o = {} + + o.server = host_format(node.server) + o.port = node.port + o.name = node.name + + -- ===== TLS ===== + o.tls = {} + + if node["reality-opts"] then + o.tls.security = "reality" + o.tls.pbk = node["reality-opts"]["public-key"] + o.tls.sid = node["reality-opts"]["short-id"] + elseif node.tls then + o.tls.security = "tls" + end + + o.tls.sni = node.servername or node.sni + o.tls.fp = node["client-fingerprint"] + o.tls.pcs = node.fingerprint + o.tls.insecure = node["skip-cert-verify"] == true + + o.tls.alpn = build_alpn(node.alpn) + + local ech_opts = node["ech-opts"] + if ech_opts and ech_opts.enable == true then + if ech_opts.config then + o.tls.ech = ech_opts.config + elseif ech_opts["query-server-name"] then + o.tls.ech = ech_opts["query-server-name"] .. "+https://223.5.5.5/dns-query" + end + end + + -- ===== transport ===== + o.transport = {} + local net = node.network or "tcp" + o.transport.type = net + + local function get_first(v) + if type(v) == "table" then return v[1] end + return v + end + + if net == "ws" then + local opts = node["ws-opts"] + if opts then + o.transport.path = opts.path + o.transport.host = opts.headers and opts.headers.Host + end + + elseif net == "grpc" then + local opts = node["grpc-opts"] + if opts then + o.transport.serviceName = opts["grpc-service-name"] + end + + elseif net == "http" then + local opts = node["http-opts"] + if opts then + o.transport.host = get_first(opts.host) + o.transport.path = get_first(opts.path) + end + + elseif net == "h2" then + local opts = node["h2-opts"] + if opts then + o.transport.host = get_first(opts.host) + o.transport.path = opts.path + end + + elseif net == "xhttp" then + local opts = node["xhttp-opts"] + if opts then + o.transport.host = opts.host + o.transport.path = opts.path + o.transport.mode = opts.mode + + local extra = {} + + -- headers + if opts.headers then + extra.headers = opts.headers + end + + if opts["x-padding-bytes"] then + extra.xPaddingBytes = opts["x-padding-bytes"] + end + + if opts["no-grpc-header"] ~= nil then + extra.noGRPCHeader = opts["no-grpc-header"] + end + + if opts["sc-max-each-post-bytes"] then + extra.scMaxEachPostBytes = opts["sc-max-each-post-bytes"] + end + + if opts["sc-min-posts-interval-ms"] then + extra.scMinPostsIntervalMs = opts["sc-min-posts-interval-ms"] + end + + -- xmux + if opts["reuse-settings"] then + local r = opts["reuse-settings"] + local xmux = {} + + if r["max-concurrency"] then xmux.maxConcurrency = r["max-concurrency"] end + if r["max-connections"] then xmux.maxConnections = tonumber(r["max-connections"]) end + if r["c-max-reuse-times"] then xmux.cMaxReuseTimes = tonumber(r["c-max-reuse-times"]) end + if r["h-max-request-times"] then xmux.hMaxRequestTimes = r["h-max-request-times"] end + if r["h-max-reusable-secs"] then xmux.hMaxReusableSecs = r["h-max-reusable-secs"] end + if r["h-keep-alive-period"] then xmux.hKeepAlivePeriod = tonumber(r["h-keep-alive-period"]) end + + if next(xmux) then extra.xmux = xmux end + end + + -- download-settings + if opts["download-settings"] then + local d = opts["download-settings"] + local ds = {} + + if d.server then ds.address = d.server end + if d.port then ds.port = d.port end + + ds.network = "xhttp" + + -- TLS + if d.tls then + ds.security = "tls" + ds.tlsSettings = {} + + if d.servername then + ds.tlsSettings.serverName = d.servername + end + if d["skip-cert-verify"] == true then + ds.tlsSettings.allowInsecure = true + end + if d["client-fingerprint"] then + ds.tlsSettings.fingerprint = d["client-fingerprint"] + end + if d.fingerprint then + ds.tlsSettings.pinnedPeerCertSha256 = d.fingerprint + end + if d.alpn then + ds.tlsSettings.alpn = d.alpn + end + end + + -- xhttpSettings + local xs = {} + + if d.path then xs.path = d.path end + if d.host then xs.host = d.host end + + if next(xs) then + ds.xhttpSettings = xs + end + + extra.downloadSettings = ds + end + + if next(extra) then + o.transport.extra = json.stringify(extra) + end + end + end + + return o +end + +-- VLESS +local function encode_vless(node) + local o = build_common(node) + + local link = "vless://" .. node.uuid .. "@" .. o.server .. ":" .. o.port + local p = {} + + if node.flow then table.insert(p, "flow=" .. urlencode(node.flow)) end + if node.encryption then table.insert(p, "encryption=" .. urlencode(node.encryption)) end + + -- TLS + if o.tls.security then table.insert(p, "security=" .. o.tls.security) end + if o.tls.pbk then table.insert(p, "pbk=" .. urlencode(o.tls.pbk)) end + if o.tls.sid then table.insert(p, "sid=" .. urlencode(o.tls.sid)) end + if o.tls.sni then table.insert(p, "sni=" .. urlencode(o.tls.sni)) end + if o.tls.fp then table.insert(p, "fp=" .. urlencode(o.tls.fp)) end + if o.tls.alpn then table.insert(p, "alpn=" .. urlencode(o.tls.alpn)) end + if o.tls.ech then table.insert(p, "ech=" .. urlencode(o.tls.ech)) end + if o.tls.pcs then table.insert(p, "pcs=" .. urlencode(o.tls.pcs)) end + table.insert(p, "allowInsecure=" .. (o.tls.insecure and "1" or "0")) + + -- transport + table.insert(p, "type=" .. o.transport.type) + if o.transport.host then table.insert(p, "host=" .. urlencode(o.transport.host)) end + if o.transport.path then table.insert(p, "path=" .. urlencode(o.transport.path)) end + if o.transport.serviceName then table.insert(p, "serviceName=" .. urlencode(o.transport.serviceName)) end + if o.transport.mode then table.insert(p, "mode=" .. urlencode(o.transport.mode)) end + if o.transport.extra then table.insert(p, "extra=" .. urlencode(o.transport.extra)) end + + if #p > 0 then + link = link .. "?" .. table.concat(p, "&") + end + + return link .. "#" .. urlencode(o.name or "") +end + +-- Trojan +local function encode_trojan(node) + local o = build_common(node) + + local link = "trojan://" .. node.password .. "@" .. o.server .. ":" .. o.port + local p = {} + + if o.tls.security then table.insert(p, "security=" .. o.tls.security) end + if o.tls.pbk then table.insert(p, "pbk=" .. urlencode(o.tls.pbk)) end + if o.tls.sid then table.insert(p, "sid=" .. urlencode(o.tls.sid)) end + if o.tls.sni then table.insert(p, "sni=" .. urlencode(o.tls.sni)) end + if o.tls.fp then table.insert(p, "fp=" .. urlencode(o.tls.fp)) end + if o.tls.alpn then table.insert(p, "alpn=" .. urlencode(o.tls.alpn)) end + if o.tls.pcs then table.insert(p, "pcs=" .. urlencode(o.tls.pcs)) end + table.insert(p, "allowInsecure=" .. (o.tls.insecure and "1" or "0")) + + table.insert(p, "type=" .. o.transport.type) + if o.transport.host then table.insert(p, "host=" .. urlencode(o.transport.host)) end + if o.transport.path then table.insert(p, "path=" .. urlencode(o.transport.path)) end + if o.transport.serviceName then table.insert(p, "serviceName=" .. urlencode(o.transport.serviceName)) end + + if #p > 0 then + link = link .. "?" .. table.concat(p, "&") + end + + return link .. "#" .. urlencode(o.name or "") +end + +-- VMess +local function encode_vmess(node) + local o = build_common(node) + + local obj = { + v = "2", + ps = o.name, + add = node.server, + port = tostring(node.port), + id = node.uuid, + aid = tostring(node.alterId or 0), + net = o.transport.type, + security = node.cipher, + scy = node.cipher, + type = "none", + host = o.transport.host or "", + path = o.transport.path or "", + tls = o.tls.security == "tls" and "tls" or "", + sni = o.tls.sni, + alpn = o.tls.alpn, + fp = o.tls.fp, + pcs = o.tls.pcs, + insecure = o.tls.insecure and "1" or "0", + tfo = node.tfo and "1" or "0" + } + + if o.transport.type == "grpc" then + obj.path = o.transport.serviceName or "" + end + + return "vmess://" .. base64(json.stringify(obj)) +end + +-- SS +local function encode_ss(node) + local userinfo = node.cipher .. ":" .. node.password + local base = userinfo .. "@" .. host_format(node.server) .. ":" .. node.port + local link = "ss://" .. base64(base) + + local p = {} + + if node.udp then table.insert(p, "udp=1") end + if node["udp-over-tcp"] then table.insert(p, "uot=1") end + + if node.plugin then + local plugin = node.plugin + if node["plugin-opts"] then + local opts = {} + for k, v in pairs(node["plugin-opts"]) do + table.insert(opts, k .. "=" .. v) + end + plugin = plugin .. ";" .. table.concat(opts, ";") + end + table.insert(p, "plugin=" .. urlencode(plugin)) + end + + if #p > 0 then + link = link .. "?" .. table.concat(p, "&") + end + + return link .. "#" .. urlencode(node.name or "") +end + +-- Hysteria +local function encode_hysteria2(node) + local link = "hysteria://" .. host_format(node.server) .. ":" .. (node.port or "") + local p = {} + + if node["auth-str"] then table.insert(p, "auth=" .. node["auth-str"]) end + if node["ports"] then table.insert(p, "mport=" .. node["ports"]) end + if node.obfs then table.insert(p, "obfsParam=" .. node.obfs) end + if node.sni then table.insert(p, "sni=" .. node.sni) end + if node.up then table.insert(p, "upmbps=" .. node.up) end + if node.down then table.insert(p, "downmbps=" .. node.down) end + if node["skip-cert-verify"] then table.insert(p, "insecure=1") end + if node["fingerprint"] then table.insert(p, "pinSHA256=" .. urlencode(node["fingerprint"])) end + + if node.alpn then + table.insert(p, "alpn=" .. urlencode(build_alpn(node.alpn))) + end + + if #p > 0 then + link = link .. "?" .. table.concat(p, "&") + end + + return link .. "#" .. urlencode(node.name or "") +end + +-- Hysteria2 +local function encode_hysteria2(node) + local link = "hysteria2://" .. (node.password or "") .. "@" .. host_format(node.server) .. ":" .. (node.port or "") + local p = {} + + if node["ports"] then table.insert(p, "mport=" .. urlencode(node["ports"])) end + if node.obfs then table.insert(p, "obfs=" .. node.obfs) end + if node["obfs-password"] then table.insert(p, "obfs-password=" .. node["obfs-password"]) end + if node.up then table.insert(p, "upmbps=" .. node.up) end + if node.down then table.insert(p, "downmbps=" .. node.down) end + + if node.sni then table.insert(p, "sni=" .. urlencode(node.sni)) end + if node["skip-cert-verify"] then table.insert(p, "insecure=1") end + if node["fingerprint"] then table.insert(p, "pinSHA256=" .. urlencode(node["fingerprint"])) end + + if #p > 0 then + link = link .. "?" .. table.concat(p, "&") + end + + return link .. "#" .. urlencode(node.name or "") +end + +-- TUIC +local function encode_tuic(node) + local link = "tuic://" .. node.uuid .. ":" .. node.password .. "@" .. host_format(node.server) .. ":" .. node.port + local p = {} + + if node["congestion-controller"] then + table.insert(p, "congestion_control=" .. node["congestion-controller"]) + end + + if node.alpn then + table.insert(p, "alpn=" .. urlencode(build_alpn(node.alpn))) + end + + if node.sni then table.insert(p, "sni=" .. urlencode(node.sni)) end + if node["disable-sni"] then table.insert(p, "disable_sni=1") end + if node["skip-cert-verify"] then table.insert(p, "allowInsecure=1") end + if node["udp-relay-mode"] then table.insert(p, "udp_relay_mode=" .. node["udp-relay-mode"]) end + + + if #p > 0 then + link = link .. "?" .. table.concat(p, "&") + end + + return link .. "#" .. urlencode(node.name or "") +end + +-- AnyTLS +local function encode_anytls(node) + local link = "anytls://" .. (node.password or "") .. "@" .. host_format(node.server) .. ":" .. node.port + local p = {} + + if node.sni then table.insert(p, "sni=" .. urlencode(node.sni)) end + if node["skip-cert-verify"] then table.insert(p, "allowInsecure=1") end + + if node.alpn then + table.insert(p, "alpn=" .. urlencode(build_alpn(node.alpn))) + end + + if #p > 0 then + link = link .. "?" .. table.concat(p, "&") + end + + return link .. "#" .. urlencode(node.name or "") +end + +-- SSR +local function encode_ssr(node) + local link = host_format(node.server) .. ":" .. node.port .. ":" .. (node.protocol or "") .. ":" .. + (node.cipher or "") .. ":" .. (node.obfs or "") .. ":" .. base64(node.password) + local p = {} + + if node["obfs-param"] then table.insert(p, "obfsparam=" .. base64(node["obfs-param"])) end + if node["protocol-param"] then table.insert(p, "protoparam=" .. base64(node["protocol-param"])) end + table.insert(p, "remarks=" .. base64(node.name)) + + if #p > 0 then + link = link .. "?" .. table.concat(p, "&") + end + + return "ssr://" .. base64(link) +end + +local function encode_node(node) + if (not node.type) or (not node.name) then return nil end + + local t = node.type + + if t == "vless" then return encode_vless(node) + elseif t == "trojan" then return encode_trojan(node) + elseif t == "vmess" then return encode_vmess(node) + elseif t == "ss" then return encode_ss(node) + elseif t == "hysteria" then return encode_hysteria(node) + elseif t == "hysteria2" then return encode_hysteria2(node) + elseif t == "tuic" then return encode_tuic(node) + elseif t == "anytls" then return encode_anytls(node) + elseif t == "ssr" then return encode_ssr(node) + else api.log("订阅转换 → 丢弃不支持的节点:" .. node.name .. ",节点类型:" .. t) + end +end + +function parseClashNode(raw, remark) + if not raw then return "" end + local ok, lyaml = pcall(require, "lyaml") + if not ok then return raw end + + local data = lyaml.load(raw) + if not data or type(data) ~= "table" then return raw end + if not data.proxies then return "" end + + api.log('检测到 Clash 订阅,正在进行转换 ...') + + local links = {} + for _, node in ipairs(data.proxies) do + local link = encode_node(node) + if link then + table.insert(links, link) + end + end + + return #links > 0 and table.concat(links, "\n") or "" +end diff --git a/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/luci-app-passwall/root/usr/share/passwall/subscribe.lua index c418e21e..e8b4c74d 100755 --- a/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -10,6 +10,8 @@ local appname = 'passwall' local api = require ("luci.passwall.api") local datatypes = require "luci.cbi.datatypes" +loadfile("/usr/share/" .. appname .. "/clash_subconverter.lua")() + -- these global functions are accessed all the time by the event handler -- so caching them is worth the effort local tinsert = table.insert @@ -1490,7 +1492,7 @@ local function processData(szType, content, add_mode, group, sub_cfg) end result.tls_serverName = params.sni result.tls_disable_sni = params.disable_sni - result.tuic_alpn = params.alpn or "default" + result.tuic_alpn = params.alpn or "h3" result.tuic_congestion_control = params.congestion_control or "cubic" result.tuic_udp_relay_mode = params.udp_relay_mode or "native" local insecure = params.allowinsecure or params.insecure or params.allow_insecure @@ -1641,11 +1643,14 @@ local function curl(url, file, ua, mode) local curl_args = { "-fskL", "-w %{http_code}", "--retry 3", "--connect-timeout 3", "-H 'Accept-Encoding: identity'" } - if ua and ua ~= "" and ua ~= "curl" then - ua = (ua == "passwall") and ("passwall/" .. api.get_version()) or ua - curl_args[#curl_args + 1] = '--user-agent "' .. ua .. '"' + + ua = (ua and ua ~= "") and ua or "passwall" + ua = (ua == "passwall") and ("passwall/" .. api.get_version()) or ua + curl_args[#curl_args + 1] = '--user-agent "' .. ua .. '"' + if not ua:lower():find("clash", 1, true) then + curl_args[#curl_args + 1] = get_headers() end - curl_args[#curl_args + 1] = get_headers() + local return_code, result if mode == "direct" then return_code, result = api.curl_base(url, file, curl_args) @@ -2126,6 +2131,7 @@ local execute = function() local cfgid = value[".name"] local remark = value.remark or "" local url = value.url or "" + local tmp_file, ua local url_is_local if fs.access(url) then @@ -2134,7 +2140,7 @@ local execute = function() url_is_local = true tmp_file = url else - local ua = value.user_agent + ua = value.user_agent local access_mode = value.access_mode local result = (not access_mode) and "自动" or (access_mode == "direct" and "直连" or (access_mode == "proxy" and "代理" or "自动")) log('正在订阅:【' .. remark .. '】' .. url .. ' [' .. result .. ']') @@ -2157,6 +2163,7 @@ local execute = function() if not manual_sub and old_md5 == new_md5 then log('订阅:【' .. remark .. '】没有变化,无需更新。') else + raw_data = parseClashNode(raw_data) parse_link(raw_data, "2", remark, value) uci:set(appname, cfgid, "md5", new_md5) end diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index ec5d2a93..4841ca81 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 PKG_VERSION:=26.5.1 -PKG_RELEASE:=41 +PKG_RELEASE:=42 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ @@ -15,7 +15,6 @@ PKG_CONFIG_DEPENDS:= \ CONFIG_PACKAGE_$(PKG_NAME)_Nftables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_IPv6_Nat \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Client \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Shadowsocks_Libev_Server \ @@ -24,16 +23,19 @@ PKG_CONFIG_DEPENDS:= \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Client \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_ShadowsocksR_Libev_Server \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_Simple_Obfs \ - CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_SingBox \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_tuic_client \ CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin LUCI_TITLE:=LuCI support for PassWall 2 LUCI_PKGARCH:=all LUCI_DEPENDS:=+coreutils +coreutils-base64 +coreutils-nohup +curl \ - +ip-full +libuci-lua +lua +luci-compat +luci-lib-jsonc +resolveip +tcping \ - +xray-core +geoview +v2ray-geoip +v2ray-geosite \ - +unzip \ + +ip-full +libuci-lua +lua +luci-compat +luci-lib-jsonc +lyaml +resolveip +tcping \ + +geoview +v2ray-geoip +v2ray-geosite \ + +unzip + +PACKAGE_$(PKG_NAME)_Basic_Core_Xray:xray-core \ + +PACKAGE_$(PKG_NAME)_Basic_Core_SingBox:sing-box \ + +PACKAGE_$(PKG_NAME)_Basic_Core_All:xray-core \ + +PACKAGE_$(PKG_NAME)_Basic_Core_All:sing-box \ +PACKAGE_$(PKG_NAME)_INCLUDE_Haproxy:haproxy \ +PACKAGE_$(PKG_NAME)_INCLUDE_Hysteria:hysteria \ +PACKAGE_$(PKG_NAME)_INCLUDE_NaiveProxy:naiveproxy \ @@ -49,17 +51,29 @@ LUCI_DEPENDS:=+coreutils +coreutils-base64 +coreutils-nohup +curl \ +PACKAGE_$(PKG_NAME)_INCLUDE_SingBox:sing-box \ +PACKAGE_$(PKG_NAME)_INCLUDE_tuic_client:tuic-client \ +PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin:v2ray-plugin \ - +PACKAGE_$(PKG_NAME)_INCLUDE_IPv6_Nat:ip6tables-mod-nat define Package/$(PKG_NAME)/config menu "Configuration" + depends on PACKAGE_$(PKG_NAME) -config PACKAGE_$(PKG_NAME)_INCLUDE_IPv6_Nat - depends on PACKAGE_ip6tables - bool "Include IPv6 Nat" - default n +choice + prompt "Basic Core" + default PACKAGE_$(PKG_NAME)_Basic_Core_All if aarch64||i386||x86_64 + default PACKAGE_$(PKG_NAME)_Basic_Core_Xray -if PACKAGE_$(PKG_NAME) + config PACKAGE_$(PKG_NAME)_Basic_Core_Xray + bool "Xray" + select PACKAGE_xray-core + + config PACKAGE_$(PKG_NAME)_Basic_Core_SingBox + bool "SingBox" + select PACKAGE_sing-box + + config PACKAGE_$(PKG_NAME)_Basic_Core_All + bool "All" + select PACKAGE_xray-core + select PACKAGE_sing-box +endchoice config PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy bool "Iptables Transparent Proxy" @@ -74,6 +88,7 @@ config PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy select PACKAGE_iptables-mod-iprange select PACKAGE_iptables-mod-socket select PACKAGE_iptables-mod-tproxy + select PACKAGE_ip6tables-mod-nat select PACKAGE_kmod-ipt-nat default y if ! PACKAGE_firewall4 @@ -143,10 +158,6 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_Simple_Obfs select PACKAGE_simple-obfs-client default y -config PACKAGE_$(PKG_NAME)_INCLUDE_SingBox - bool "Include Sing-Box" - default y - config PACKAGE_$(PKG_NAME)_INCLUDE_tuic_client bool "Include tuic-client" depends on aarch64||arm||i386||x86_64 @@ -158,7 +169,6 @@ config PACKAGE_$(PKG_NAME)_INCLUDE_V2ray_Plugin select PACKAGE_v2ray-plugin default y if aarch64||arm||i386||x86_64 -endif endmenu endef diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua index cb0d9ae1..cba6733b 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/ray.lua @@ -397,13 +397,6 @@ o.validate = function(self, value) return api.trim(value:gsub("[\r\n]", "")) end -o = s:option(ListValue, _n("ech_ForceQuery"), translate("ECH Query Policy"), translate("Controls the policy used when performing DNS queries for ECH configuration.")) -o.default = "full" -o:value("none") -o:value("half") -o:value("full") -o:depends({ [_n("ech")] = true }) - -- [[ REALITY ]] -- o = s:option(Value, _n("reality_publicKey"), translate("Public Key")) o:depends({ [_n("tls")] = true, [_n("reality")] = true }) diff --git a/luci-app-passwall2/luasrc/passwall2/util_hysteria2.lua b/luci-app-passwall2/luasrc/passwall2/util_hysteria2.lua index 6fd0103b..1b8ef901 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_hysteria2.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_hysteria2.lua @@ -45,7 +45,7 @@ function gen_config(var) local local_http_port = var["local_http_port"] local local_http_username = var["local_http_username"] local local_http_password = var["local_http_password"] - local server_host = var["server_host"] or node.address + local server_host = var["server_host"] or (node.address or ""):lower() local server_port = var["server_port"] or node.port if api.is_ipv6(server_host) then diff --git a/luci-app-passwall2/luasrc/passwall2/util_naiveproxy.lua b/luci-app-passwall2/luasrc/passwall2/util_naiveproxy.lua index 8d146208..b515c75f 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_naiveproxy.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_naiveproxy.lua @@ -13,7 +13,7 @@ function gen_config(var) local run_type = var["run_type"] local local_addr = var["local_addr"] local local_port = var["local_port"] - local server_host = var["server_host"] or node.address + local server_host = var["server_host"] or (node.address or ""):lower() local server_port = var["server_port"] or node.port if api.is_ipv6(server_host) then diff --git a/luci-app-passwall2/luasrc/passwall2/util_shadowsocks.lua b/luci-app-passwall2/luasrc/passwall2/util_shadowsocks.lua index e543d915..5978991b 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_shadowsocks.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_shadowsocks.lua @@ -37,7 +37,7 @@ function gen_config(var) return end local node = uci:get_all("passwall2", node_id) - local server_host = var["server_host"] or node.address + local server_host = var["server_host"] or (node.address or ""):lower() local server_port = var["server_port"] or node.port local local_addr = var["local_addr"] local local_port = var["local_port"] diff --git a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua index 68b13073..0116accb 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua @@ -176,6 +176,8 @@ function gen_outbound(flag, node, tag, proxy_table) tag = tag .. ":" .. remarks end + node.address = (node.address or ""):lower() + result = { _id = node_id, _flag = flag, @@ -1312,7 +1314,7 @@ function gen_config(var) end if #ut_nodes == 0 then return nil end local valid_nodes = {} - for i = 1, #ut_nodes do + for i = 1, #(ut_nodes or {}) do local ut_node_id = ut_nodes[i] local ut_node_tag = "ut-" .. ut_node_id local is_new_ut_node = true @@ -1853,6 +1855,7 @@ function gen_config(var) local nodes_domain = {} local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u') string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w) + w = (w or ""):lower() table.insert(nodes_domain, w) end) if #nodes_domain > 0 then @@ -2350,6 +2353,7 @@ function gen_front_dns_config(var) local node_domain = {} local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u') string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w) + w = (w or ""):lower() table.insert(node_domain, w) end) if #node_domain > 0 then diff --git a/luci-app-passwall2/luasrc/passwall2/util_tuic.lua b/luci-app-passwall2/luasrc/passwall2/util_tuic.lua index a1ccce45..9695c4f4 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_tuic.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_tuic.lua @@ -12,7 +12,7 @@ function gen_config(var) local node = uci:get_all("passwall2", node_id) local local_addr = var["local_addr"] local local_port = var["local_port"] - local server_host = var["server_host"] or node.address + local server_host = var["server_host"] or (node.address or ""):lower() local server_port = var["server_port"] or node.port local loglevel = var["loglevel"] or "warn" diff --git a/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/luci-app-passwall2/luasrc/passwall2/util_xray.lua index 273c668f..27c7c9f0 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -149,6 +149,8 @@ function gen_outbound(flag, node, tag, proxy_table) tag = tag .. ":" .. remarks end + node.address = (node.address or ""):lower() + result = { _id = node_id, _flag = flag, @@ -187,8 +189,7 @@ function gen_outbound(flag, node, tag, proxy_table) if not node.tls_CertByName then return "" end return node.tls_CertByName end)(), - echConfigList = (node.ech == "1") and node.ech_config or nil, - echForceQuery = (node.ech == "1") and (node.ech_ForceQuery or "full") or nil + echConfigList = (node.ech == "1") and node.ech_config or nil } or nil, realitySettings = (node.stream_security == "reality") and { serverName = node.tls_serverName, @@ -481,7 +482,7 @@ function gen_config_server(node) local settings = nil local routing = nil local outbounds = { - {protocol = "freedom", tag = "direct"}, {protocol = "blackhole", tag = "blocked"} + { protocol = "freedom", tag = "direct", settings = { finalRules = {{ action = "allow" }}}}, { protocol = "blackhole", tag = "blocked" } } if node.protocol == "vmess" or node.protocol == "vless" then @@ -603,6 +604,9 @@ function gen_config_server(node) mark = 255, interface = node.outbound_node_iface } + }, + settings = { + finalRules = {{ action = "allow" }} } } sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.outbound_node_iface)) @@ -1024,7 +1028,7 @@ function gen_config(var) blc_nodes = _node.balancing_node end local valid_nodes = {} - for i = 1, #blc_nodes do + for i = 1, #(blc_nodes or {}) do local blc_node_id = blc_nodes[i] local blc_node_tag = "blc-" .. blc_node_id local is_new_blc_node = true @@ -1259,6 +1263,9 @@ function gen_config(var) mark = 255, interface = node.iface } + }, + settings = { + finalRules = {{ action = "allow" }} } } sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface)) @@ -1628,6 +1635,7 @@ function gen_config(var) local domain = {} local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u') string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w) + w = (w or ""):lower() table.insert(domain, "full:" .. w) end) if #domain > 0 then @@ -1658,10 +1666,21 @@ function gen_config(var) address = direct_dns_udp_server, port = tonumber(direct_dns_udp_port) or 53, network = "udp", - nonIPQuery = "skip", - blockTypes = { - 65 - } + nonIPQuery = (api.compare_versions(xray_version, "<", "26.4.25")) and "skip" or nil, -- Todo is to remove it + blockTypes = (api.compare_versions(xray_version, "<", "26.4.25")) and { 65 } or nil, -- Todo is to remove it + rules = (api.compare_versions(xray_version, ">", "26.4.17")) and { + { + qtype = "1,28", + action = "hijack" + }, + { + qtype = 65, + action = "reject", + }, + { + action = "direct" + } + } or nil }, proxySettings = { tag = "direct" @@ -1672,7 +1691,16 @@ function gen_config(var) address = remote_dns_udp_server, port = tonumber(remote_dns_udp_port) or 53, network = _remote_dns_proto or "tcp", - nonIPQuery = "reject" + nonIPQuery = (api.compare_versions(xray_version, "<", "26.4.25")) and "reject" or nil, -- Todo is to remove it + rules = (api.compare_versions(xray_version, ">", "26.4.17")) and { + { + qtype = "1,28", + action = "hijack" + }, + { + action = "reject" + } + } or nil } } local type_dns = direct_type_dns @@ -1921,7 +1949,8 @@ function gen_config(var) protocol = "freedom", tag = "direct", settings = { - domainStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP" + domainStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP", + finalRules = {{ action = "allow" }} }, streamSettings = { sockopt = { @@ -2051,7 +2080,7 @@ function gen_proto_config(var) end table.insert(outbounds, { - protocol = "freedom", tag = "direct", settings = {keep = ""} + protocol = "freedom", tag = "direct", settings = {finalRules = {{ action = "allow" }}}, sockopt = {mark = 255} }) local config = { @@ -2149,6 +2178,7 @@ function gen_front_dns_config(var) local node_domain = {} local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u') string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w) + w = (w or ""):lower() table.insert(node_domain, "full:" .. w) end) if #node_domain > 0 then diff --git a/luci-app-passwall2/luasrc/view/passwall2/node_config/link_share_man.htm b/luci-app-passwall2/luasrc/view/passwall2/node_config/link_share_man.htm index e6631f3b..e6c4a5d2 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/node_config/link_share_man.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/node_config/link_share_man.htm @@ -402,16 +402,19 @@ local current_node = map:get(section) } else if (v_transport === "grpc") { info.path = opt.get(dom_prefix + "grpc_serviceName").value; } - if (info.path && info.path != "") { - info.path = encodeURI(info.path); - } info.net = v_transport; info.security = opt.get(dom_prefix + "security").value || "auto"; + info.scy = info.security; if (opt.get(dom_prefix + "tls").checked) { var v_security = "tls"; info.tls = "tls"; info.sni = opt.get(dom_prefix + "tls_serverName").value; + info.alpn = opt.get(dom_prefix + "alpn")?.value; + if (info.alpn === "default") info.alpn = undefined; + info.fp = opt.get(dom_prefix + "fingerprint")?.value; + info.pcs = opt.get(dom_prefix + "tls_pinSHA256")?.value; + opt.get(dom_prefix + "tls_allowInsecure")?.checked && (info.insecure = "1"); } opt.get(dom_prefix + "tcp_fast_open")?.checked && (info.tfo = "1"); @@ -1344,9 +1347,16 @@ local current_node = map:get(section) opt.set(dom_prefix + 'address', unbracketIP(ssm.add)); opt.set(dom_prefix + 'port', ssm.port); opt.set(dom_prefix + 'uuid', ssm.id); + opt.set(dom_prefix + 'security', ssm.security || ssm.scy || "auto"); opt.set(dom_prefix + 'tls', ssm.tls === "tls"); if (ssm.tls === "tls") { opt.set(dom_prefix + 'tls_serverName', ssm.sni || ssm.host); + opt.set(dom_prefix + 'alpn', ssm.alpn || "default"); + if (ssm.fp && ssm.fp !== "") { + opt.set(dom_prefix + 'utls', true); + opt.set(dom_prefix + 'fingerprint', ssm.fp); + } + opt.set(dom_prefix + 'tls_pinSHA256', ssm.pcs || ""); opt.set( dom_prefix + 'tls_allowInsecure', !((ssm.allowinsecure ?? '0') === '0' && (ssm.allowInsecure ?? '0') === '0' && (ssm.insecure ?? '0') === '0') @@ -1792,7 +1802,8 @@ local current_node = map:get(section) opt.get(dom_prefix + 'port').blur(); } } else { - s.innerHTML = "<%:Invalid Share URL Format%>: " + ssu[0]; + s.innerHTML = "<%:Invalid Share URL Format%>"; + sessionStorage.removeItem("fromUrl"); return false; } s.innerHTML = "<%:Import Finished %>"; diff --git a/luci-app-passwall2/po/zh-cn/passwall2.po b/luci-app-passwall2/po/zh-cn/passwall2.po index ec3ccca0..baa0ec3e 100644 --- a/luci-app-passwall2/po/zh-cn/passwall2.po +++ b/luci-app-passwall2/po/zh-cn/passwall2.po @@ -1684,12 +1684,6 @@ msgstr "ECH 配置" msgid "ECH Key" msgstr "ECH 密钥" -msgid "ECH Query Policy" -msgstr "ECH 查询策略" - -msgid "Controls the policy used when performing DNS queries for ECH configuration." -msgstr "控制使用 DNS 查询 ECH 配置时的策略。" - msgid "ECH Query Domain" msgstr "ECH 查询域名" diff --git a/luci-app-passwall2/root/usr/share/passwall2/helper_dnsmasq.lua b/luci-app-passwall2/root/usr/share/passwall2/helper_dnsmasq.lua index c15d7d37..44fa7fd8 100644 --- a/luci-app-passwall2/root/usr/share/passwall2/helper_dnsmasq.lua +++ b/luci-app-passwall2/root/usr/share/passwall2/helper_dnsmasq.lua @@ -270,6 +270,7 @@ function add_rule(var) fwd_dns = LOCAL_DNS uci:foreach(appname, "nodes", function(t) local function process_address(address) + address = (address or ""):lower() if address == "engage.cloudflareclient.com" then return end if datatypes.hostname(address) then set_domain_dns(address, fwd_dns) diff --git a/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua index f02703f9..ab9aa6e5 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua +++ b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua @@ -23,6 +23,7 @@ local uci = api.uci local fs = api.fs local log = api.log local i18n = api.i18n +local lyaml = require "lyaml" local has_ss = api.is_finded("ss-redir") local has_ss_rust = api.is_finded("sslocal") @@ -477,6 +478,292 @@ local function set_ss_implementation(ss_type, result) return result end +local function parseClashNode(node, add_mode, group, sub_cfg) + local sub_allowinsecure = DEFAULT_ALLOWINSECURE + local sub_ss_type = DEFAULT_SS_TYPE + local sub_trojan_type = DEFAULT_TROJAN_TYPE + local sub_vmess_type = DEFAULT_VMESS_TYPE + local sub_vless_type = DEFAULT_VLESS_TYPE + local sub_hysteria2_type = DEFAULT_HYSTERIA2_TYPE + if sub_cfg then + if sub_cfg.allowInsecure and sub_cfg.allowInsecure ~= "1" then + sub_allowinsecure = nil + end + local ss_type = sub_cfg.ss_type or "global" + if ss_type ~= "global" and core_has[ss_type] then + sub_ss_type = ss_type + end + local trojan_type = sub_cfg.trojan_type or "global" + if trojan_type ~= "global" and core_has[trojan_type] then + sub_trojan_type = trojan_type + end + local vmess_type = sub_cfg.vmess_type or "global" + if vmess_type ~= "global" and core_has[vmess_type] then + sub_vmess_type = vmess_type + end + local vless_type = sub_cfg.vless_type or "global" + if vless_type ~= "global" and core_has[vless_type] then + sub_vless_type = vless_type + end + local hysteria2_type = sub_cfg.hysteria2_type or "global" + if hysteria2_type ~= "global" and core_has[hysteria2_type] then + sub_hysteria2_type = hysteria2_type + end + end + local result = { + timeout = 60, + add_mode = add_mode, -- `0` for manual configuration, `1` for import, `2` for subscription + group = group + } + result.remarks = node.name + result.address = node.server + result.port = node.port + + if node.type == 'ss' then + result = set_ss_implementation(sub_ss_type, result) + if not result then return nil end + result.method = node.cipher + result.password = node.password + if node.plugin == "obfs" then + result.plugin = "obfs-local" + elseif node.plugin == "v2ray-plugin" then + result.plugin = "v2ray-plugin" + end + if node["plugin-opts"] then + if node.plugin == "obfs" then + local plugin_opts = "" + local opts_mode = node["plugin-opts"].mode + if opts_mode then + plugin_opts = plugin_opts .. "obfs=" .. opts_mode .. ";" + end + local opts_host = node["plugin-opts"].host + if opts_host then + plugin_opts = plugin_opts .. "obfs-host=" .. opts_host + end + result.plugin_opts = plugin_opts + elseif node.plugin == "v2ray-plugin" then + local plugin_opts = "" + local opts_mode = node["plugin-opts"].mode + local opts_tls = node["plugin-opts"].tls + if opts_tls then + plugin_opts = plugin_opts .. "tls;" + end + local opts_skip_cert_verify = node["plugin-opts"]["skip-cert-verify"] + local opts_host = node["plugin-opts"].host + if opts_host then + plugin_opts = plugin_opts .. "host=" .. opts_host .. ";" + end + local opts_path = node["plugin-opts"].path + local opts_mux = node["plugin-opts"].mux + if node["plugin-opts"].headers then + --todo + end + result.plugin_opts = plugin_opts + end + end + elseif node.type == 'ssr' then + if not has_ssr then + log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "SSR", "shadowsocksr-libev")) + return nil + end + result.type = "SSR" + result.method = node.cipher + result.password = node.password + result.obfs = node.obfs + result.protocol = node.protocol + result.obfs_param = node["obfs-param"] + result.protocol_param = node["protocol-param"] + elseif node.type == 'vmess' then + if sub_vmess_type == "sing-box" and has_singbox then + result.type = 'sing-box' + elseif sub_vmess_type == "xray" and has_xray then + result.type = "Xray" + else + log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "VMess", "VMess")) + return nil + end + result.protocol = 'vmess' + result.uuid = node.uuid + result.alter_id = node.alterId + result.security = node.cipher or "auto" + result.tcp_fast_open = node.tfo + result.tls = "0" + if node.tls then + result.tls = "1" + result.tls_serverName = node.servername or "" + local insecure = node["skip-cert-verify"] + result.tls_allowInsecure = insecure and "1" or "0" + if sub_allowinsecure then + result.tls_allowInsecure = "1" + end + end + result.transport = node.network and string.lower(node.network) or "tcp" + if result.type == "sing-box" and result.transport == "raw" then + result.transport = "tcp" + elseif result.type == "Xray" and result.transport == "tcp" then + result.transport = "raw" + end + if result.transport == 'ws' then + local ws_opts = node["ws-opts"] + if ws_opts then + if ws_opts.headers then + result.ws_host = ws_opts.headers.Host or ws_opts.headers.host + end + if ws_opts.path then + result.ws_path = ws_opts.path + if ws_opts["max-early-data"] then + if result.type == "sing-box" then + result.ws_enableEarlyData = "1" + result.ws_maxEarlyData = tonumber(ws_opts["max-early-data"]) + result.ws_earlyDataHeaderName = "Sec-WebSocket-Protocol" + elseif result.type == "Xray" then + result.ws_path = result.ws_path .. "?ed=" .. ws_opts["max-early-data"] + end + end + end + end + elseif result.transport == 'h2' then + local h2_opts = node["h2-opts"] + if h2_opts then + if result.type == "sing-box" then + result.http_path = h2_opts.path + result.http_host = h2_opts.host + elseif result.type == "Xray" then + result.xhttp_mode = "stream-one" + result.xhttp_path = h2_opts.path + result.xhttp_host = h2_opts.host + end + end + elseif result.transport == 'grpc' then + local grpc_opts = node["grpc-opts"] + if grpc_opts then + result.grpc_serviceName = grpc_opts["grpc-service-name"] + end + end + elseif node.type == 'vless' then + if sub_vless_type == "sing-box" and has_singbox then + result.type = 'sing-box' + elseif sub_vless_type == "xray" and has_xray then + result.type = "Xray" + else + log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "VLESS", "VLESS")) + return nil + end + result.protocol = "vless" + result.uuid = node.uuid + result.tcp_fast_open = node.tfo + result.encryption = node.cipher or "none" + result.flow = node.flow + result.tls = "0" + if node.tls then + result.tls = "1" + result.tls_serverName = node.servername or "" + local insecure = node["skip-cert-verify"] + result.tls_allowInsecure = insecure and "1" or "0" + if sub_allowinsecure then + result.tls_allowInsecure = "1" + end + end + result.transport = node.network and string.lower(node.network) or "tcp" + if result.type == "sing-box" and result.transport == "raw" then + result.transport = "tcp" + elseif result.type == "Xray" and result.transport == "tcp" then + result.transport = "raw" + end + if result.transport == 'ws' then + local ws_opts = node["ws-opts"] + if ws_opts then + if ws_opts.headers then + result.ws_host = ws_opts.headers.Host or ws_opts.headers.host + end + if ws_opts.path then + result.ws_path = ws_opts.path + if ws_opts["max-early-data"] then + if result.type == "sing-box" then + result.ws_enableEarlyData = "1" + result.ws_maxEarlyData = tonumber(ws_opts["max-early-data"]) + result.ws_earlyDataHeaderName = "Sec-WebSocket-Protocol" + elseif result.type == "Xray" then + result.ws_path = result.ws_path .. "?ed=" .. ws_opts["max-early-data"] + end + end + end + end + end + elseif node.type == 'trojan' then + if sub_trojan_type == "sing-box" and has_singbox then + result.type = 'sing-box' + result.protocol = 'trojan' + elseif sub_trojan_type == "xray" and has_xray then + result.type = 'Xray' + result.protocol = 'trojan' + else + log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "Trojan", "Trojan")) + return nil + end + result.password = node.password + result.tls = '1' + result.tls_serverName = node.sni or "" + local insecure = node["skip-cert-verify"] + result.tls_allowInsecure = insecure and "1" or "0" + if sub_allowinsecure then + result.tls_allowInsecure = "1" + end + if node.alpn then + --todo + end + if node.network == "grpc" then + if node["grpc-opts"] then + result.grpc_serviceName = node["grpc-opts"]["grpc-service-name"] + end + elseif node.network == "ws" then + local ws_opts = node["ws-opts"] + if ws_opts then + result.ws_path = ws_opts.path + if ws_opts.headers then + result.ws_host = ws_opts.headers.Host or ws_opts.headers.host + end + end + end + elseif node.type == 'anytls' then + if has_singbox then + result.type = 'sing-box' + result.protocol = "anytls" + else + log(2, i18n.translatef("Skip the %s node because the %s core program is not installed.", "AnyTLS", "AnyTLS", "Sing-Box 1.12")) + return nil + end + result.password = node.password + result.tls = '1' + result.tls_serverName = node.sni or "" + local insecure = node["skip-cert-verify"] + result.tls_allowInsecure = insecure and "1" or "0" + if sub_allowinsecure then + result.tls_allowInsecure = "1" + end + end + if not result.remarks or result.remarks == "" then + if result.address and result.port then + result.remarks = result.address .. ':' .. result.port + else + result.remarks = "NULL" + end + end + return result +end + +-- Processing Clash data +local function processClashData(content, add_mode, group, sub_cfg) + local results = {} + for i, node in ipairs(content.proxies or {}) do + local result = parseClashNode(node, add_mode, group, sub_cfg) + if result then + table.insert(results, result) + end + end + return results +end + -- Processing data local function processData(szType, content, add_mode, group, sub_cfg) --log(2, content, add_mode, group) @@ -650,9 +937,14 @@ local function processData(szType, content, add_mode, group, sub_cfg) result.httpupgrade_host = info.host result.httpupgrade_path = info.path end - if not info.security then result.security = "auto" end + result.security = info.security or info.scy or "auto" if info.tls == "tls" or info.tls == "1" then result.tls = "1" + result.alpn = info.alpn + if info.fp and info.fp ~= "" then + result.utls = "1" + result.fingerprint = info.fp + end result.tls_serverName = (info.sni and info.sni ~= "") and info.sni or info.host result.tls_pinSHA256 = info.pcs result.tls_CertByName = info.vcn @@ -2001,7 +2293,7 @@ local function parse_link(raw, add_mode, group, sub_cfg) if sub_cfg then cfgid = sub_cfg[".name"] end - local nodes, szType + local nodes, szType, clashTable local node_list = {} -- ssd appear to be in this format, starting with ssd://. if raw:find('ssd://') then @@ -2022,11 +2314,45 @@ local function parse_link(raw, add_mode, group, sub_cfg) end nodes = servers else - -- Formats other than ssd - if add_mode == "1" then - nodes = split(raw, "\n") + -- Try parseYAML, if success, is clash. + local yamlTable = lyaml.load(raw) + if yamlTable and type(yamlTable) == "table" then + -- clash + szType = "clash" + clashTable = yamlTable else - nodes = split(base64Decode(raw):gsub("\r\n", "\n"), "\n") + -- Formats other than ssd + if add_mode == "1" then + nodes = split(raw, "\n") + else + nodes = split(base64Decode(raw):gsub("\r\n", "\n"), "\n") + end + end + end + + function nodeFilter(node) + if node then + if node.error_msg then + log(2, i18n.translatef("Discard node: %s, Reason:", node.remarks) .. " " .. node.error_msg) + elseif not node.type then + log(2, i18n.translatef("Discard node: %s, Reason:", node.remarks) .. " " .. i18n.translatef("No usable binary was found.")) + elseif (add_mode == "2" and is_filter_keyword(sub_cfg, node.remarks)) or not node.address or node.remarks == "NULL" or node.address == "127.0.0.1" or + (not datatypes.hostname(node.address) and not (api.is_ip(node.address))) then + log(2, i18n.translatef("Discard filter nodes: %s type node %s", node.type, node.remarks)) + else + tinsert(node_list, node) + end + if add_mode == "2" then + get_subscribe_info(cfgid, node.remarks) + end + end + end + + if szType == "clash" and clashTable then + nodes = {} + local clashNodes = processClashData(clashTable, add_mode, group, sub_cfg) + for _, v in ipairs(clashNodes) do + nodeFilter(v) end end @@ -2052,21 +2378,7 @@ local function parse_link(raw, add_mode, group, sub_cfg) log(2, i18n.translatef("Skip unknown types:") .. " " .. szType) end -- log(2, result) - if result then - if result.error_msg then - log(2, i18n.translatef("Discard node: %s, Reason:", result.remarks) .. " " .. result.error_msg) - elseif not result.type then - log(2, i18n.translatef("Discard node: %s, Reason:", result.remarks) .. " " .. i18n.translatef("No usable binary was found.")) - elseif (add_mode == "2" and is_filter_keyword(sub_cfg, result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or - (not datatypes.hostname(result.address) and not (api.is_ip(result.address))) then - log(2, i18n.translatef("Discard filter nodes: %s type node %s", result.type, result.remarks)) - else - tinsert(node_list, result) - end - if add_mode == "2" then - get_subscribe_info(cfgid, result.remarks) - end - end + nodeFilter(result) end, function (err) --log(2, err) log(2, v, i18n.translatef("Parsing error, skip this node.")) diff --git a/luci-app-socat/Makefile b/luci-app-socat/Makefile index 7c25cdb0..8976bf96 100644 --- a/luci-app-socat/Makefile +++ b/luci-app-socat/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-socat PKG_VERSION:=1.1 -PKG_RELEASE:=2 +PKG_RELEASE:=3 PKG_MAINTAINER:=Lienol \ sbwml diff --git a/luci-app-socat/htdocs/luci-static/resources/view/socat.js b/luci-app-socat/htdocs/luci-static/resources/view/socat.js index 07c1fcd6..d42eae4a 100644 --- a/luci-app-socat/htdocs/luci-static/resources/view/socat.js +++ b/luci-app-socat/htdocs/luci-static/resources/view/socat.js @@ -6,7 +6,7 @@ 'require poll'; 'require network'; -var callServiceList = rpc.declare({ +const callServiceList = rpc.declare({ object: 'service', method: 'list', params: ['name'], @@ -14,15 +14,15 @@ var callServiceList = rpc.declare({ }); return view.extend({ - load: function () { + load() { return Promise.all([ network.getHostHints() ]); }, - render: function (data) { - var m, s, o; - var hostHints = data[0]; + render(data) { + let m, s, o; + const hostHints = data[0]; m = new form.Map('luci_socat', _('Socat'), _('Socat is a relay for bidirectional data transfer between two independent data channels.')); @@ -38,13 +38,13 @@ return view.extend({ s.sortable = true; s.nodescriptions = true; - var super_handleAdd = s.handleAdd; - s.handleAdd = function (ev) { - var uuid = Array.from({length: 32}, () => Math.floor(Math.random() * 16).toString(16)).join(''); + const super_handleAdd = s.handleAdd; + s.handleAdd = function(ev) { + const uuid = Array.from({ length: 32 }, () => Math.floor(Math.random() * 16).toString(16)).join(''); return super_handleAdd.call(this, ev, uuid); }; - s.modaltitle = function (section_id) { + s.modaltitle = function(section_id) { return _('Socat Configuration'); }; @@ -59,19 +59,19 @@ return view.extend({ return '-'; }; - poll.add(function () { - return callServiceList('luci_socat').then(function (res) { - var instances = (res && res.luci_socat && res.luci_socat.instances) ? res.luci_socat.instances : {}; + poll.add(() => { + return callServiceList('luci_socat').then(res => { + const instances = (res && res.luci_socat && res.luci_socat.instances) ? res.luci_socat.instances : {}; - document.querySelectorAll('tr[data-sid]').forEach(function (row) { - var sid = row.getAttribute('data-sid'); + document.querySelectorAll('tr[data-sid]').forEach(row => { + let sid = row.getAttribute('data-sid'); if (!sid) return; sid = sid.trim(); - var cell = row.querySelector('td[data-name="_status"]'); + const cell = row.querySelector('td[data-name="_status"]'); if (!cell) return; - var isRunning = !!instances[sid]; + const isRunning = !!instances[sid]; cell.innerHTML = isRunning ? '🟢' : '🔴'; }); @@ -80,67 +80,67 @@ return view.extend({ o = s.option(form.Value, 'remarks', _('Remarks')); - o = s.option(form.ListValue, "protocol", _("Protocol Type")); - o.value("port_forwards", _("Port Forwarding")); + o = s.option(form.ListValue, 'protocol', _('Protocol Type')); + o.value('port_forwards', _('Port Forwarding')); o.modalonly = true; - o = s.option(form.ListValue, "family", _("Listen Address Family")); - o.value("", _("IPv4 and IPv6")); - o.value("4", _("IPv4 Only")); - o.value("6", _("IPv6 Only")); - o.depends("protocol", "port_forwards"); + o = s.option(form.ListValue, 'family', _('Listen Address Family')); + o.value('', _('IPv4 and IPv6')); + o.value('4', _('IPv4 Only')); + o.value('6', _('IPv6 Only')); + o.depends('protocol', 'port_forwards'); o.modalonly = true; - o = s.option(form.ListValue, "proto", _("Listen Protocol")); - o.value("tcp", "TCP"); - o.value("udp", "UDP"); - o.depends("protocol", "port_forwards"); - o.textvalue = function (section_id) { - var family_val = uci.get('luci_socat', section_id, 'family'); - var proto_val = uci.get('luci_socat', section_id, 'proto'); + o = s.option(form.ListValue, 'proto', _('Listen Protocol')); + o.value('tcp', 'TCP'); + o.value('udp', 'UDP'); + o.depends('protocol', 'port_forwards'); + o.textvalue = function(section_id) { + const family_val = uci.get('luci_socat', section_id, 'family'); + const proto_val = uci.get('luci_socat', section_id, 'proto'); if (proto_val) { - if (family_val === '4') return 'IPv4-' + proto_val.toUpperCase(); - if (family_val === '6') return 'IPv6-' + proto_val.toUpperCase(); - return 'ALL-' + proto_val.toUpperCase(); + if (family_val === '4') return `IPv4-${proto_val.toUpperCase()}`; + if (family_val === '6') return `IPv6-${proto_val.toUpperCase()}`; + return `ALL-${proto_val.toUpperCase()}`; } }; - o = s.option(form.Value, "listen_port", _("Listen Port")); - o.datatype = "portrange"; + o = s.option(form.Value, 'listen_port', _('Listen Port')); + o.datatype = 'portrange'; o.rmempty = false; - o.depends("protocol", "port_forwards"); + o.depends('protocol', 'port_forwards'); - o = s.option(form.Flag, "reuseaddr", _("Reuse Port"), _("Allow binding to the port even when previous connections are in TIME_WAIT, preventing restart failures.")); - o.default = "1"; + o = s.option(form.Flag, 'reuseaddr', _('Reuse Port'), _('Allow binding to the port even when previous connections are in TIME_WAIT, preventing restart failures.')); + o.default = '1'; o.rmempty = false; o.modalonly = true; - o = s.option(form.ListValue, "dest_proto", _("Destination Protocol")); - o.value("tcp4", "IPv4-TCP"); - o.value("udp4", "IPv4-UDP"); - o.value("tcp6", "IPv6-TCP"); - o.value("udp6", "IPv6-UDP"); - o.depends("protocol", "port_forwards"); + o = s.option(form.ListValue, 'dest_proto', _('Destination Protocol')); + o.value('tcp4', 'IPv4-TCP'); + o.value('udp4', 'IPv4-UDP'); + o.value('tcp6', 'IPv6-TCP'); + o.value('udp6', 'IPv6-UDP'); + o.depends('protocol', 'port_forwards'); o = s.option(form.Value, 'dest_ip', _('Destination Address')); o.rmempty = false; - o.depends("protocol", "port_forwards"); - o.value('127.0.0.1', ('127.0.0.1 (localhost)')); - hostHints.getMACHints().forEach(function (entry) { - var mac = entry[0]; - var hint = entry[1] || mac; - var ip = hostHints.getIPAddrByMACAddr(mac); + o.depends('protocol', 'port_forwards'); + o.value('127.0.0.1', '127.0.0.1 (localhost)'); + hostHints.getMACHints().forEach(entry => { + const mac = entry[0]; + const hint = entry[1] || mac; + const ip = hostHints.getIPAddrByMACAddr(mac); if (ip) - o.value(ip, '%s (%s)'.format(ip, hint)); + o.value(ip, `${ip} (${hint})`); }); - o = s.option(form.Value, "dest_port", _("Destination Port")); - o.datatype = "portrange"; + o = s.option(form.Value, 'dest_port', _('Destination Port')); + o.datatype = 'portrange'; o.rmempty = false; - o = s.option(form.Flag, "firewall_accept", _("Open Firewall Port")); + o = s.option(form.Flag, 'firewall_accept', _('Open Firewall Port')); o.editable = true; - o.default = "0"; + o.default = '0'; o.rmempty = false; return m.render(); diff --git a/luci-app-socat/po/templates/socat.pot b/luci-app-socat/po/templates/socat.pot index da54eb68..b6d455fb 100644 --- a/luci-app-socat/po/templates/socat.pot +++ b/luci-app-socat/po/templates/socat.pot @@ -1,108 +1,97 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -#, fuzzy msgid "" -msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-22 03:30+0800\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language-Team: LANGUAGE \n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=CHARSET\n" -"Content-Transfer-Encoding: 8bit\n" +msgstr "Content-Type: text/plain; charset=UTF-8" -#: htdocs/luci-static/resources/view/socat.js:27 -msgid "Socat" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:27 -msgid "" -"Socat is a relay for bidirectional data transfer between two independent " -"data channels." -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:29 -msgid "Global Settings" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:32 -#: htdocs/luci-static/resources/view/socat.js:51 -msgid "Enabled" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:35 -#: htdocs/luci-static/resources/view/socat.js:84 -msgid "Port Forwarding" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:48 -msgid "Socat Configuration" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:56 -msgid "Status" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:81 -msgid "Remarks" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:83 -msgid "Protocol Type" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:87 -msgid "Listen Address Family" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:88 -msgid "IPv4 and IPv6" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:89 -msgid "IPv4 Only" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:90 -msgid "IPv6 Only" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:94 -msgid "Listen Protocol" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:108 -msgid "Listen Port" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:113 -msgid "Reuse Port" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:113 +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:113 msgid "" "Allow binding to the port even when previous connections are in TIME_WAIT, " "preventing restart failures." msgstr "" -#: htdocs/luci-static/resources/view/socat.js:118 -msgid "Destination Protocol" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:125 +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:125 msgid "Destination Address" msgstr "" -#: htdocs/luci-static/resources/view/socat.js:137 +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:137 msgid "Destination Port" msgstr "" -#: htdocs/luci-static/resources/view/socat.js:141 +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:118 +msgid "Destination Protocol" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:32 +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:51 +msgid "Enabled" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:29 +msgid "Global Settings" +msgstr "" + +#: luci-app-socat/root/usr/share/rpcd/acl.d/luci-app-socat.json:3 +msgid "Grant UCI access for luci-app-socat" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:89 +msgid "IPv4 Only" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:88 +msgid "IPv4 and IPv6" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:90 +msgid "IPv6 Only" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:87 +msgid "Listen Address Family" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:108 +msgid "Listen Port" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:94 +msgid "Listen Protocol" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:141 msgid "Open Firewall Port" msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:35 +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:84 +msgid "Port Forwarding" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:83 +msgid "Protocol Type" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:81 +msgid "Remarks" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:113 +msgid "Reuse Port" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:27 +#: luci-app-socat/root/usr/share/luci/menu.d/luci-app-socat.json:3 +msgid "Socat" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:48 +msgid "Socat Configuration" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:27 +msgid "" +"Socat is a relay for bidirectional data transfer between two independent " +"data channels." +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:56 +msgid "Status" +msgstr "" diff --git a/luci-app-socat/po/zh_Hans/socat.po b/luci-app-socat/po/zh_Hans/socat.po index dba7f362..a0226a71 100644 --- a/luci-app-socat/po/zh_Hans/socat.po +++ b/luci-app-socat/po/zh_Hans/socat.po @@ -11,93 +11,97 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: htdocs/luci-static/resources/view/socat.js:27 -#: root/usr/share/luci/menu.d/luci-app-socat.json:3 -msgid "Socat" -msgstr "" - -#: htdocs/luci-static/resources/view/socat.js:27 -msgid "" -"Socat is a relay for bidirectional data transfer between two independent " -"data channels." -msgstr "Socat 是一个在两个独立数据通道之间进行双向数据转发的工具。" - -#: htdocs/luci-static/resources/view/socat.js:29 -msgid "Global Settings" -msgstr "全局设置" - -#: htdocs/luci-static/resources/view/socat.js:32 -#: htdocs/luci-static/resources/view/socat.js:51 -msgid "Enabled" -msgstr "启用" - -#: htdocs/luci-static/resources/view/socat.js:35 -#: htdocs/luci-static/resources/view/socat.js:84 -msgid "Port Forwarding" -msgstr "端口转发" - -#: htdocs/luci-static/resources/view/socat.js:48 -msgid "Socat Configuration" -msgstr "Socat 配置" - -#: htdocs/luci-static/resources/view/socat.js:56 -msgid "Status" -msgstr "状态" - -#: htdocs/luci-static/resources/view/socat.js:81 -msgid "Remarks" -msgstr "备注" - -#: htdocs/luci-static/resources/view/socat.js:83 -msgid "Protocol Type" -msgstr "协议类型" - -#: htdocs/luci-static/resources/view/socat.js:87 -msgid "Listen Address Family" -msgstr "监听地址族" - -#: htdocs/luci-static/resources/view/socat.js:88 -msgid "IPv4 and IPv6" -msgstr "IPv4 和 IPv6" - -#: htdocs/luci-static/resources/view/socat.js:89 -msgid "IPv4 Only" -msgstr "仅 IPv4" - -#: htdocs/luci-static/resources/view/socat.js:90 -msgid "IPv6 Only" -msgstr "仅 IPv6" - -#: htdocs/luci-static/resources/view/socat.js:94 -msgid "Listen Protocol" -msgstr "监听协议" - -#: htdocs/luci-static/resources/view/socat.js:108 -msgid "Listen Port" -msgstr "监听端口" - -#: htdocs/luci-static/resources/view/socat.js:113 -msgid "Reuse Port" -msgstr "重用端口" - -#: htdocs/luci-static/resources/view/socat.js:113 +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:113 msgid "" "Allow binding to the port even when previous connections are in TIME_WAIT, " "preventing restart failures." msgstr "允许在端口处于 TIME_WAIT 状态时立即重新绑定,避免服务重启失败。" -#: htdocs/luci-static/resources/view/socat.js:118 -msgid "Destination Protocol" -msgstr "目标协议" - -#: htdocs/luci-static/resources/view/socat.js:125 +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:125 msgid "Destination Address" msgstr "目标地址" -#: htdocs/luci-static/resources/view/socat.js:137 +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:137 msgid "Destination Port" msgstr "目标端口" -#: htdocs/luci-static/resources/view/socat.js:141 +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:118 +msgid "Destination Protocol" +msgstr "目标协议" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:32 +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:51 +msgid "Enabled" +msgstr "启用" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:29 +msgid "Global Settings" +msgstr "全局设置" + +#: luci-app-socat/root/usr/share/rpcd/acl.d/luci-app-socat.json:3 +msgid "Grant UCI access for luci-app-socat" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:89 +msgid "IPv4 Only" +msgstr "仅 IPv4" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:88 +msgid "IPv4 and IPv6" +msgstr "IPv4 和 IPv6" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:90 +msgid "IPv6 Only" +msgstr "仅 IPv6" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:87 +msgid "Listen Address Family" +msgstr "监听地址族" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:108 +msgid "Listen Port" +msgstr "监听端口" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:94 +msgid "Listen Protocol" +msgstr "监听协议" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:141 msgid "Open Firewall Port" msgstr "打开防火墙端口" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:35 +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:84 +msgid "Port Forwarding" +msgstr "端口转发" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:83 +msgid "Protocol Type" +msgstr "协议类型" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:81 +msgid "Remarks" +msgstr "备注" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:113 +msgid "Reuse Port" +msgstr "重用端口" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:27 +#: luci-app-socat/root/usr/share/luci/menu.d/luci-app-socat.json:3 +msgid "Socat" +msgstr "" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:48 +msgid "Socat Configuration" +msgstr "Socat 配置" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:27 +msgid "" +"Socat is a relay for bidirectional data transfer between two independent " +"data channels." +msgstr "Socat 是一个在两个独立数据通道之间进行双向数据转发的工具。" + +#: luci-app-socat/htdocs/luci-static/resources/view/socat.js:56 +msgid "Status" +msgstr "状态" diff --git a/luci-app-socat/root/usr/share/rpcd/acl.d/luci-app-socat.json b/luci-app-socat/root/usr/share/rpcd/acl.d/luci-app-socat.json index 0969c8dd..74f55bf5 100644 --- a/luci-app-socat/root/usr/share/rpcd/acl.d/luci-app-socat.json +++ b/luci-app-socat/root/usr/share/rpcd/acl.d/luci-app-socat.json @@ -5,10 +5,10 @@ "ubus": { "service": [ "list" ] }, - "uci": [ "socat" ] + "uci": [ "luci_socat" ] }, "write": { - "uci": [ "socat" ] + "uci": [ "luci_socat" ] } } } diff --git a/luci-app-wechatpush/Makefile b/luci-app-wechatpush/Makefile index 7ec4f6ea..b0550f9b 100755 --- a/luci-app-wechatpush/Makefile +++ b/luci-app-wechatpush/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-wechatpush PKG_VERSION:=3.6.12 -PKG_RELEASE:=3 +PKG_RELEASE:=4 PKG_MAINTAINER:=tty228 PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-wechatpush/po/zh_Hans/wechatpush.po b/luci-app-wechatpush/po/zh_Hans/wechatpush.po index 5e0fc848..4860004d 100644 --- a/luci-app-wechatpush/po/zh_Hans/wechatpush.po +++ b/luci-app-wechatpush/po/zh_Hans/wechatpush.po @@ -1252,8 +1252,8 @@ msgstr "/ (首页登录)" msgid "%s frequent %s login attempts" msgstr "%s 频繁尝试 %s 登录" -msgid "Block Information" -msgstr "封禁信息" +msgid "Frequent Login Attempts Information" +msgstr "频繁尝试登录信息" msgid "Device %s (%s) frequently attempted %s %s login" msgstr "设备 %s (%s) 频繁尝试 %s %s 登录" diff --git a/luci-app-wechatpush/root/usr/share/wechatpush/wechatpush b/luci-app-wechatpush/root/usr/share/wechatpush/wechatpush index 7d34d316..4145b540 100755 --- a/luci-app-wechatpush/root/usr/share/wechatpush/wechatpush +++ b/luci-app-wechatpush/root/usr/share/wechatpush/wechatpush @@ -2356,8 +2356,8 @@ process_login() { if [[ $count -ge $login_max_num && ("$3" == "web_failed_counts" || "$3" == "ssh_failed_counts") ]]; then add_ip_black ${login_ip} && { unset login_counts["$login_ip"] - login_send "$login_ip" "$login_time" "$3" } + login_send "$login_ip" "$login_time" "$3" fi # 正常登录 @@ -2590,7 +2590,7 @@ login_send() { if [ -z "$login_disturb" ] || [ "$login_disturb" -ne "1" ]; then if [[ "$log_type" == *"failed"* ]]; then local login_title=$(translate "%s frequent %s login attempts" "$login_ip" "$log_type_short") - local login_content_info="${str_splitline}${str_title_start}$(translate "Block Information")${str_title_end}" + local login_content_info="${str_splitline}${str_title_start}$(translate "Frequent Login Attempts Information")${str_title_end}" log_change "${disturb_text} $(translate "Device %s (%s) frequently attempted %s %s login" "$login_ip" "$login_ip_attribution" "$log_type_short" "$login_mode")" else local login_title=$(translate "%s logged into router via %s" "$login_ip" "$log_type_short") diff --git a/luci-theme-material3/Readme.md b/luci-theme-material3/Readme.md index dbc3d411..1fa4935c 100644 --- a/luci-theme-material3/Readme.md +++ b/luci-theme-material3/Readme.md @@ -3,7 +3,8 @@ A Material Design 3 theme for OpenWrt Luci. This is a personal project based on the LuCI Bootstrap theme, imitating the Material Design 3 style. -**Nice to initiate an issue or PR!** +**Nice to initiate an issue or PR, and star! ⭐** + ## ✨ Preview @@ -19,37 +20,61 @@ This is a personal project based on the LuCI Bootstrap theme, imitating the Mate ### 🎨 All color schemes
- - - + + + - - - + + + - - - + + + - - - + + + - - - + + + - - - + + +
Baseline PurpleTrust BlueGrowth GreenBaseline PurpleTrust BlueGrowth Green
+ + + + + +
Calm TealWarm OrangeVivid OrangeCalm TealWarm OrangeVivid Orange
+ + + + + +
Sakura RedExpressive RoseMonochrome GraySakura RedExpressive RoseMonochrome Gray
+ + + + + +
+## 📦 Release +Download prebuilt packages from [GitHub Releases](https://github.com/KawaiiHachimi/luci-theme-material3/releases). + +- `luci-theme-material3-*.apk` -> OpenWrt LuCI 25.12+ +- `luci-theme-material3_*.ipk` -> OpenWrt LuCI 23.05~24.10 + ## 🙏 Credits - [OpenWrt Luci](https://github.com/openwrt/luci) - [Cursor](https://www.cursor.com/) @@ -57,4 +82,4 @@ This is a personal project based on the LuCI Bootstrap theme, imitating the Mate - [MDUI](https://www.mdui.org/zh-cn/) - [Fluent UI System Icons](https://github.com/microsoft/fluentui-system-icons) - [Material Design 3](https://m3.material.io/) -- [Reddit - LuCI theme bootstrap: Some modifications using userstyle](https://www.reddit.com/r/openwrt/comments/qmtmpf/lucithemebootstrap_some_modifications_using/) \ No newline at end of file +- [Reddit - LuCI theme bootstrap: Some modifications using userstyle](https://www.reddit.com/r/openwrt/comments/qmtmpf/lucithemebootstrap_some_modifications_using/) diff --git a/luci-theme-material3/htdocs/luci-static/material3/cascade.css b/luci-theme-material3/htdocs/luci-static/material3/cascade.css index 51507531..317108eb 100644 --- a/luci-theme-material3/htdocs/luci-static/material3/cascade.css +++ b/luci-theme-material3/htdocs/luci-static/material3/cascade.css @@ -1,6 +1,6 @@ /*! * LuCI Bootstrap Theme - * Material3 Mod + * Material3 Mod by KawaiiHachimi * Copyright 2012 Nut & Bolt * By David Menting * Based on Bootstrap v1.4.0 @@ -131,7 +131,8 @@ --material3-shell-background: var(--surface); --material3-shell-foreground: var(--on-surface); --material3-shell-state: var(--surface-container); - --material3-header-background: color-mix(in srgb, var(--material3-shell-background) 88%, transparent); + --material3-header-background: color-mix(in srgb, var(--material3-shell-background) 72%, transparent); + --material3-header-scrolled-background: color-mix(in srgb, var(--material3-card-background) 72%, transparent); --material3-header-foreground: var(--material3-shell-foreground); --material3-header-state: var(--material3-shell-state); --material3-indicator-hover-background: var(--primary-container); @@ -1512,7 +1513,8 @@ select { .cbi-value { display: flex; flex-wrap: wrap; - margin-bottom: 18px; + margin: 0; + padding: 9px 0; align-items: baseline; } @@ -2079,12 +2081,17 @@ header p { header { background: var(--material3-header-background); box-shadow: none; - transition: box-shadow .3s; + transition: background-color .3s, box-shadow .3s; padding: 0; } header.with-shadow { - box-shadow: none; + background: var(--material3-header-scrolled-background); + box-shadow: 0 .5px 1.5px 0 rgba(0, 0, 0, 19%), 0 0 1px 0 rgba(0, 0, 0, 3.9%); +} + +body.sidebar-open header { + background: var(--material3-shell-background); } .menu-btn { @@ -2166,7 +2173,7 @@ header.with-shadow { top: var(--material3-header-height); left: 0; bottom: 0; - z-index: 760; + z-index: 820; box-sizing: border-box; width: var(--material3-sidebar-width); padding: 0; @@ -2246,6 +2253,17 @@ header.with-shadow { opacity: 1; visibility: visible; } + + .cbi-dropdown[open], + .cbi-dropdown[open]>ul.dropdown { + max-width: 100%; + } + + .cbi-dropdown[open]>ul.dropdown { + box-sizing: border-box; + width: 100%; + min-width: 100%; + } } .nav { @@ -2282,6 +2300,22 @@ header.with-shadow { transition: .3s; } +.nav-menu-title { + display: block; + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + white-space: nowrap; + -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent); + mask-image: linear-gradient(90deg, #000 calc(100% - 24px), transparent); +} + +.nav a:hover>.nav-menu-title, +.nav a:focus>.nav-menu-title { + -webkit-mask-image: none; + mask-image: none; +} + .nav>li>a::before { content: ''; width: 1.5rem; @@ -2412,7 +2446,6 @@ header { border: none; backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); - box-shadow: none; background: var(--md-sys-color-surface-translucent); } @@ -2443,7 +2476,8 @@ header { transition: .3s; padding: 1rem 1.5rem; line-height: 18px; - display: block; + display: flex; + align-items: center; border-radius: 2rem; overflow: hidden; text-shadow: none; @@ -2786,6 +2820,7 @@ footer ul.breadcrumb { white-space: pre-wrap; overflow: auto; width: 100%; + border-radius: var(--material3-card-radius); } .modal>.button-row { @@ -2822,8 +2857,16 @@ body.modal-overlay-active #modal_overlay { justify-content: center; padding: 24px; background: - radial-gradient(circle at top left, color-mix(in srgb, var(--primary-container) 64%, transparent), transparent 34rem), - radial-gradient(circle at top right, color-mix(in srgb, var(--tertiary-container) 58%, transparent), transparent 30rem), + radial-gradient(circle at 18% 8%, + color-mix(in srgb, var(--primary-container) 38%, transparent), + transparent 28rem), + linear-gradient(135deg, + color-mix(in srgb, var(--background) 78%, var(--primary-container)) 0%, + color-mix(in srgb, var(--background) 92%, var(--primary-container)) 46%, + color-mix(in srgb, var(--surface-container-low) 88%, var(--primary)) 100%), + linear-gradient(180deg, + color-mix(in srgb, var(--surface-container-low) 48%, transparent) 0%, + transparent 52%), var(--background); } @@ -2833,10 +2876,14 @@ body.modal-overlay-active #modal_overlay { max-width: 420px; margin: 0; padding: 32px; - border: 0; + border: 1px solid color-mix(in srgb, var(--outline-variant) 52%, transparent); border-radius: 28px; - background: var(--material3-card-background); - box-shadow: var(--material3-card-shadow); + background: color-mix(in srgb, var(--material3-card-background) 78%, transparent); + backdrop-filter: blur(24px) saturate(1.12); + -webkit-backdrop-filter: blur(24px) saturate(1.12); + box-shadow: + 0 12px 36px color-mix(in srgb, var(--outline) 18%, transparent), + 0 1px 1px color-mix(in srgb, var(--outline) 10%, transparent); color: var(--on-surface); } @@ -2954,8 +3001,8 @@ body.modal-overlay-active #modal_overlay { } .modal.login .material3-login-header img { - width: 48px; - height: 48px; + width: 64px; + height: 64px; } .modal.login .material3-login-header h2 { @@ -3434,16 +3481,18 @@ body.modal-overlay-active #modal_overlay { } .cbi-progressbar { + border: 0; border-radius: 3px; position: relative; min-width: 170px; height: 4px; margin: 1.4em 0 4px 0; - background: var(--background-color-medium); + background: var(--surface-container-high); + box-shadow: none; } .cbi-progressbar>div { - background: var(--primary-color-medium); + background: var(--primary); height: 100%; transition: width .25s ease-in; width: 0%; @@ -3605,7 +3654,7 @@ header [data-indicator] { font-size: 12px; font-weight: 600; text-transform: none; - color: color-mix(in srgb, var(--material3-header-foreground) 80%, var(--material3-header-background)); + color: color-mix(in srgb, var(--material3-header-foreground) 60%, var(--material3-header-background)); background: transparent; transition: background-color .2s ease, color .2s ease; } @@ -3966,7 +4015,7 @@ th[data-sort-direction="desc"]::after { background: var(--default-btn-background, var(--background-color-high)); color: var(--on-color, var(--text-color-high)); text-shadow: none; - border-radius: 16px; + border-radius: 12px; box-shadow: none; cursor: default; line-height: 1.2em; @@ -4413,8 +4462,9 @@ div.cbi-value var.cbi-tooltip-container, .cbi-section-node, .table { - margin: 0 1px 10px 1px; - width: calc(100% - 2px); + box-sizing: border-box; + margin: 0 0 10px; + width: 100%; border-radius: var(--material3-card-radius); background: var(--material3-card-background); box-shadow: var(--material3-card-shadow); @@ -4432,44 +4482,33 @@ div.cbi-value var.cbi-tooltip-container, stroke: var(--text-color-highest) !important; } -/* Status overview */ -body[data-page="admin-status-overview"] .cbi-section { +/* Status overview and dashboard */ +:is(body[data-page="admin-status-overview"], body[data-page=""]) .cbi-section, +:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .cbi-section, +:is(body[data-page="admin-dashboard"], body[data-page="admin"]) .cbi-section-node { + border: 0; + background: transparent; + box-shadow: none; +} + +:is(body[data-page="admin-status-overview"], body[data-page=""]) .cbi-section { margin: 0 0 18px; padding: 0; width: auto; - border: 0; border-radius: 0; - background: transparent; - box-shadow: none; } -body[data-page="admin-status-overview"] .cbi-title h3 { - color: var(--on-surface); - font-size: 18px; - line-height: 36px; -} - -body[data-page="admin-status-overview"] .cbi-section>.cbi-title+div { - margin: 1px; - width: calc(100% - 2px); - border-radius: 0; - background: transparent; - box-shadow: none; -} - -body[data-page="admin-status-overview"] .cbi-section-descr, -body[data-page="admin-status-overview"] .cbi-map-descr, -body[data-page="admin-status-overview"] .cbi-section p, -body[data-page="admin-status-overview"] .cbi-section li, -body[data-page="admin-status-overview"] .table .td, -body[data-page="admin-status-overview"] .table .th { +:is(body[data-page="admin-status-overview"], body[data-page=""]) :is(.cbi-section-descr, + .cbi-map-descr, + .cbi-section p, + .cbi-section li) { color: var(--on-surface-variant); font-size: 13px; font-weight: 400; line-height: 18px; } -body[data-page="admin-status-overview"] .cbi-title .label { +:is(body[data-page="admin-status-overview"], body[data-page=""]) .cbi-title .label { display: inline-flex !important; align-items: center; justify-content: center; @@ -4489,7 +4528,7 @@ body[data-page="admin-status-overview"] .cbi-title .label { transition: background-color .2s ease, color .2s ease, opacity .2s ease; } -body[data-page="admin-status-overview"] .cbi-title .label::before { +:is(body[data-page="admin-status-overview"], body[data-page=""]) .cbi-title .label::before { content: ''; width: 17px; height: 17px; @@ -4498,70 +4537,65 @@ body[data-page="admin-status-overview"] .cbi-title .label::before { mask: url("icons/eye-off.svg") center/contain no-repeat; } -body[data-page="admin-status-overview"] .cbi-title .label:hover { +:is(body[data-page="admin-status-overview"], body[data-page=""]) .cbi-title .label:hover, +:is(body[data-page="admin-status-overview"], body[data-page=""]) .cbi-title .label.notice:hover { background: var(--material3-menu-hover-background); color: var(--on-surface); } -body[data-page="admin-status-overview"] .cbi-title .label.notice { +:is(body[data-page="admin-status-overview"], body[data-page=""]) .cbi-title .label.notice { background: transparent; color: color-mix(in srgb, var(--on-surface-variant) 72%, transparent); } -body[data-page="admin-status-overview"] .cbi-title .label.notice::before { +:is(body[data-page="admin-status-overview"], body[data-page=""]) .cbi-title .label.notice::before { -webkit-mask-image: url("icons/eye.svg"); mask-image: url("icons/eye.svg"); } -body[data-page="admin-status-overview"] .cbi-title .label.notice:hover { - background: var(--material3-menu-hover-background); - color: var(--on-surface); +:is(body[data-page="admin-status-overview"], body[data-page=""]) .network-status-table { + gap: 10px; + margin: 0 0 10px; } -body[data-page="admin-status-overview"] .network-status-table { - width: calc(100% + 16px); - margin: 0 -8px 8px; -} - -body[data-page="admin-status-overview"] .network-status-table .ifacebox { - margin: 8px; - width: calc(100% - 16px); +:is(body[data-page="admin-status-overview"], body[data-page=""]) .network-status-table .ifacebox { + flex: 1 1 260px; + margin: 0; padding: 8px; - border: 0; - border-radius: var(--material3-card-radius); - background: var(--material3-card-background); - box-shadow: var(--material3-card-shadow); + text-align: left; + white-space: normal; } -body[data-page="admin-status-overview"] .ifacebox .ifacebox-head { +:is(body[data-page="admin-status-overview"], body[data-page=""]) .ifacebox .ifacebox-head { padding: 2px 0 8px; border: 0; - border-radius: 16px 16px 0 0; - background: transparent; + border-radius: 0; + background: transparent !important; color: var(--on-surface-variant); font-weight: 400; opacity: .9; + text-shadow: none; } -body[data-page="admin-status-overview"] .ifacebox .ifacebox-head strong { +:is(body[data-page="admin-status-overview"], body[data-page=""]) .ifacebox .ifacebox-head strong { font-weight: 500; } -body[data-page="admin-status-overview"] .ifacebox .ifacebox-head.active { +:is(body[data-page="admin-status-overview"], body[data-page=""]) .ifacebox .ifacebox-head.active { border: 0; - background: transparent; + background: transparent !important; color: var(--on-surface); text-shadow: none; } -body[data-page="admin-status-overview"] .ifacebox .ifacebox-body { +:is(body[data-page="admin-status-overview"], body[data-page=""]) .ifacebox .ifacebox-body { padding: 0; line-height: 1.35; } -body[data-page="admin-status-overview"] .ifacebadge, -body[data-page="admin-status-overview"] .ifacebadge.large, -body[data-page="admin-status-overview"] .network-status-table .ifacebox-body .ifacebadge { +:is(body[data-page="admin-status-overview"], body[data-page=""]) .ifacebadge, +:is(body[data-page="admin-status-overview"], body[data-page=""]) .ifacebadge.large, +:is(body[data-page="admin-status-overview"], body[data-page=""]) .network-status-table .ifacebox-body .ifacebadge { padding: 8px; border: 1px solid color-mix(in srgb, var(--outline) 48%, transparent); border-radius: 16px; @@ -4570,55 +4604,19 @@ body[data-page="admin-status-overview"] .network-status-table .ifacebox-body .if text-shadow: none; } -body[data-page="admin-status-overview"] .table { - margin: 1px; - width: calc(100% - 2px); - border-radius: var(--material3-card-radius); - background: var(--material3-card-background); - box-shadow: var(--material3-card-shadow); -} - -body[data-page="admin-status-overview"] .cbi-progressbar { - border: 0; - background: var(--surface-container-high); - box-shadow: none; -} - -body[data-page="admin-status-overview"] .cbi-progressbar>div { - background: var(--primary); -} - -body[data-page="admin-status-overview"] .table .td, -body[data-page="admin-status-overview"] .table .th, -body[data-page="admin-status-overview"] .tr.cbi-section-table-titles, -body[data-page="admin-status-overview"] .cbi-section-table-titles.named::before, -body[data-page="admin-status-overview"] .cbi-section-table-descr.named::before, -body[data-page="admin-status-overview"] .cbi-section-table-row[data-title]::before { - border-top: 0; -} - -body[data-page="admin-status-overview"] .tr.cbi-section-table-titles { - background: transparent; -} - /* Dashboard cards */ -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .cbi-section, -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .cbi-section-node { - border: 0; - background: transparent; - box-shadow: none; -} - -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .dashboard-bg.box-s1 { +:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .dashboard-bg.box-s1, +:is(body[data-page="admin-status-overview"], body[data-page=""]) .network-status-table .ifacebox { border: 0; border-radius: var(--material3-card-radius); background: var(--material3-card-background); box-shadow: var(--material3-card-shadow); } -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .thead.dashboard-bg, -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .tr.dashboard-bg, -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .tfoot.dashboard-bg { +:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard :is(.thead.dashboard-bg, + .tr.dashboard-bg, + .tfoot.dashboard-bg, + .table) { border-radius: 0; background: transparent; box-shadow: none; @@ -4627,34 +4625,28 @@ body[data-page="admin-status-overview"] .tr.cbi-section-table-titles { :is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .table { margin: 0; width: auto; - border-radius: 0; - background: transparent; - box-shadow: none; } -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard hr, -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .table .td, -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .table .th { +:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .router-status-wifi .assoclist.devices-info .tr> :nth-child(3) { + width: 30%; +} + +:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .router-status-wifi .assoclist.devices-info .tr> :nth-child(4) { + width: 20%; + min-width: 85px; + white-space: normal; + word-break: keep-all; + overflow-wrap: normal; +} + +:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard hr { border: 0; } -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard h3, -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .settings-info, -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .table .td, -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .table .th { +:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard :is(h3, .settings-info) { color: var(--on-surface-variant); } -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .cbi-progressbar { - border: 0; - background: var(--surface-container-high); - box-shadow: none; -} - -:is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard .cbi-progressbar>div { - background: var(--primary); -} - :is(body[data-page="admin-status-iptables"], body[data-page="admin-status-nftables-iptables"]) .cbi-tabmenu { margin-top: 28px; } @@ -4662,4 +4654,4 @@ body[data-page="admin-status-overview"] .tr.cbi-section-table-titles { [data-darkmode="true"] :is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard img.svgmonotone, [data-mode="dark"] :is(body[data-page="admin-dashboard"], body[data-page="admin"], body[data-page=""]) .Dashboard img.svgmonotone { filter: invert(1) brightness(1.65) contrast(.82) opacity(.86); -} \ No newline at end of file +} diff --git a/luci-theme-material3/htdocs/luci-static/material3/mobile.css b/luci-theme-material3/htdocs/luci-static/material3/mobile.css index 621002a4..c2cbbd10 100644 --- a/luci-theme-material3/htdocs/luci-static/material3/mobile.css +++ b/luci-theme-material3/htdocs/luci-static/material3/mobile.css @@ -2,10 +2,15 @@ header h3 a { display: none !important; } -footer { - width: 100%; - padding-left: 12px; - padding-right: 12px; +:is(body[data-page="admin-status-overview"], body[data-page=""]) .cbi-title .label { + width: 32px; + padding: 0; + font-size: 0 !important; + gap: 0; +} + +:is(body[data-page="admin-status-overview"], body[data-page=""]) .cbi-title .label::before { + flex: 0 0 auto; } @media screen and (max-device-width: 600px) { @@ -143,6 +148,7 @@ footer { } .tr.table-titles, + .tr.dashboard-table-titles, .cbi-section-table-titles, .cbi-section-table-descr { display: none; @@ -189,6 +195,7 @@ footer { max-width: 100%; padding: 3px 2px; flex-basis: 100%; + text-align: left; } .hide-sm, @@ -301,25 +308,41 @@ footer { padding: 8px; } - [data-page="admin-status-overview"] .td[width="33%"] { + :is([data-page="admin-status-overview"], [data-page=""]) .td[width="33%"] { flex-basis: 33%; } - [data-page="admin-status-overview"] .table { - margin: 4px; - width: calc(100% - 8px); - } - - [data-page="admin-status-overview"] .table, - [data-page="admin-status-overview"] .tr, - [data-page="admin-status-overview"] .td, - [data-page="admin-status-overview"] .th, - [data-page="admin-status-overview"] .tr[data-title]::before, - [data-page="admin-status-overview"] .td[data-title]::before { + :is([data-page="admin-status-overview"], [data-page=""]) .table, + :is([data-page="admin-status-overview"], [data-page=""]) .tr, + :is([data-page="admin-status-overview"], [data-page=""]) .td, + :is([data-page="admin-status-overview"], [data-page=""]) .th, + :is([data-page="admin-status-overview"], [data-page=""]) .tr[data-title]::before, + :is([data-page="admin-status-overview"], [data-page=""]) .td[data-title]::before { border-top: 0; border-bottom: 0; } + :is([data-page="admin-dashboard"], [data-page="admin"], [data-page=""]) :is(.Dashboard, .dashboard-bg.box-s1) .td[data-title]::before { + word-break: keep-all; + } + + :is([data-page="admin-dashboard"], [data-page="admin"], [data-page=""]) .Dashboard .devices-list .devices-info .tr .td { + padding: 3px !important; + } + + :is([data-page="admin-dashboard"], [data-page="admin"], [data-page=""]) .Dashboard .router-status-wifi.dashboard-bg.box-s1 .assoclist.devices-info.table>.tr:not(.dashboard-table-titles)>.td { + display: block; + flex: 0 0 50% !important; + width: 50% !important; + max-width: 50%; + min-width: 0; + } + + :is([data-page="admin-dashboard"], [data-page="admin"], [data-page=""]) .Dashboard .router-status-wifi.dashboard-bg.box-s1 .assoclist.devices-info .cbi-progressbar { + width: 100% !important; + min-width: 0 !important; + } + header .pull-right .label { white-space: normal; display: inline-block; @@ -530,4 +553,4 @@ footer { .ifacebox .ifacebox-body>* { margin: .125em; } -} \ No newline at end of file +} diff --git a/luci-theme-material3/htdocs/luci-static/resources/menu-material3.js b/luci-theme-material3/htdocs/luci-static/resources/menu-material3.js index 61140702..3b3f3c1c 100644 --- a/luci-theme-material3/htdocs/luci-static/resources/menu-material3.js +++ b/luci-theme-material3/htdocs/luci-static/resources/menu-material3.js @@ -6,6 +6,7 @@ return baseclass.extend({ __init__() { ui.menu.load().then((tree) => this.render(tree)); this.initNavigationShell(); + this.initDashboardTables(); }, createRipple(ev, target) { @@ -64,11 +65,16 @@ return baseclass.extend({ const sidebar = document.querySelector('.sidebar'); const overlay = document.querySelector('.sidebar-overlay'); const header = document.querySelector('header'); + const collapsedQuery = window.matchMedia('(max-width: 854px), (max-device-width: 854px)'); if (header) { - window.addEventListener('scroll', () => { - header.classList.toggle('with-shadow', window.scrollY > 8); - }); + const updateHeaderShadow = () => { + header.classList.toggle('with-shadow', collapsedQuery.matches && window.scrollY > 8); + }; + + updateHeaderShadow(); + window.addEventListener('scroll', updateHeaderShadow, { passive: true }); + collapsedQuery.addEventListener('change', updateHeaderShadow); } if (!button || !sidebar || !overlay) @@ -78,6 +84,7 @@ return baseclass.extend({ sidebar.classList.remove('active'); overlay.classList.remove('active'); button.classList.remove('active'); + document.body.classList.remove('sidebar-open'); document.body.style.overflow = ''; button.setAttribute('aria-expanded', 'false'); }; @@ -88,6 +95,7 @@ return baseclass.extend({ const isOpen = sidebar.classList.contains('active'); button.classList.toggle('active', isOpen); + document.body.classList.toggle('sidebar-open', isOpen); document.body.style.overflow = isOpen ? 'hidden' : ''; button.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); }); @@ -117,7 +125,87 @@ return baseclass.extend({ if (target) this.createRipple(ev, target); }); - }, + + sidebar.addEventListener('mouseover', ev => { + const link = ev.target.closest('.nav a'); + const title = link ? link.querySelector('.nav-menu-title') : null; + + if (title && title.scrollWidth > title.clientWidth) + title.scrollTo({ left: title.scrollWidth - title.clientWidth, behavior: 'smooth' }); + }); + + sidebar.addEventListener('mouseout', ev => { + const link = ev.target.closest('.nav a'); + const title = link ? link.querySelector('.nav-menu-title') : null; + + if (title && !link.contains(ev.relatedTarget)) + title.scrollTo({ left: 0, behavior: 'smooth' }); + }); + }, + + isDashboardPage() { + const path = (L.env.requestpath || []).join('/'); + + return path == '' || path == 'admin' || path == 'admin/dashboard'; + }, + + initDashboardTables() { + if (!this.isDashboardPage()) + return; + + const sync = () => this.updateDashboardTables(); + + if (document.readyState == 'loading') + document.addEventListener('DOMContentLoaded', sync, { once: true }); + else + sync(); + + const target = document.querySelector('#maincontent') || document.body; + let queued = false; + const observer = new MutationObserver(() => { + if (queued) + return; + + queued = true; + window.setTimeout(() => { + queued = false; + sync(); + }, 100); + }); + + if (target) + observer.observe(target, { childList: true, subtree: true }); + }, + + updateDashboardTables() { + document.querySelectorAll('.Dashboard, .dashboard-bg.box-s1').forEach(scope => { + scope.querySelectorAll('.table').forEach(table => { + const rows = Array.prototype.filter.call(table.children, child => child.classList && child.classList.contains('tr')); + const headerRow = rows.find(row => row.querySelector('.th')); + + if (!headerRow) + return; + + const titles = Array.prototype.map.call(headerRow.children, cell => + cell.classList && cell.classList.contains('th') ? cell.textContent.trim() : ''); + + headerRow.classList.add('dashboard-table-titles'); + + rows.forEach(row => { + if (row === headerRow) + return; + + Array.prototype.forEach.call(row.children, (cell, index) => { + if (!cell.classList || !cell.classList.contains('td') || !titles[index]) + return; + + if (!cell.getAttribute('data-title')) + cell.setAttribute('data-title', titles[index]); + }); + }); + }); + }); + }, render(tree) { let node = tree; @@ -148,7 +236,7 @@ return baseclass.extend({ const className = 'tabmenu-item-%s %s'.format(child.name, activeClass); ul.appendChild(E('li', { 'class': className }, [ - E('a', { 'href': L.url(url, child.name) }, [ _(child.title) ] )])); + E('a', { 'href': L.url(url, child.name) }, [_(child.title)])])); if (isActive) activeNode = child; @@ -204,7 +292,7 @@ return baseclass.extend({ const li = E('li', attrs, [ E('a', linkAttrs, [ - _(child.title), + E('span', { 'class': 'nav-menu-title' }, [_(child.title)]), ]), submenu ]); @@ -230,7 +318,7 @@ return baseclass.extend({ : index === 0; ul.appendChild(E('li', { 'class': isActive ? 'active' : '' }, [ - E('a', { 'href': L.url(child.name) }, [ _(child.title) ]) + E('a', { 'href': L.url(child.name) }, [_(child.title)]) ])); if (isActive) diff --git a/luci-theme-material3/ucode/template/themes/material3/footer.ut b/luci-theme-material3/ucode/template/themes/material3/footer.ut index 084dba41..bc700627 100644 --- a/luci-theme-material3/ucode/template/themes/material3/footer.ut +++ b/luci-theme-material3/ucode/template/themes/material3/footer.ut @@ -8,6 +8,9 @@ / {{ version.distname }} {{ version.distversion }} ({{ version.distrevision }}) + / + + luci-theme-material3 {% if (lua_active): %} / {{ _('Lua compatibility mode active') }} {% endif %} diff --git a/luci-theme-material3/ucode/template/themes/material3/header.ut b/luci-theme-material3/ucode/template/themes/material3/header.ut index 37ae6f08..7db55a79 100644 --- a/luci-theme-material3/ucode/template/themes/material3/header.ut +++ b/luci-theme-material3/ucode/template/themes/material3/header.ut @@ -45,7 +45,7 @@ - + {% if (node?.css): %} {% endif %} diff --git a/quickstart/Makefile b/quickstart/Makefile index a33c9bb5..725fb02e 100644 --- a/quickstart/Makefile +++ b/quickstart/Makefile @@ -10,8 +10,8 @@ include $(TOPDIR)/rules.mk PKG_ARCH_quickstart:=$(ARCH) PKG_NAME:=quickstart -PKG_VERSION:=0.12.1 -PKG_RELEASE:=3 +PKG_VERSION:=0.12.2 +PKG_RELEASE:=4 PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://github.com/linkease/istore-packages/releases/download/prebuilt/ PKG_HASH:=skip