mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-08-02 07:29:28 +08:00
🎄 Sync 2026-05-06 19:35:12
This commit is contained in:
parent
b3aea13cdd
commit
f58b6dd3a1
@ -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)?
|
||||
|
||||
@ -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)?
|
||||
|
||||
8
hickory-dns/files/etc/hickory-dns/0.zone
Normal file
8
hickory-dns/files/etc/hickory-dns/0.zone
Normal 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.
|
||||
10
hickory-dns/files/etc/hickory-dns/127.0.0.1.zone
Normal file
10
hickory-dns/files/etc/hickory-dns/127.0.0.1.zone
Normal 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.
|
||||
8
hickory-dns/files/etc/hickory-dns/255.zone
Normal file
8
hickory-dns/files/etc/hickory-dns/255.zone
Normal 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.
|
||||
@ -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"
|
||||
|
||||
10
hickory-dns/files/etc/hickory-dns/ipv6_1.zone
Normal file
10
hickory-dns/files/etc/hickory-dns/ipv6_1.zone
Normal 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.
|
||||
11
hickory-dns/files/etc/hickory-dns/localhost.zone
Normal file
11
hickory-dns/files/etc/hickory-dns/localhost.zone
Normal 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
|
||||
@ -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
17
ipv6-neigh/Cargo.lock
generated
@ -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",
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) \
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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(®istered).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send ICMPv6 Echo Request / ICMPv4 Echo Request to all registered neighbours.
|
||||
/// This forces the kernel NUD state machine to verify reachability, generating
|
||||
/// NewNeighbour events with the resulting state (Reachable or Failed).
|
||||
async fn probe_registered_neighbours(registered: &HashMap<(String, String), Instant>) {
|
||||
let now = Instant::now();
|
||||
for ((_, ip_str), last_confirmed) in registered.iter() {
|
||||
// Only probe entries not confirmed recently (older than 30s)
|
||||
if now.duration_since(*last_confirmed) < Duration::from_secs(30) {
|
||||
continue;
|
||||
}
|
||||
if let Ok(addr) = ip_str.parse::<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)
|
||||
}
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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过滤模式保存逻辑
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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(
|
||||
|
||||
486
luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua
Normal file
486
luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua
Normal file
@ -0,0 +1,486 @@
|
||||
#!/usr/bin/lua
|
||||
|
||||
-- Copyright(c): lwb1978 2025–2027
|
||||
|
||||
local api = require("luci.passwall.api")
|
||||
local urlencode = api.UrlEncode
|
||||
local base64 = api.base64Encode
|
||||
local json = api.jsonc
|
||||
|
||||
local function host_format(host)
|
||||
if not host then return "" end
|
||||
local str = host:match("%[(.-)%]") or host
|
||||
if api.datatypes.ip6addr(str) then
|
||||
return "[" .. str .. "]"
|
||||
end
|
||||
return host
|
||||
end
|
||||
|
||||
local function build_alpn(alpn) -- 排序+去重
|
||||
if not alpn then return nil end
|
||||
|
||||
local seen = {}
|
||||
local order = { "h3", "h2", "http/1.1" }
|
||||
|
||||
if type(alpn) == "table" then
|
||||
for _, v in ipairs(alpn) do
|
||||
if v then seen[v] = true end
|
||||
end
|
||||
else
|
||||
seen[tostring(alpn)] = true
|
||||
end
|
||||
|
||||
local t = {}
|
||||
for _, v in ipairs(order) do
|
||||
if seen[v] then table.insert(t, v) end
|
||||
end
|
||||
|
||||
if #t == 0 then return nil end
|
||||
return table.concat(t, ",")
|
||||
end
|
||||
|
||||
local function build_common(node)
|
||||
local o = {}
|
||||
|
||||
o.server = host_format(node.server)
|
||||
o.port = node.port
|
||||
o.name = node.name
|
||||
|
||||
-- ===== TLS =====
|
||||
o.tls = {}
|
||||
|
||||
if node["reality-opts"] then
|
||||
o.tls.security = "reality"
|
||||
o.tls.pbk = node["reality-opts"]["public-key"]
|
||||
o.tls.sid = node["reality-opts"]["short-id"]
|
||||
elseif node.tls then
|
||||
o.tls.security = "tls"
|
||||
end
|
||||
|
||||
o.tls.sni = node.servername or node.sni
|
||||
o.tls.fp = node["client-fingerprint"]
|
||||
o.tls.pcs = node.fingerprint
|
||||
o.tls.insecure = node["skip-cert-verify"] == true
|
||||
|
||||
o.tls.alpn = build_alpn(node.alpn)
|
||||
|
||||
local ech_opts = node["ech-opts"]
|
||||
if ech_opts and ech_opts.enable == true then
|
||||
if ech_opts.config then
|
||||
o.tls.ech = ech_opts.config
|
||||
elseif ech_opts["query-server-name"] then
|
||||
o.tls.ech = ech_opts["query-server-name"] .. "+https://223.5.5.5/dns-query"
|
||||
end
|
||||
end
|
||||
|
||||
-- ===== transport =====
|
||||
o.transport = {}
|
||||
local net = node.network or "tcp"
|
||||
o.transport.type = net
|
||||
|
||||
local function get_first(v)
|
||||
if type(v) == "table" then return v[1] end
|
||||
return v
|
||||
end
|
||||
|
||||
if net == "ws" then
|
||||
local opts = node["ws-opts"]
|
||||
if opts then
|
||||
o.transport.path = opts.path
|
||||
o.transport.host = opts.headers and opts.headers.Host
|
||||
end
|
||||
|
||||
elseif net == "grpc" then
|
||||
local opts = node["grpc-opts"]
|
||||
if opts then
|
||||
o.transport.serviceName = opts["grpc-service-name"]
|
||||
end
|
||||
|
||||
elseif net == "http" then
|
||||
local opts = node["http-opts"]
|
||||
if opts then
|
||||
o.transport.host = get_first(opts.host)
|
||||
o.transport.path = get_first(opts.path)
|
||||
end
|
||||
|
||||
elseif net == "h2" then
|
||||
local opts = node["h2-opts"]
|
||||
if opts then
|
||||
o.transport.host = get_first(opts.host)
|
||||
o.transport.path = opts.path
|
||||
end
|
||||
|
||||
elseif net == "xhttp" then
|
||||
local opts = node["xhttp-opts"]
|
||||
if opts then
|
||||
o.transport.host = opts.host
|
||||
o.transport.path = opts.path
|
||||
o.transport.mode = opts.mode
|
||||
|
||||
local extra = {}
|
||||
|
||||
-- headers
|
||||
if opts.headers then
|
||||
extra.headers = opts.headers
|
||||
end
|
||||
|
||||
if opts["x-padding-bytes"] then
|
||||
extra.xPaddingBytes = opts["x-padding-bytes"]
|
||||
end
|
||||
|
||||
if opts["no-grpc-header"] ~= nil then
|
||||
extra.noGRPCHeader = opts["no-grpc-header"]
|
||||
end
|
||||
|
||||
if opts["sc-max-each-post-bytes"] then
|
||||
extra.scMaxEachPostBytes = opts["sc-max-each-post-bytes"]
|
||||
end
|
||||
|
||||
if opts["sc-min-posts-interval-ms"] then
|
||||
extra.scMinPostsIntervalMs = opts["sc-min-posts-interval-ms"]
|
||||
end
|
||||
|
||||
-- xmux
|
||||
if opts["reuse-settings"] then
|
||||
local r = opts["reuse-settings"]
|
||||
local xmux = {}
|
||||
|
||||
if r["max-concurrency"] then xmux.maxConcurrency = r["max-concurrency"] end
|
||||
if r["max-connections"] then xmux.maxConnections = tonumber(r["max-connections"]) end
|
||||
if r["c-max-reuse-times"] then xmux.cMaxReuseTimes = tonumber(r["c-max-reuse-times"]) end
|
||||
if r["h-max-request-times"] then xmux.hMaxRequestTimes = r["h-max-request-times"] end
|
||||
if r["h-max-reusable-secs"] then xmux.hMaxReusableSecs = r["h-max-reusable-secs"] end
|
||||
if r["h-keep-alive-period"] then xmux.hKeepAlivePeriod = tonumber(r["h-keep-alive-period"]) end
|
||||
|
||||
if next(xmux) then extra.xmux = xmux end
|
||||
end
|
||||
|
||||
-- download-settings
|
||||
if opts["download-settings"] then
|
||||
local d = opts["download-settings"]
|
||||
local ds = {}
|
||||
|
||||
if d.server then ds.address = d.server end
|
||||
if d.port then ds.port = d.port end
|
||||
|
||||
ds.network = "xhttp"
|
||||
|
||||
-- TLS
|
||||
if d.tls then
|
||||
ds.security = "tls"
|
||||
ds.tlsSettings = {}
|
||||
|
||||
if d.servername then
|
||||
ds.tlsSettings.serverName = d.servername
|
||||
end
|
||||
if d["skip-cert-verify"] == true then
|
||||
ds.tlsSettings.allowInsecure = true
|
||||
end
|
||||
if d["client-fingerprint"] then
|
||||
ds.tlsSettings.fingerprint = d["client-fingerprint"]
|
||||
end
|
||||
if d.fingerprint then
|
||||
ds.tlsSettings.pinnedPeerCertSha256 = d.fingerprint
|
||||
end
|
||||
if d.alpn then
|
||||
ds.tlsSettings.alpn = d.alpn
|
||||
end
|
||||
end
|
||||
|
||||
-- xhttpSettings
|
||||
local xs = {}
|
||||
|
||||
if d.path then xs.path = d.path end
|
||||
if d.host then xs.host = d.host end
|
||||
|
||||
if next(xs) then
|
||||
ds.xhttpSettings = xs
|
||||
end
|
||||
|
||||
extra.downloadSettings = ds
|
||||
end
|
||||
|
||||
if next(extra) then
|
||||
o.transport.extra = json.stringify(extra)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return o
|
||||
end
|
||||
|
||||
-- VLESS
|
||||
local function encode_vless(node)
|
||||
local o = build_common(node)
|
||||
|
||||
local link = "vless://" .. node.uuid .. "@" .. o.server .. ":" .. o.port
|
||||
local p = {}
|
||||
|
||||
if node.flow then table.insert(p, "flow=" .. urlencode(node.flow)) end
|
||||
if node.encryption then table.insert(p, "encryption=" .. urlencode(node.encryption)) end
|
||||
|
||||
-- TLS
|
||||
if o.tls.security then table.insert(p, "security=" .. o.tls.security) end
|
||||
if o.tls.pbk then table.insert(p, "pbk=" .. urlencode(o.tls.pbk)) end
|
||||
if o.tls.sid then table.insert(p, "sid=" .. urlencode(o.tls.sid)) end
|
||||
if o.tls.sni then table.insert(p, "sni=" .. urlencode(o.tls.sni)) end
|
||||
if o.tls.fp then table.insert(p, "fp=" .. urlencode(o.tls.fp)) end
|
||||
if o.tls.alpn then table.insert(p, "alpn=" .. urlencode(o.tls.alpn)) end
|
||||
if o.tls.ech then table.insert(p, "ech=" .. urlencode(o.tls.ech)) end
|
||||
if o.tls.pcs then table.insert(p, "pcs=" .. urlencode(o.tls.pcs)) end
|
||||
table.insert(p, "allowInsecure=" .. (o.tls.insecure and "1" or "0"))
|
||||
|
||||
-- transport
|
||||
table.insert(p, "type=" .. o.transport.type)
|
||||
if o.transport.host then table.insert(p, "host=" .. urlencode(o.transport.host)) end
|
||||
if o.transport.path then table.insert(p, "path=" .. urlencode(o.transport.path)) end
|
||||
if o.transport.serviceName then table.insert(p, "serviceName=" .. urlencode(o.transport.serviceName)) end
|
||||
if o.transport.mode then table.insert(p, "mode=" .. urlencode(o.transport.mode)) end
|
||||
if o.transport.extra then table.insert(p, "extra=" .. urlencode(o.transport.extra)) end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return link .. "#" .. urlencode(o.name or "")
|
||||
end
|
||||
|
||||
-- Trojan
|
||||
local function encode_trojan(node)
|
||||
local o = build_common(node)
|
||||
|
||||
local link = "trojan://" .. node.password .. "@" .. o.server .. ":" .. o.port
|
||||
local p = {}
|
||||
|
||||
if o.tls.security then table.insert(p, "security=" .. o.tls.security) end
|
||||
if o.tls.pbk then table.insert(p, "pbk=" .. urlencode(o.tls.pbk)) end
|
||||
if o.tls.sid then table.insert(p, "sid=" .. urlencode(o.tls.sid)) end
|
||||
if o.tls.sni then table.insert(p, "sni=" .. urlencode(o.tls.sni)) end
|
||||
if o.tls.fp then table.insert(p, "fp=" .. urlencode(o.tls.fp)) end
|
||||
if o.tls.alpn then table.insert(p, "alpn=" .. urlencode(o.tls.alpn)) end
|
||||
if o.tls.pcs then table.insert(p, "pcs=" .. urlencode(o.tls.pcs)) end
|
||||
table.insert(p, "allowInsecure=" .. (o.tls.insecure and "1" or "0"))
|
||||
|
||||
table.insert(p, "type=" .. o.transport.type)
|
||||
if o.transport.host then table.insert(p, "host=" .. urlencode(o.transport.host)) end
|
||||
if o.transport.path then table.insert(p, "path=" .. urlencode(o.transport.path)) end
|
||||
if o.transport.serviceName then table.insert(p, "serviceName=" .. urlencode(o.transport.serviceName)) end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return link .. "#" .. urlencode(o.name or "")
|
||||
end
|
||||
|
||||
-- VMess
|
||||
local function encode_vmess(node)
|
||||
local o = build_common(node)
|
||||
|
||||
local obj = {
|
||||
v = "2",
|
||||
ps = o.name,
|
||||
add = node.server,
|
||||
port = tostring(node.port),
|
||||
id = node.uuid,
|
||||
aid = tostring(node.alterId or 0),
|
||||
net = o.transport.type,
|
||||
security = node.cipher,
|
||||
scy = node.cipher,
|
||||
type = "none",
|
||||
host = o.transport.host or "",
|
||||
path = o.transport.path or "",
|
||||
tls = o.tls.security == "tls" and "tls" or "",
|
||||
sni = o.tls.sni,
|
||||
alpn = o.tls.alpn,
|
||||
fp = o.tls.fp,
|
||||
pcs = o.tls.pcs,
|
||||
insecure = o.tls.insecure and "1" or "0",
|
||||
tfo = node.tfo and "1" or "0"
|
||||
}
|
||||
|
||||
if o.transport.type == "grpc" then
|
||||
obj.path = o.transport.serviceName or ""
|
||||
end
|
||||
|
||||
return "vmess://" .. base64(json.stringify(obj))
|
||||
end
|
||||
|
||||
-- SS
|
||||
local function encode_ss(node)
|
||||
local userinfo = node.cipher .. ":" .. node.password
|
||||
local base = userinfo .. "@" .. host_format(node.server) .. ":" .. node.port
|
||||
local link = "ss://" .. base64(base)
|
||||
|
||||
local p = {}
|
||||
|
||||
if node.udp then table.insert(p, "udp=1") end
|
||||
if node["udp-over-tcp"] then table.insert(p, "uot=1") end
|
||||
|
||||
if node.plugin then
|
||||
local plugin = node.plugin
|
||||
if node["plugin-opts"] then
|
||||
local opts = {}
|
||||
for k, v in pairs(node["plugin-opts"]) do
|
||||
table.insert(opts, k .. "=" .. v)
|
||||
end
|
||||
plugin = plugin .. ";" .. table.concat(opts, ";")
|
||||
end
|
||||
table.insert(p, "plugin=" .. urlencode(plugin))
|
||||
end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- Hysteria
|
||||
local function encode_hysteria2(node)
|
||||
local link = "hysteria://" .. host_format(node.server) .. ":" .. (node.port or "")
|
||||
local p = {}
|
||||
|
||||
if node["auth-str"] then table.insert(p, "auth=" .. node["auth-str"]) end
|
||||
if node["ports"] then table.insert(p, "mport=" .. node["ports"]) end
|
||||
if node.obfs then table.insert(p, "obfsParam=" .. node.obfs) end
|
||||
if node.sni then table.insert(p, "sni=" .. node.sni) end
|
||||
if node.up then table.insert(p, "upmbps=" .. node.up) end
|
||||
if node.down then table.insert(p, "downmbps=" .. node.down) end
|
||||
if node["skip-cert-verify"] then table.insert(p, "insecure=1") end
|
||||
if node["fingerprint"] then table.insert(p, "pinSHA256=" .. urlencode(node["fingerprint"])) end
|
||||
|
||||
if node.alpn then
|
||||
table.insert(p, "alpn=" .. urlencode(build_alpn(node.alpn)))
|
||||
end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- Hysteria2
|
||||
local function encode_hysteria2(node)
|
||||
local link = "hysteria2://" .. (node.password or "") .. "@" .. host_format(node.server) .. ":" .. (node.port or "")
|
||||
local p = {}
|
||||
|
||||
if node["ports"] then table.insert(p, "mport=" .. urlencode(node["ports"])) end
|
||||
if node.obfs then table.insert(p, "obfs=" .. node.obfs) end
|
||||
if node["obfs-password"] then table.insert(p, "obfs-password=" .. node["obfs-password"]) end
|
||||
if node.up then table.insert(p, "upmbps=" .. node.up) end
|
||||
if node.down then table.insert(p, "downmbps=" .. node.down) end
|
||||
|
||||
if node.sni then table.insert(p, "sni=" .. urlencode(node.sni)) end
|
||||
if node["skip-cert-verify"] then table.insert(p, "insecure=1") end
|
||||
if node["fingerprint"] then table.insert(p, "pinSHA256=" .. urlencode(node["fingerprint"])) end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- TUIC
|
||||
local function encode_tuic(node)
|
||||
local link = "tuic://" .. node.uuid .. ":" .. node.password .. "@" .. host_format(node.server) .. ":" .. node.port
|
||||
local p = {}
|
||||
|
||||
if node["congestion-controller"] then
|
||||
table.insert(p, "congestion_control=" .. node["congestion-controller"])
|
||||
end
|
||||
|
||||
if node.alpn then
|
||||
table.insert(p, "alpn=" .. urlencode(build_alpn(node.alpn)))
|
||||
end
|
||||
|
||||
if node.sni then table.insert(p, "sni=" .. urlencode(node.sni)) end
|
||||
if node["disable-sni"] then table.insert(p, "disable_sni=1") end
|
||||
if node["skip-cert-verify"] then table.insert(p, "allowInsecure=1") end
|
||||
if node["udp-relay-mode"] then table.insert(p, "udp_relay_mode=" .. node["udp-relay-mode"]) end
|
||||
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- AnyTLS
|
||||
local function encode_anytls(node)
|
||||
local link = "anytls://" .. (node.password or "") .. "@" .. host_format(node.server) .. ":" .. node.port
|
||||
local p = {}
|
||||
|
||||
if node.sni then table.insert(p, "sni=" .. urlencode(node.sni)) end
|
||||
if node["skip-cert-verify"] then table.insert(p, "allowInsecure=1") end
|
||||
|
||||
if node.alpn then
|
||||
table.insert(p, "alpn=" .. urlencode(build_alpn(node.alpn)))
|
||||
end
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return link .. "#" .. urlencode(node.name or "")
|
||||
end
|
||||
|
||||
-- SSR
|
||||
local function encode_ssr(node)
|
||||
local link = host_format(node.server) .. ":" .. node.port .. ":" .. (node.protocol or "") .. ":" ..
|
||||
(node.cipher or "") .. ":" .. (node.obfs or "") .. ":" .. base64(node.password)
|
||||
local p = {}
|
||||
|
||||
if node["obfs-param"] then table.insert(p, "obfsparam=" .. base64(node["obfs-param"])) end
|
||||
if node["protocol-param"] then table.insert(p, "protoparam=" .. base64(node["protocol-param"])) end
|
||||
table.insert(p, "remarks=" .. base64(node.name))
|
||||
|
||||
if #p > 0 then
|
||||
link = link .. "?" .. table.concat(p, "&")
|
||||
end
|
||||
|
||||
return "ssr://" .. base64(link)
|
||||
end
|
||||
|
||||
local function encode_node(node)
|
||||
if (not node.type) or (not node.name) then return nil end
|
||||
|
||||
local t = node.type
|
||||
|
||||
if t == "vless" then return encode_vless(node)
|
||||
elseif t == "trojan" then return encode_trojan(node)
|
||||
elseif t == "vmess" then return encode_vmess(node)
|
||||
elseif t == "ss" then return encode_ss(node)
|
||||
elseif t == "hysteria" then return encode_hysteria(node)
|
||||
elseif t == "hysteria2" then return encode_hysteria2(node)
|
||||
elseif t == "tuic" then return encode_tuic(node)
|
||||
elseif t == "anytls" then return encode_anytls(node)
|
||||
elseif t == "ssr" then return encode_ssr(node)
|
||||
else api.log("订阅转换 → 丢弃不支持的节点:" .. node.name .. ",节点类型:" .. t)
|
||||
end
|
||||
end
|
||||
|
||||
function parseClashNode(raw, remark)
|
||||
if not raw then return "" end
|
||||
local ok, lyaml = pcall(require, "lyaml")
|
||||
if not ok then return raw end
|
||||
|
||||
local data = lyaml.load(raw)
|
||||
if not data or type(data) ~= "table" then return raw end
|
||||
if not data.proxies then return "" end
|
||||
|
||||
api.log('检测到 Clash 订阅,正在进行转换 ...')
|
||||
|
||||
local links = {}
|
||||
for _, node in ipairs(data.proxies) do
|
||||
local link = encode_node(node)
|
||||
if link then
|
||||
table.insert(links, link)
|
||||
end
|
||||
end
|
||||
|
||||
return #links > 0 and table.concat(links, "\n") or ""
|
||||
end
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 })
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>";
|
||||
|
||||
@ -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 查询域名"
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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."))
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 ""
|
||||
|
||||
@ -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 "状态"
|
||||
|
||||
@ -5,10 +5,10 @@
|
||||
"ubus": {
|
||||
"service": [ "list" ]
|
||||
},
|
||||
"uci": [ "socat" ]
|
||||
"uci": [ "luci_socat" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "socat" ]
|
||||
"uci": [ "luci_socat" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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:= \
|
||||
|
||||
@ -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 登录"
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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/)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user