🎄 Sync 2026-05-06 19:35:12

This commit is contained in:
github-actions[bot] 2026-05-06 19:35:12 +08:00
parent b3aea13cdd
commit f58b6dd3a1
50 changed files with 1787 additions and 626 deletions

View File

@ -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)?

View File

@ -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)?

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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"

View File

@ -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.

View File

@ -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

View File

@ -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"
}

17
ipv6-neigh/Cargo.lock generated
View File

@ -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",
]

View File

@ -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

View File

@ -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) \

View File

@ -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"
}

View File

@ -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<String>,
#[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(&registered).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::<Ipv6Addr>() {
if let Err(e) = send_icmpv6_echo(addr) {
debug!("probe failed for {}: {}", ip_str, e);
}
} else if let Ok(addr) = ip_str.parse::<Ipv4Addr>() {
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<u8>) -> 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)
}

View File

@ -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') {

View File

@ -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 \

View File

@ -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过滤模式保存逻辑

View File

@ -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")

View File

@ -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

View File

@ -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" }

View File

@ -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(

View File

@ -0,0 +1,486 @@
#!/usr/bin/lua
-- Copyright(c): lwb1978 20252027
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

View File

@ -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

View File

@ -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

View File

@ -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 })

View File

@ -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

View File

@ -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

View File

@ -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"]

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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 = "<font color='red'><%:Invalid Share URL Format%></font>: " + ssu[0];
s.innerHTML = "<font color='red'><%:Invalid Share URL Format%></font>";
sessionStorage.removeItem("fromUrl");
return false;
}
s.innerHTML = "<font color='green'><%:Import Finished %></font>";

View File

@ -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 查询域名"

View File

@ -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)

View File

@ -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."))

View File

@ -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 <lawlienol@gmail.com> \
sbwml <admin@cooluc.com>

View File

@ -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();

View File

@ -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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""

View File

@ -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 "状态"

View File

@ -5,10 +5,10 @@
"ubus": {
"service": [ "list" ]
},
"uci": [ "socat" ]
"uci": [ "luci_socat" ]
},
"write": {
"uci": [ "socat" ]
"uci": [ "luci_socat" ]
}
}
}

View File

@ -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 <tty228@yeah.net>
PKG_CONFIG_DEPENDS:= \

View File

@ -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 登录"

View File

@ -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")

View File

@ -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
<table>
<tr>
@ -19,37 +20,61 @@ This is a personal project based on the LuCI Bootstrap theme, imitating the Mate
### 🎨 All color schemes
<table>
<tr>
<th>Baseline Purple</th>
<th>Trust Blue</th>
<th>Growth Green</th>
<th width="33.33%" align="center">Baseline Purple</th>
<th width="33.33%" align="center">Trust Blue</th>
<th width="33.33%" align="center">Growth Green</th>
</tr>
<tr>
<td><img src="imgs/baseline.jpg" /></td>
<td><img src="imgs/blue.jpg" /></td>
<td><img src="imgs/green.jpg" /></td>
<td width="33.33%" align="center">
<img src="imgs/baseline.jpg" width="100%" />
</td>
<td width="33.33%" align="center">
<img src="imgs/blue.jpg" width="100%" />
</td>
<td width="33.33%" align="center">
<img src="imgs/green.jpg" width="100%" />
</td>
</tr>
<tr>
<th>Calm Teal</th>
<th>Warm Orange</th>
<th>Vivid Orange</th>
<th width="33.33%" align="center">Calm Teal</th>
<th width="33.33%" align="center">Warm Orange</th>
<th width="33.33%" align="center">Vivid Orange</th>
</tr>
<tr>
<td><img src="imgs/teal.jpg" /></td>
<td><img src="imgs/orange.jpg" /></td>
<td><img src="imgs/vivid-orange.jpg" /></td>
<td width="33.33%" align="center">
<img src="imgs/teal.jpg" width="100%" />
</td>
<td width="33.33%" align="center">
<img src="imgs/orange.jpg" width="100%" />
</td>
<td width="33.33%" align="center">
<img src="imgs/vivid-orange.jpg" width="100%" />
</td>
</tr>
<tr>
<th>Sakura Red</th>
<th>Expressive Rose</th>
<th>Monochrome Gray</th>
<th width="33.33%" align="center">Sakura Red</th>
<th width="33.33%" align="center">Expressive Rose</th>
<th width="33.33%" align="center">Monochrome Gray</th>
</tr>
<tr>
<td><img src="imgs/sakura.jpg" /></td>
<td><img src="imgs/rose.jpg" /></td>
<td><img src="imgs/gray.jpg" /></td>
<td width="33.33%" align="center">
<img src="imgs/sakura.jpg" width="100%" />
</td>
<td width="33.33%" align="center">
<img src="imgs/rose.jpg" width="100%" />
</td>
<td width="33.33%" align="center">
<img src="imgs/gray.jpg" width="100%" />
</td>
</tr>
</table>
## 📦 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/)
- [Reddit - LuCI theme bootstrap: Some modifications using userstyle](https://www.reddit.com/r/openwrt/comments/qmtmpf/lucithemebootstrap_some_modifications_using/)

View File

@ -1,6 +1,6 @@
/*!
* LuCI Bootstrap Theme
* Material3 Mod
* Material3 Mod by KawaiiHachimi <bilibili@att.net>
* Copyright 2012 Nut & Bolt
* By David Menting <david@nut-bolt.nl>
* 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);
}
}

View File

@ -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;
}
}
}

View File

@ -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)

View File

@ -8,6 +8,9 @@
/
<a href="{{ entityencode(version.disturl ?? '#', true) }}" target="_blank" rel="noreferrer">
{{ version.distname }} {{ version.distversion }} ({{ version.distrevision }})</a>
/
<a href="https://github.com/KawaiiHachimi/luci-theme-material3" target="_blank" rel="noreferrer">
luci-theme-material3</a>
{% if (lua_active): %}
/ {{ _('Lua compatibility mode active') }}
{% endif %}

View File

@ -45,7 +45,7 @@
<meta name="darkreader-lock">
<link rel="stylesheet" href="{{ media }}/cascade.css">
<link rel="stylesheet" media="only screen and (max-device-width: 854px)" href="{{ media }}/mobile.css" />
<link rel="icon" href="{{ media }}/logo.svg" sizes="any">
<link rel="icon" href="{{ media }}/logo.svg" sizes="any">
{% if (node?.css): %}
<link rel="stylesheet" href="{{ resource }}/{{ node.css }}">
{% endif %}

View File

@ -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