diff --git a/ipv6-neigh/Cargo.lock b/ipv6-neigh/Cargo.lock index 5cccbb09..4b8b3216 100644 --- a/ipv6-neigh/Cargo.lock +++ b/ipv6-neigh/Cargo.lock @@ -2,12 +2,65 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -104,6 +157,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "combine" version = "4.6.7" @@ -135,6 +194,15 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -146,6 +214,29 @@ dependencies = [ "syn", ] +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -322,6 +413,7 @@ name = "hickory-proto" version = "0.26.0-beta.3" source = "git+https://github.com/hickory-dns/hickory-dns.git?rev=34a4cca1a707b642052b427938b7d1e8aaa71e3a#34a4cca1a707b642052b427938b7d1e8aaa71e3a" dependencies = [ + "bitflags", "data-encoding", "idna", "ipnet", @@ -329,7 +421,9 @@ dependencies = [ "once_cell", "rand", "ring", + "rustls-pki-types", "thiserror 2.0.18", + "time", "tinyvec", "tracing", "url", @@ -461,8 +555,10 @@ name = "ip-neigh" version = "0.1.0" dependencies = [ "clap", + "env_logger", "futures", "hickory-proto", + "log", "netlink-packet-core", "netlink-packet-route", "netlink-sys", @@ -478,12 +574,42 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "jni" version = "0.22.4" @@ -660,6 +786,12 @@ dependencies = [ "libc", ] +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + [[package]] name = "once_cell" version = "1.21.4" @@ -670,6 +802,12 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "parking_lot" version = "0.12.5" @@ -717,6 +855,15 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -726,6 +873,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "prettyplease" version = "0.2.37" @@ -786,6 +939,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "ring" version = "0.17.14" @@ -826,6 +1008,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + [[package]] name = "same-file" version = "1.0.6" @@ -1024,6 +1215,25 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + [[package]] name = "tinystr" version = "0.8.3" @@ -1144,6 +1354,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "walkdir" version = "2.5.0" @@ -1453,6 +1669,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/ipv6-neigh/Cargo.toml b/ipv6-neigh/Cargo.toml index 561c9e3a..908d293b 100644 --- a/ipv6-neigh/Cargo.toml +++ b/ipv6-neigh/Cargo.toml @@ -16,6 +16,8 @@ clap = { version = "4.0", default-features = false, features = ["cargo", "derive hickory-proto = { git = "https://github.com/hickory-dns/hickory-dns.git", rev = "34a4cca1a707b642052b427938b7d1e8aaa71e3a", features = ["dnssec-ring"] } ubus = "0.1.6" serde_json = "1.0.108" +log = "0.4" +env_logger = "0.11" [profile.release] opt-level = 3 diff --git a/ipv6-neigh/Makefile b/ipv6-neigh/Makefile index 6d7a1c1b..ab205ce0 100644 --- a/ipv6-neigh/Makefile +++ b/ipv6-neigh/Makefile @@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=ipv6-neigh PKG_VERSION:=0.1.0 -PKG_RELEASE:=2 +PKG_RELEASE:=3 PKG_BUILD_DEPENDS:=rust/host PKG_BUILD_PARALLEL:=1 diff --git a/ipv6-neigh/src/db.rs b/ipv6-neigh/src/db.rs index b43a7a2b..21bc545f 100644 --- a/ipv6-neigh/src/db.rs +++ b/ipv6-neigh/src/db.rs @@ -1,10 +1,22 @@ use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use hickory_proto::op::{Message, ResponseCode, update_message}; use hickory_proto::rr::{Name, RData, RecordSet, RecordType, TSigner}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::TcpStream; +use tokio::time::timeout; + +const TCP_TIMEOUT: Duration = Duration::from_secs(5); + +fn check_response(response: &Message, allowed: &[ResponseCode]) -> Result<(), Box> { + let code = response.metadata.response_code; + if code == ResponseCode::NoError || allowed.contains(&code) { + Ok(()) + } else { + Err(format!("DNS server returned {code:?}").into()) + } +} /// DNS dynamic update client that sends RFC 2136 updates over TCP to the hickory-dns server. pub(crate) struct DnsUpdater { @@ -35,7 +47,10 @@ impl DnsUpdater { let mut rrset = RecordSet::with_ttl(name, RecordType::AAAA, ttl); rrset.add_rdata(RData::AAAA(addr.into())); let msg = update_message::append(rrset, self.zone.clone(), false, false); - self.send_tcp(msg).await?; + let response = self.send_tcp(msg).await?; + check_response(&response, &[])?; + } else { + check_response(&response, &[])?; } Ok(()) } @@ -57,7 +72,10 @@ impl DnsUpdater { let mut rrset = RecordSet::with_ttl(name, RecordType::A, ttl); rrset.add_rdata(RData::A(addr.into())); let msg = update_message::append(rrset, self.zone.clone(), false, false); - self.send_tcp(msg).await?; + let response = self.send_tcp(msg).await?; + check_response(&response, &[])?; + } else { + check_response(&response, &[])?; } Ok(()) } @@ -72,7 +90,8 @@ impl DnsUpdater { let mut rrset = RecordSet::new(name, RecordType::AAAA, 0); rrset.add_rdata(RData::AAAA(addr.into())); let msg = update_message::delete_by_rdata(rrset, self.zone.clone(), false); - self.send_tcp(msg).await?; + let response = self.send_tcp(msg).await?; + check_response(&response, &[])?; Ok(()) } @@ -86,7 +105,8 @@ impl DnsUpdater { let mut rrset = RecordSet::new(name, RecordType::A, 0); rrset.add_rdata(RData::A(addr.into())); let msg = update_message::delete_by_rdata(rrset, self.zone.clone(), false); - self.send_tcp(msg).await?; + let response = self.send_tcp(msg).await?; + check_response(&response, &[])?; Ok(()) } @@ -98,15 +118,23 @@ impl DnsUpdater { let bytes = msg.to_vec()?; let len = u16::try_from(bytes.len())?; - let mut stream = TcpStream::connect(self.server_addr).await?; - stream.write_all(&len.to_be_bytes()).await?; - stream.write_all(&bytes).await?; - stream.flush().await?; + let result = timeout(TCP_TIMEOUT, async { + let mut stream = TcpStream::connect(self.server_addr).await?; + stream.write_all(&len.to_be_bytes()).await?; + stream.write_all(&bytes).await?; + stream.flush().await?; - let resp_len = stream.read_u16().await? as usize; - let mut resp_buf = vec![0u8; resp_len]; - stream.read_exact(&mut resp_buf).await?; + let resp_len = stream.read_u16().await? as usize; + let mut resp_buf = vec![0u8; resp_len]; + stream.read_exact(&mut resp_buf).await?; - Ok(Message::from_vec(&resp_buf)?) + Ok::<_, Box>(Message::from_vec(&resp_buf)?) + }) + .await + .map_err(|_| -> Box { + format!("DNS TCP timeout after {}s connecting to {}", TCP_TIMEOUT.as_secs(), self.server_addr).into() + })??; + + Ok(result) } } diff --git a/ipv6-neigh/src/main.rs b/ipv6-neigh/src/main.rs index 5ac86aa7..61348729 100644 --- a/ipv6-neigh/src/main.rs +++ b/ipv6-neigh/src/main.rs @@ -1,8 +1,9 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr as StdSocketAddr}; use futures::stream::StreamExt; use futures::stream::TryStreamExt; +use log::{debug, error, info, warn}; use netlink_packet_core::NetlinkPayload; use netlink_packet_route::RouteNetlinkMessage; use netlink_packet_route::neighbour::NeighbourMessage; @@ -47,6 +48,9 @@ struct Cli { /// TSIG key name #[clap(short = 'n', long, default_value = "update-key.")] key_name: String, + /// Log level (error, warn, info, debug, trace) + #[clap(short = 'l', long, default_value = "info")] + log_level: String, } #[derive(Debug)] @@ -95,52 +99,70 @@ fn if_ipv4_in_private_subnet(ip: &Ipv4Addr) -> bool { false } -async fn process_new_neigh(neigh: &Neigh, updater: &db::DnsUpdater, leases: &HashMap) { +async fn process_new_neigh(neigh: &Neigh, updater: &db::DnsUpdater, leases: &HashMap) -> bool { let Some(hostname) = leases.get(&neigh.mac) else { - eprintln!("no lease for mac {}, skipping DNS update", neigh.mac); - return; + debug!("no lease for mac {}, skipping DNS update", neigh.mac); + return true; }; let result = match &neigh.inet { NeighbourAddress::Inet6(addr) => updater.upsert_aaaa(hostname, *addr, DEFAULT_TTL).await, NeighbourAddress::Inet(addr) => updater.upsert_a(hostname, *addr, DEFAULT_TTL).await, - _ => return, + _ => return true, }; match result { - Ok(()) => println!("DNS update: added {} -> {:?}", hostname, neigh.inet), - Err(e) => eprintln!("DNS update failed for {}: {}", hostname, e), + Ok(()) => { + info!("DNS update: added {} -> {:?}", hostname, neigh.inet); + true + } + Err(e) => { + error!("DNS update failed for {}: {}", hostname, e); + false + } } } -async fn process_del_neigh(neigh: &Neigh, updater: &db::DnsUpdater, leases: &HashMap) { +async fn process_del_neigh(neigh: &Neigh, updater: &db::DnsUpdater, leases: &HashMap) -> bool { let Some(hostname) = leases.get(&neigh.mac) else { - return; + return true; }; let result = match &neigh.inet { NeighbourAddress::Inet6(addr) => updater.delete_aaaa(hostname, *addr).await, NeighbourAddress::Inet(addr) => updater.delete_a(hostname, *addr).await, - _ => return, + _ => return true, }; match result { - Ok(()) => println!("DNS update: removed {} -> {:?}", hostname, neigh.inet), - Err(e) => eprintln!("DNS delete failed for {}: {}", hostname, e), + Ok(()) => { + info!("DNS update: removed {} -> {:?}", hostname, neigh.inet); + true + } + Err(e) => { + error!("DNS delete failed for {}: {}", hostname, e); + false + } } } -fn is_multicast_or_broadcast_route_type(route_type: RouteType) -> bool { - match route_type { - RouteType::Multicast => true, - RouteType::Broadcast => true, - _ => false, - } +fn should_skip_route_type(route_type: RouteType) -> bool { + matches!(route_type, RouteType::Multicast | RouteType::Broadcast | RouteType::Local) } -fn is_multicast_or_broadcast(neigh: &Neigh) -> bool { - return is_multicast_or_broadcast_route_type(neigh.kind); +fn should_skip_neigh(neigh: &Neigh) -> bool { + should_skip_route_type(neigh.kind) } #[tokio::main] async fn main() -> Result<(), ()> { let args = Cli::parse(); + + env_logger::Builder::new() + .filter_level( + args.log_level + .parse() + .expect("invalid log level (use: error, warn, info, debug, trace)"), + ) + .format_timestamp_secs() + .init(); + let private_subnet = args.private_subnet; let zone = Name::from_ascii(&args.zone).expect("invalid zone name"); @@ -154,23 +176,34 @@ async fn main() -> Result<(), ()> { let (connection, handle, _) = new_connection().unwrap(); tokio::spawn(connection); - dump_addresses(handle.clone(), args.iface).await.unwrap(); + 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(); - println!("loaded {} DHCP leases", leases.len()); + info!("loaded {} DHCP leases", leases.len()); + + // State cache: tracks (mac, ip_string) pairs already registered + let mut registered: HashSet<(String, String)> = HashSet::new(); // Dump existing neighbours and register them - println!("dumping neighbours"); + debug!("dumping neighbours"); if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet).await { for neigh in &neighbours { - println!("{:?}", neigh); - if !is_multicast_or_broadcast(neigh) { - process_new_neigh(neigh, &updater, &leases).await; + 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); + } + } } } } - println!(); let (mut conn, mut _handle, mut messages) = new_connection().map_err(|e| format!("{e}")).unwrap(); @@ -194,7 +227,7 @@ async fn main() -> Result<(), ()> { if event_count % 50 == 0 { if let Ok(new_leases) = op::get_lease() { leases = new_leases; - println!("refreshed {} DHCP leases", leases.len()); + info!("refreshed {} DHCP leases", leases.len()); } } @@ -205,21 +238,41 @@ async fn main() -> Result<(), ()> { let Some(neigh) = parse_neighbour_message(new_neigh, private_subnet) else { continue; }; - if is_multicast_or_broadcast(&neigh) { + if should_skip_neigh(&neigh) { continue; } - println!("New neighbour: {:?}", neigh); - process_new_neigh(&neigh, &updater, &leases).await; + // 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); + } } RouteNetlinkMessage::DelNeighbour(del_neigh) => { let Some(neigh) = parse_neighbour_message(del_neigh, private_subnet) else { continue; }; - if is_multicast_or_broadcast(&neigh) { + if should_skip_neigh(&neigh) { continue; } - println!("Del neighbour: {:?}", neigh); - process_del_neigh(&neigh, &updater, &leases).await; + 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); + } } _ => {} } @@ -237,11 +290,17 @@ fn format_mac(mac: Vec) -> String { mac_str } -async fn dump_addresses(handle: Handle, link: Option) -> Result<(), Error> { - let mut request = handle.link().get(); - if let Some(link) = link { - request = request.match_name(link); +fn inet_to_string(addr: &NeighbourAddress) -> String { + match addr { + NeighbourAddress::Inet(ip) => ip.to_string(), + NeighbourAddress::Inet6(ip) => ip.to_string(), + other => format!("{other:?}"), } +} + +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? { @@ -251,24 +310,33 @@ async fn dump_addresses(handle: Handle, link: Option) -> Result<(), Erro .set_link_index_filter(link.header.index) .execute(); while let Some(msg) = addresses.try_next().await? { - println!("{msg:?}"); + debug!("{msg:?}"); } Ok(()) } else { - eprintln!("link not found"); + warn!("link not found"); Ok(()) } } +fn is_link_local_ipv6(addr: &NeighbourAddress) -> bool { + matches!(addr, NeighbourAddress::Inet6(ip) if (ip.segments()[0] & 0xffc0) == 0xfe80) +} + fn parse_neighbour_message(neigh: NeighbourMessage, private_subnet: bool) -> Option { let state = neigh.header.state; - if state == NeighbourState::Permanent { + // Filter out static and incomplete entries + if matches!(state, NeighbourState::Permanent | NeighbourState::Noarp) { return None; } let addr: NeighbourAddress = neigh.attributes.iter().find_map(|attr| match attr { NeighbourAttribute::Destination(inet) => Some(inet.to_owned()), _ => None, })?; + // Link-local IPv6 addresses are interface-scoped and useless in DNS + if is_link_local_ipv6(&addr) { + return None; + } if private_subnet { match addr { NeighbourAddress::Inet(addr) => { @@ -290,12 +358,17 @@ fn parse_neighbour_message(neigh: NeighbourMessage, private_subnet: bool) -> Opt NeighbourAttribute::LinkLocalAddress(mac) => Some(mac.to_owned()), _ => None, })?; + let mac_str = format_mac(mac); + // Filter out empty or all-zero MACs (router own addresses, incomplete entries) + if mac_str.is_empty() || mac_str == "00:00:00:00:00:00" { + return None; + } Some(Neigh { ifindex, state, kind, inet: addr, - mac: format_mac(mac), + mac: mac_str, }) } @@ -304,7 +377,7 @@ async fn dump_neighbours(handle: Handle, private_subnet: bool) -> Result = Vec::new(); while let Some(route) = neighbours.try_next().await? { if let Some(neigh) = parse_neighbour_message(route, private_subnet) { - if !is_multicast_or_broadcast(&neigh) { + if !should_skip_neigh(&neigh) { vec.push(neigh); } } diff --git a/ipv6-neigh/src/op.rs b/ipv6-neigh/src/op.rs index a483cdd5..d51c249f 100644 --- a/ipv6-neigh/src/op.rs +++ b/ipv6-neigh/src/op.rs @@ -5,32 +5,65 @@ use std::path::Path; pub fn call_ubus(obj_path: &str, method: &str) -> Result> { let socket = Path::new("/var/run/ubus/ubus.sock"); - let mut connection = match ubus::Connection::connect(&socket) { - Ok(connection) => connection, - Err(err) => { - return Err(Box::new(err)); - } - }; + let mut connection = ubus::Connection::connect(&socket)?; - let json = connection.call(obj_path, method, "").unwrap(); - let parsed: Value = serde_json::from_str(&json).unwrap(); + let json = connection.call(obj_path, method, "")?; + let parsed: Value = serde_json::from_str(&json)?; Ok(parsed) } +/// Convert a hex MAC string like "44237cdcb75b" to colon-separated "44:23:7c:dc:b7:5b" +fn format_mac_from_hex(hex: &str) -> String { + hex.as_bytes() + .chunks(2) + .map(|chunk| std::str::from_utf8(chunk).unwrap()) + .collect::>() + .join(":") +} + +/// Sanitize a DHCP hostname into a valid DNS label. +/// Replaces invalid characters with '-', collapses consecutive hyphens, +/// strips leading/trailing hyphens, and truncates to 63 chars. +fn sanitize_hostname(raw: &str) -> String { + let sanitized: String = raw + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() || c == '-' { + c.to_ascii_lowercase() + } else { + '-' + } + }) + .collect(); + // Collapse consecutive hyphens and strip leading/trailing + let collapsed: String = sanitized + .split('-') + .filter(|s| !s.is_empty()) + .collect::>() + .join("-"); + // Truncate to 63 chars (max DNS label length) + collapsed.chars().take(63).collect() +} + // get mac to hostname mapping pub fn get_lease() -> Result, Box> { // dhcpv6 does not use mac address, so we only need to get ipv4 leases - let ip4lease = call_ubus("dhcp", "ipv4leases"); - let leases = ip4lease.unwrap(); - let devices = leases["device"].as_object().unwrap(); + let leases = call_ubus("dhcp", "ipv4leases")?; + let devices = leases["device"].as_object() + .ok_or("missing 'device' in ipv4leases response")?; let mut result = HashMap::new(); - // hard code - for (device, leases) in devices { - let suffix = if device == "phy1-ap0" { ".iot" } else { ".lan" }; - let leases = leases["leases"].as_array().unwrap(); + for (_device, leases) in devices { + let leases = leases["leases"].as_array() + .ok_or("missing 'leases' array in device")?; for lease in leases { - let mac = lease["mac"].as_str().unwrap().to_string(); - let hostname = lease["hostname"].as_str().unwrap().to_string() + suffix; + let Some(raw_mac) = lease["mac"].as_str() else { continue }; + let Some(hostname) = lease["hostname"].as_str() else { continue }; + let mac = format_mac_from_hex(raw_mac); + let hostname = sanitize_hostname(hostname); + if hostname.is_empty() { + continue; + } + // Store bare hostname only; the zone is appended by DnsUpdater result.insert(mac, hostname); } } diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index d46b5377..829fd589 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.4.15 -PKG_RELEASE:=103 +PKG_RELEASE:=104 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua index 4b50ab75..45632140 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/acl_config.lua @@ -509,7 +509,7 @@ o = s:option(ListValue, "xray_dns_mode", translate("Request protocol")) o.default = "tcp" o:value("tcp", "TCP") o:value("udp", "UDP") -o:value("tcp+doh", "TCP + DoH (" .. translate("A/AAAA type") .. ")") +o:value("doh", "DoH") o:depends("dns_mode", "xray") o.cfgvalue = function(self, section) return m:get(section, "v2ray_dns_mode") @@ -550,7 +550,6 @@ o:value("208.67.222.222", "208.67.222.222 (OpenDNS)") o:depends({dns_mode = "dns2socks"}) o:depends({xray_dns_mode = "udp"}) o:depends({xray_dns_mode = "tcp"}) -o:depends({xray_dns_mode = "tcp+doh"}) o:depends({singbox_dns_mode = "udp"}) o:depends({singbox_dns_mode = "tcp"}) @@ -589,7 +588,7 @@ o.validate = function(self, value, t) end return nil, translate("DoH request address") .. " " .. translate("Format must be:") .. " URL,IP" end -o:depends({xray_dns_mode = "tcp+doh"}) +o:depends({xray_dns_mode = "doh"}) o:depends({singbox_dns_mode = "doh"}) o:depends({singbox_dns_mode = "http3"}) diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua index 2f5a8435..bb4e95a6 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/global.lua @@ -379,7 +379,7 @@ o = s:taboption("DNS", ListValue, "xray_dns_mode", translate("Remote DNS") .. " o.default = "tcp" o:value("tcp", "TCP") o:value("udp", "UDP") -o:value("tcp+doh", "TCP + DoH (" .. translate("A/AAAA type") .. ")") +o:value("doh", "DoH") o:depends("dns_mode", "xray") o:depends("smartdns_dns_mode", "xray") o.cfgvalue = function(self, section) @@ -438,7 +438,6 @@ o:depends({dns_mode = "tcp"}) o:depends({dns_mode = "udp"}) o:depends({xray_dns_mode = "udp"}) o:depends({xray_dns_mode = "tcp"}) -o:depends({xray_dns_mode = "tcp+doh"}) o:depends({singbox_dns_mode = "udp"}) o:depends({singbox_dns_mode = "tcp"}) @@ -457,7 +456,7 @@ o:value("https://dns.adguard.com/dns-query,94.140.14.14", "94.140.14.14 (AdGuard o:value("https://doh.libredns.gr/dns-query,116.202.176.26", "116.202.176.26 (LibreDNS)") o:value("https://doh.libredns.gr/ads,116.202.176.26", "116.202.176.26 (LibreDNS-NoAds)") o.validate = doh_validate -o:depends({xray_dns_mode = "tcp+doh"}) +o:depends({xray_dns_mode = "doh"}) o:depends({singbox_dns_mode = "doh"}) o:depends({singbox_dns_mode = "http3"}) diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index e1e591de..9adc858d 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -1498,7 +1498,7 @@ function gen_config(var) }) end - if (remote_dns_udp_server and remote_dns_udp_port) or (remote_dns_tcp_server and remote_dns_tcp_port) or #node_dns > 0 then + if (remote_dns_udp_server and remote_dns_udp_port) or (remote_dns_tcp_server and remote_dns_tcp_port) or remote_dns_doh_url or #node_dns > 0 then if not routing then routing = { domainStrategy = "IPOnDemand", @@ -1659,19 +1659,9 @@ function gen_config(var) table.insert(outbounds, { tag = "dns-out", protocol = "dns", - proxySettings = dns_outbound_tag and { - tag = dns_outbound_tag - } or nil, settings = { - address = remote_dns_udp_server or remote_dns_tcp_server, - port = tonumber(remote_dns_udp_port) or tonumber(remote_dns_tcp_port), - network = remote_dns_udp_server and "udp" or "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 = ((node and node.protocol == "_shunt") or remote_dns_fake or dns.clientIP) and "hijack" or "direct" - } - } or nil + rules = (api.compare_versions(xray_version, ">", "26.4.17")) and {{ action = "hijack" }} or nil } }) diff --git a/luci-app-passwall/po/zh-cn/passwall.po b/luci-app-passwall/po/zh-cn/passwall.po index f813dded..43c20824 100644 --- a/luci-app-passwall/po/zh-cn/passwall.po +++ b/luci-app-passwall/po/zh-cn/passwall.po @@ -181,9 +181,6 @@ msgstr "%s 不能为空。" msgid "Filter Mode" msgstr "过滤模式" -msgid "A/AAAA type" -msgstr "A/AAAA 类型" - msgid "TCP node must be '%s' type to use FakeDNS." msgstr "TCP 节点必须是 '%s' 类型才能使用 FakeDNS。" diff --git a/luci-app-passwall/root/usr/share/passwall/app.sh b/luci-app-passwall/root/usr/share/passwall/app.sh index 0ee590cb..b7b817e3 100755 --- a/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/luci-app-passwall/root/usr/share/passwall/app.sh @@ -181,6 +181,7 @@ run_singbox() { [ "$remote_dns_protocol" = "http3" ] && json_add_string "remote_dns_http3" "1" ;; esac + [ -n "$remote_dns_client_ip" ] && json_add_string "remote_dns_client_ip" "${remote_dns_client_ip}" [ "$remote_fakedns" = "1" ] && json_add_string "remote_dns_fake" "1" [ -n "$no_run" ] && json_add_string "no_run" "1" @@ -259,29 +260,24 @@ run_xray() { [ -n "$dns_cache" ] && json_add_string "dns_cache" "${dns_cache}" case "$remote_dns_protocol" in - udp) - local _dns=$(get_first_dns remote_dns_udp_server 53 | sed 's/#/:/g') + udp|tcp) + local _proto="$remote_dns_protocol" + local _dns=$(get_first_dns remote_dns_${_proto}_server 53 | sed 's/#/:/g') local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}') local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}') - json_add_string "remote_dns_udp_server" "${_dns_address}" - json_add_string "remote_dns_udp_port" "${_dns_port}" + json_add_string "remote_dns_${_proto}_server" "${_dns_address}" + json_add_string "remote_dns_${_proto}_port" "${_dns_port}" ;; - tcp|tcp+doh) - local _dns=$(get_first_dns remote_dns_tcp_server 53 | sed 's/#/:/g') - local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}') - local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}') - json_add_string "remote_dns_tcp_server" "${_dns_address}" - json_add_string "remote_dns_tcp_port" "${_dns_port}" - [ "$remote_dns_protocol" = "tcp+doh" ] && { - local _doh_url _doh_host _doh_port _doh_bootstrap - parse_doh "$remote_dns_doh" _doh_url _doh_host _doh_port _doh_bootstrap - [ -n "$_doh_bootstrap" ] && json_add_string "remote_dns_doh_ip" "${_doh_bootstrap}" - json_add_string "remote_dns_doh_port" "${_doh_port}" - json_add_string "remote_dns_doh_url" "${_doh_url}" - json_add_string "remote_dns_doh_host" "${_doh_host}" - } + doh) + local _doh_url _doh_host _doh_port _doh_bootstrap + parse_doh "$remote_dns_doh" _doh_url _doh_host _doh_port _doh_bootstrap + [ -n "$_doh_bootstrap" ] && json_add_string "remote_dns_doh_ip" "${_doh_bootstrap}" + json_add_string "remote_dns_doh_port" "${_doh_port}" + json_add_string "remote_dns_doh_url" "${_doh_url}" + json_add_string "remote_dns_doh_host" "${_doh_host}" ;; esac + json_add_string "loglevel" "$loglevel" [ -n "$no_run" ] && json_add_string "no_run" "1" local _json_arg="$(json_dump)" @@ -754,7 +750,7 @@ run_redir() { local v2ray_dns_mode=$(config_t_get global v2ray_dns_mode tcp) [ "${DNS_MODE}" != "sing-box" ] && [ "$protocol" = "_shunt" ] && { DNS_MODE="sing-box" - [ "$v2ray_dns_mode" != "udp" ] && [ "$v2ray_dns_mode" != "tcp" ] && v2ray_dns_mode="tcp" + [ "$v2ray_dns_mode" = "tcp+doh" ] && v2ray_dns_mode="tcp" } [ "$protocol" = "_shunt" ] && { @@ -793,7 +789,7 @@ run_redir() { resolve_dns_log="Sing-Box DNS(127.0.0.1#${resolve_dns_port}) -> ${_proto}://${REMOTE_DNS}" ;; doh|http3) - remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query") + local remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query") _args="${_args} remote_dns_doh=${remote_dns_doh}" resolve_dns_log="Sing-Box DNS(127.0.0.1#${resolve_dns_port}) -> ${remote_dns_doh}" ;; @@ -833,8 +829,10 @@ run_redir() { local v2ray_dns_mode=$(config_t_get global v2ray_dns_mode tcp) [ "${DNS_MODE}" != "xray" ] && [ "$protocol" = "_shunt" ] && { DNS_MODE="xray" - [ "$v2ray_dns_mode" != "udp" ] && [ "$v2ray_dns_mode" != "tcp" ] && v2ray_dns_mode="tcp" + [ "$v2ray_dns_mode" = "http3" ] && v2ray_dns_mode="tcp" } + #兼容旧模式,择机移除 + [ "$v2ray_dns_mode" = "tcp+doh" ] && v2ray_dns_mode="tcp" [ "$protocol" = "_shunt" ] && { local geoip_path="${V2RAY_LOCATION_ASSET%*/}/geoip.dat" @@ -866,19 +864,15 @@ run_redir() { _args="${_args} remote_dns_protocol=${v2ray_dns_mode}" case "$v2ray_dns_mode" in - udp) - _args="${_args} remote_dns_udp_server=${REMOTE_DNS}" - resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> udp://${REMOTE_DNS}" + udp|tcp) + local _proto="$v2ray_dns_mode" + _args="${_args} remote_dns_${_proto}_server=${REMOTE_DNS}" + resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> ${_proto}://${REMOTE_DNS}" ;; - tcp|tcp+doh) - _args="${_args} remote_dns_tcp_server=${REMOTE_DNS}" - if [ "$v2ray_dns_mode" = "tcp+doh" ]; then - remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query") - _args="${_args} remote_dns_doh=${remote_dns_doh}" - resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> (${remote_dns_doh})(A/AAAA) + tcp://${REMOTE_DNS}" - else - resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> tcp://${REMOTE_DNS}" - fi + doh) + local remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query") + _args="${_args} remote_dns_doh=${remote_dns_doh}" + resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> ${remote_dns_doh}" ;; esac local remote_fakedns=$(config_t_get global remote_fakedns 0) @@ -1348,7 +1342,7 @@ start_dns() { echolog " - Sing-Box DNS(${TUN_DNS}) -> ${_proto}://${REMOTE_DNS}" ;; doh|http3) - remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query") + local remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query") _args="${_args} remote_dns_doh=${remote_dns_doh}" echolog " - Sing-Box DNS(${TUN_DNS}) -> ${remote_dns_doh}" @@ -1376,26 +1370,24 @@ start_dns() { [ -n "${_remote_dns_client_ip}" ] && _args="${_args} remote_dns_client_ip=${_remote_dns_client_ip}" TCP_PROXY_DNS=1 local v2ray_dns_mode=$(config_t_get global v2ray_dns_mode tcp) + #兼容旧模式,择机移除 + [ "$v2ray_dns_mode" = "tcp+doh" ] && v2ray_dns_mode="tcp" _args="${_args} dns_listen_port=${NEXT_DNS_LISTEN_PORT}" _args="${_args} remote_dns_protocol=${v2ray_dns_mode}" case "$v2ray_dns_mode" in - udp) - _args="${_args} remote_dns_udp_server=${REMOTE_DNS}" - echolog " - Xray DNS(${TUN_DNS}) -> udp://${REMOTE_DNS}" + udp|tcp) + local _proto="$v2ray_dns_mode" + _args="${_args} remote_dns_${_proto}_server=${REMOTE_DNS}" + echolog " - Xray DNS(${TUN_DNS}) -> ${_proto}://${REMOTE_DNS}" ;; - tcp|tcp+doh) - _args="${_args} remote_dns_tcp_server=${REMOTE_DNS}" - if [ "$v2ray_dns_mode" = "tcp+doh" ]; then - remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query") - _args="${_args} remote_dns_doh=${remote_dns_doh}" - echolog " - Xray DNS(${TUN_DNS}) -> (${remote_dns_doh})(A/AAAA) + tcp://${REMOTE_DNS}" + doh) + local remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query") + _args="${_args} remote_dns_doh=${remote_dns_doh}" + echolog " - Xray DNS(${TUN_DNS}) -> ${remote_dns_doh}" - local _doh_url _doh_host _doh_port _doh_bootstrap - parse_doh "$remote_dns_doh" _doh_url _doh_host _doh_port _doh_bootstrap - [ -n "${_doh_bootstrap}" ] && REMOTE_DNS="${REMOTE_DNS},${_doh_bootstrap}#${_doh_port}" - else - echolog " - Xray DNS(${TUN_DNS}) -> tcp://${REMOTE_DNS}" - fi + local _doh_url _doh_host _doh_port _doh_bootstrap + parse_doh "$remote_dns_doh" _doh_url _doh_host _doh_port _doh_bootstrap + [ -n "${_doh_bootstrap}" ] && REMOTE_DNS="${REMOTE_DNS},${_doh_bootstrap}#${_doh_port}" ;; esac _args="${_args} dns_socks_address=127.0.0.1 dns_socks_port=${tcp_node_socks_port}" @@ -1664,12 +1656,14 @@ acl_app() { else local type=$(echo $(config_n_get $tcp_node type) | tr 'A-Z' 'a-z') local protocol=$(config_n_get $tcp_node protocol) + #兼容旧模式,择机移除 + [ "$v2ray_dns_mode" = "tcp+doh" ] && v2ray_dns_mode="tcp" ([ "$type" = "sing-box" ] || [ "$type" = "xray" ]) && [ "$protocol" = "_shunt" ] && [ "$type" != "$dns_mode" ] && { dns_mode=$type - ([ "$type" = "xray" ] || [ "$type" = "sing-box" ]) && [ "$v2ray_dns_mode" != "udp" ] && [ "$v2ray_dns_mode" != "tcp" ] && v2ray_dns_mode="tcp" + [ "$type" = "xray" ] && [ "$v2ray_dns_mode" = "http3" ] && v2ray_dns_mode="tcp" } dns_cache_key="${dns_mode}_${remote_dns}_${v2ray_dns_mode:-none}_${remote_dns_client_ip:-0}_${remote_fakedns:-0}" - [ "$dns_mode" = "sing-box" ] && [ "$v2ray_dns_mode" != "udp" ] && [ "$v2ray_dns_mode" != "tcp" ] && { + ([ "$v2ray_dns_mode" = "doh" ] || [ "$v2ray_dns_mode" = "http3" ]) && { dns_cache_key="${dns_mode}_${remote_dns_doh:-https://1.1.1.1/dns-query}_${v2ray_dns_mode:-doh}_${remote_dns_client_ip:-0}_${remote_fakedns:-0}" } diff --git a/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/luci-app-passwall/root/usr/share/passwall/subscribe.lua index e664fd33..85ba61e4 100755 --- a/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -1940,11 +1940,12 @@ local function update_node(manual) end end if domain_strategy then + local ds = domain_strategy if vvv == "sing-box" then local map = { UseIPv4v6 = "prefer_ipv4", UseIPv6v4 = "prefer_ipv6", UseIPv4 = "ipv4_only", UseIPv6 = "ipv6_only" } - domain_strategy = map[domain_strategy] or "" + ds = map[ds] or "" end - uci:set(appname, cfgid, "domain_strategy", domain_strategy) + uci:set(appname, cfgid, "domain_strategy", ds) end end -- 订阅组链式代理 diff --git a/luci-app-rtp2httpd/Makefile b/luci-app-rtp2httpd/Makefile new file mode 100644 index 00000000..31ccc0a3 --- /dev/null +++ b/luci-app-rtp2httpd/Makefile @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2026 ImmortalWrt.org + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI support for rtp2httpd +LUCI_DEPENDS:=+rtp2httpd + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js b/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js new file mode 100644 index 00000000..413bc933 --- /dev/null +++ b/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Apache-2.0 + +'use strict'; +'require view'; +'require form'; +'require tools.widgets as widgets'; + +return view.extend({ + render() { + let m, s, o; + + m = new form.Map('rtp2httpd', _('rtp2httpd'), + _('Multicast RTP/RTSP to Unicast HTTP stream converter.')); + + s = m.section(form.TypedSection, 'instance'); + s.anonymous = true; + s.addremove = true; + s.addbtntitle = _('Add instance'); + + s.tab('general', _('General settings')); + + o = s.taboption('general', form.Flag, 'enabled', _('Enable')); + + o = s.taboption('general', form.DynamicList, 'listen', _('Bind address')); + o.datatype = 'ipaddrport(1)'; + o.rmempty = false; + + o = s.taboption('general', form.ListValue, 'verbose', _('Log level'), + _('syslog severity level.')); + o.value('0', _('Fatal')); + o.value('1', _('Error')); + o.value('2', _('Warn')); + o.value('3', _('Info')); + o.value('4', _('Debug')); + o.default = '1'; + + o = s.taboption('general', widgets.DeviceSelect, 'upstream_interface', _('Upstream interface'), + _('Default interface for all upstream traffic.')); + o.noaliases = true; + o.nocreate = true; + o.optional = true; + + o = s.taboption('general', form.Value, 'mcast_rejoin_interval', _('Multicast rejoin interval'), + _('Periodic multicast rejoin interval in seconds.')); + o.datatype = 'and(uinteger, range(0,86400))'; + o.placeholder = '0'; + + o = s.taboption('general', form.Value, 'maxclients', _('Max clients'), + _('Serve max N requests simultaneously.')); + o.datatype = 'and(uinteger, range(1,5000))'; + o.placeholder = '5'; + + o = s.taboption('general', form.Value, 'workers', _('Worker processes'), + _('Number of worker processes with SO_REUSEPORT.')); + o.datatype = 'and(uinteger, range(1,64))'; + o.placeholder = '1'; + + o = s.taboption('general', form.Flag, 'zerocopy_on_send', _('Zero-copy on send'), + _('Enable zero-copy send with MSG_ZEROCOPY for better performance.')); + + s.tab('web', _('Web settings')); + + o = s.taboption('web', form.Value, 'hostname', _('Hostname'), + _('HTTP host header will be checked and must match this value to allow access when configured.')); + o.datatype = 'host'; + + o = s.taboption('web', form.Value, 'r2h_token', _('R2H token'), + _('Authentication token for HTTP requests.')); + + o = s.taboption('web', form.Value, 'cors_allow_origin', _('CORS allowed origin'), + _('Set Access-Control-Allow-Origin header to enable CORS.')); + + o = s.taboption('web', form.Flag, 'xff', _('Accept X-Forwarded-For header'), + _('Use X-Forwarded-For header as client address, also accepts X-Forwarded-Host / X-Forwarded-Proto headers as the base URL for M3U playlist conversion.')); + + o = s.taboption('web', form.Value, 'external_m3u', _('External M3U playlist URL'), + _('External M3U playlist URL (file://, http://, https:// supported).')); + + o = s.taboption('web', form.Value, 'external_m3u_update_interval', _('External M3U playlist update interval'), + _('External M3U playlist update interval in seconds.')); + o.datatype = 'uinteger'; + o.placeholder = '7200'; + o.depends({ 'external_m3u': '', '!reverse': true }); + + o = s.taboption('web', form.Value, 'player_page_path', _('Player page path'), + _('HTTP path for player UI.')); + o.placeholder = '/player'; + + o = s.taboption('web', form.Value, 'status_page_path', _('Status page path'), + _('HTTP path for status UI.')); + o.placeholder = '/status'; + + s.tab('advanced', _('Advanced settings')); + + o = s.taboption('advanced', form.Flag, 'noudpxy', _('Disable udpxy compatibility')); + + o = s.taboption('advanced', widgets.DeviceSelect, 'upstream_interface_fcc', _('Upstream FCC interface'), + _('Interface for FCC unicast traffic (overrides upstream interface).')); + o.noaliases = true; + o.nocreate = true; + o.optional = true; + + o = s.taboption('advanced', widgets.DeviceSelect, 'upstream_interface_http', _('Upstream HTTP interface'), + _('Interface for HTTP proxy upstream traffic (overrides upstream interface).')); + o.noaliases = true; + o.nocreate = true; + o.optional = true; + + o = s.taboption('advanced', widgets.DeviceSelect, 'upstream_interface_multicast', _('Upstream multicast interface'), + _('Interface for multicast traffic (overrides upstream interface).')); + o.noaliases = true; + o.nocreate = true; + o.optional = true; + + o = s.taboption('advanced', widgets.DeviceSelect, 'upstream_interface_rtsp', _('Upstream RTSP interface'), + _('Interface for RTSP unicast traffic (overrides upstream interface).')); + o.noaliases = true; + o.nocreate = true; + o.optional = true; + + o = s.taboption('advanced', form.Value, 'fcc_listen_port_range', _('FCC listen port range'), + _('Restrict FCC UDP listen sockets to specific ports.')); + o.datatype = 'or(port,portrange)'; + + o = s.taboption('advanced', form.Value, 'http_proxy_user_agent', _('HTTP proxy User-Agent'), + _('Override the User-Agent header sent to upstream HTTP proxy requests.')); + + o = s.taboption('advanced', form.Value, 'rtsp_stun_server', _('RTSP STUN server'), + _('STUN server for RTSP NAT traversal.')); + o.datatype = 'or(host,hostport)'; + + o = s.taboption('advanced', form.Value, 'rtsp_user_agent', _('RTSP User-Agent'), + _('User-Agent header for upstream RTSP requests.')); + + o = s.taboption('advanced', form.Value, 'buffer_pool_max_size', _('Buffer pool max size'), + _('Maximum number of buffers in zero-copy pool.')); + o.datatype = 'and(uinteger, range(1024,1048576))'; + o.placeholder = '16384'; + + o = s.taboption('advanced', form.Value, 'udp_rcvbuf_size', _('UDP receive buffer size'), + _('UDP socket receive buffer size (in bytes) for multicast/FCC/RTSP.')); + o.datatype = 'and(uinteger, range(65536,16777216))'; + o.placeholder = '524288'; + + o = s.taboption('advanced', form.Flag, 'video_snapshot', _('Video snapshot'), + _('Enable video snapshot feature. Clients can request snapshots with snapshot=1 query parameter.')); + + o = s.taboption('advanced', form.Value, 'ffmpeg_path', _('FFmpeg path'), + _('Path to FFmpeg executable.')); + o.placeholder = 'ffmpeg'; + o.depends('video_snapshot', '1'); + + o = s.taboption('advanced', form.Value, 'ffmpeg_args', _('FFmpeg arguments'), + _('Additional FFmpeg arguments for snapshot generation.')); + o.placeholder = '-hwaccel none'; + o.depends('video_snapshot', '1'); + + return m.render(); + } +}); diff --git a/luci-app-rtp2httpd/po/templates/rtp2httpd.pot b/luci-app-rtp2httpd/po/templates/rtp2httpd.pot new file mode 100644 index 00000000..806cb76a --- /dev/null +++ b/luci-app-rtp2httpd/po/templates/rtp2httpd.pot @@ -0,0 +1,288 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:73 +msgid "Accept X-Forwarded-For header" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:18 +msgid "Add instance" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:154 +msgid "Additional FFmpeg arguments for snapshot generation." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:93 +msgid "Advanced settings" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:68 +msgid "Authentication token for HTTP requests." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:24 +msgid "Bind address" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:135 +msgid "Buffer pool max size" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:70 +msgid "CORS allowed origin" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:34 +msgid "Debug" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:38 +msgid "Default interface for all upstream traffic." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:95 +msgid "Disable udpxy compatibility" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:22 +msgid "Enable" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:146 +msgid "" +"Enable video snapshot feature. Clients can request snapshots with " +"snapshot=1 query parameter." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:59 +msgid "" +"Enable zero-copy send with MSG_ZEROCOPY for better performance." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:31 +msgid "Error" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:76 +msgid "External M3U playlist URL" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:77 +msgid "External M3U playlist URL (file://, http://, https:// supported)." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:79 +msgid "External M3U playlist update interval" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:80 +msgid "External M3U playlist update interval in seconds." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:121 +msgid "FCC listen port range" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:153 +msgid "FFmpeg arguments" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:148 +msgid "FFmpeg path" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:30 +msgid "Fatal" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:20 +msgid "General settings" +msgstr "" + +#: applications/luci-app-rtp2httpd/root/usr/share/rpcd/acl.d/luci-app-rtp2httpd.json:3 +msgid "Grant UCI access for luci-app-rtp2httpd" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:64 +msgid "" +"HTTP host header will be checked and must match this value to allow access " +"when configured." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:86 +msgid "HTTP path for player UI." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:90 +msgid "HTTP path for status UI." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:125 +msgid "HTTP proxy User-Agent" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:63 +msgid "Hostname" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:33 +msgid "Info" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:98 +msgid "Interface for FCC unicast traffic (overrides upstream interface)." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:104 +msgid "" +"Interface for HTTP proxy upstream traffic (overrides upstream interface)." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:116 +msgid "Interface for RTSP unicast traffic (overrides upstream interface)." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:110 +msgid "Interface for multicast traffic (overrides upstream interface)." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:28 +msgid "Log level" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:48 +msgid "Max clients" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:136 +msgid "Maximum number of buffers in zero-copy pool." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:13 +msgid "Multicast RTP/RTSP to Unicast HTTP stream converter." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:43 +msgid "Multicast rejoin interval" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:54 +msgid "Number of worker processes with SO_REUSEPORT." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:126 +msgid "Override the User-Agent header sent to upstream HTTP proxy requests." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:149 +msgid "Path to FFmpeg executable." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:44 +msgid "Periodic multicast rejoin interval in seconds." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:85 +msgid "Player page path" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:67 +msgid "R2H token" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:128 +msgid "RTSP STUN server" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:132 +msgid "RTSP User-Agent" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:122 +msgid "Restrict FCC UDP listen sockets to specific ports." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:129 +msgid "STUN server for RTSP NAT traversal." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:49 +msgid "Serve max N requests simultaneously." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:71 +msgid "Set Access-Control-Allow-Origin header to enable CORS." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:89 +msgid "Status page path" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:140 +msgid "UDP receive buffer size" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:141 +msgid "UDP socket receive buffer size (in bytes) for multicast/FCC/RTSP." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:97 +msgid "Upstream FCC interface" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:103 +msgid "Upstream HTTP interface" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:115 +msgid "Upstream RTSP interface" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:37 +msgid "Upstream interface" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:109 +msgid "Upstream multicast interface" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:74 +msgid "" +"Use X-Forwarded-For header as client address, also accepts " +"X-Forwarded-Host / X-Forwarded-Proto headers as " +"the base URL for M3U playlist conversion." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:133 +msgid "User-Agent header for upstream RTSP requests." +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:145 +msgid "Video snapshot" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:32 +msgid "Warn" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:61 +msgid "Web settings" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:53 +msgid "Worker processes" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:58 +msgid "Zero-copy on send" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:12 +#: applications/luci-app-rtp2httpd/root/usr/share/luci/menu.d/luci-app-rtp2httpd.json:3 +msgid "rtp2httpd" +msgstr "" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:29 +msgid "syslog severity level." +msgstr "" diff --git a/luci-app-rtp2httpd/po/zh-cn b/luci-app-rtp2httpd/po/zh-cn new file mode 120000 index 00000000..8d69574d --- /dev/null +++ b/luci-app-rtp2httpd/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po b/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po new file mode 100644 index 00000000..d9af8b07 --- /dev/null +++ b/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po @@ -0,0 +1,299 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Project-Id-Version: PACKAGE VERSION\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh-Hans\n" +"MIME-Version: 1.0\n" +"Content-Transfer-Encoding: 8bit\n" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:73 +msgid "Accept X-Forwarded-For header" +msgstr "接受 X-Forwarded-For 头" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:18 +msgid "Add instance" +msgstr "添加实例" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:154 +msgid "Additional FFmpeg arguments for snapshot generation." +msgstr "用于快照生成的额外 FFmpeg 参数。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:93 +msgid "Advanced settings" +msgstr "高级设置" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:68 +msgid "Authentication token for HTTP requests." +msgstr "HTTP 请求的认证令牌。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:24 +msgid "Bind address" +msgstr "绑定地址" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:135 +msgid "Buffer pool max size" +msgstr "缓冲池最大容量" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:70 +msgid "CORS allowed origin" +msgstr "CORS 允许来源" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:34 +msgid "Debug" +msgstr "调试" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:38 +msgid "Default interface for all upstream traffic." +msgstr "所有上游流量的默认接口。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:95 +msgid "Disable udpxy compatibility" +msgstr "禁用 udpxy 兼容性" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:22 +msgid "Enable" +msgstr "启用" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:146 +msgid "" +"Enable video snapshot feature. Clients can request snapshots with " +"snapshot=1 query parameter." +msgstr "" +"启用视频快照功能。客户端可以通过 snapshot=1 查询参数请求快照。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:59 +msgid "" +"Enable zero-copy send with MSG_ZEROCOPY for better performance." +msgstr "启用 MSG_ZEROCOPY 零拷贝发送以提高性能。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:31 +msgid "Error" +msgstr "错误" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:76 +msgid "External M3U playlist URL" +msgstr "外部 M3U 播放列表 URL" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:77 +msgid "External M3U playlist URL (file://, http://, https:// supported)." +msgstr "外部 M3U 播放列表 URL(支持 file://、http://、https://)。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:79 +msgid "External M3U playlist update interval" +msgstr "外部 M3U 播放列表更新间隔" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:80 +msgid "External M3U playlist update interval in seconds." +msgstr "外部 M3U 播放列表更新间隔(秒)。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:121 +msgid "FCC listen port range" +msgstr "FCC 监听端口范围" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:153 +msgid "FFmpeg arguments" +msgstr "FFmpeg 参数" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:148 +msgid "FFmpeg path" +msgstr "FFmpeg 路径" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:30 +msgid "Fatal" +msgstr "致命" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:20 +msgid "General settings" +msgstr "常规设置" + +#: applications/luci-app-rtp2httpd/root/usr/share/rpcd/acl.d/luci-app-rtp2httpd.json:3 +msgid "Grant UCI access for luci-app-rtp2httpd" +msgstr "授予 luci-app-rtp2httpd 访问 UCI 配置的权限" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:64 +msgid "" +"HTTP host header will be checked and must match this value to allow access " +"when configured." +msgstr "配置后将检查 HTTP Host 头并且必须匹配此值才允许访问。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:86 +msgid "HTTP path for player UI." +msgstr "播放器页面 HTTP 路径。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:90 +msgid "HTTP path for status UI." +msgstr "状态页面 HTTP 路径。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:125 +msgid "HTTP proxy User-Agent" +msgstr "HTTP 代理 User-Agent" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:63 +msgid "Hostname" +msgstr "主机名" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:33 +msgid "Info" +msgstr "信息" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:98 +msgid "Interface for FCC unicast traffic (overrides upstream interface)." +msgstr "用于 FCC 单播流量的接口(覆盖默认上游接口)。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:104 +msgid "" +"Interface for HTTP proxy upstream traffic (overrides upstream interface)." +msgstr "用于 HTTP 代理上游流量的接口(覆盖默认上游接口)。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:116 +msgid "Interface for RTSP unicast traffic (overrides upstream interface)." +msgstr "用于 RTSP 单播流量的接口(覆盖默认上游接口)。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:110 +msgid "Interface for multicast traffic (overrides upstream interface)." +msgstr "用于组播流量的接口(覆盖默认上游接口)。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:28 +msgid "Log level" +msgstr "日志级别" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:48 +msgid "Max clients" +msgstr "最大客户端数" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:136 +msgid "Maximum number of buffers in zero-copy pool." +msgstr "零拷贝池中缓冲区的最大数量。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:13 +msgid "Multicast RTP/RTSP to Unicast HTTP stream converter." +msgstr "组播 RTP/RTSP 到单播 HTTP 流转换器。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:43 +msgid "Multicast rejoin interval" +msgstr "组播重新加入间隔" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:54 +msgid "Number of worker processes with SO_REUSEPORT." +msgstr "使用 SO_REUSEPORT 端口复用的工作进程数量。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:126 +msgid "Override the User-Agent header sent to upstream HTTP proxy requests." +msgstr "覆盖发送到上游 HTTP 代理请求的 User-Agent 头。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:149 +msgid "Path to FFmpeg executable." +msgstr "FFmpeg 可执行文件路径。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:44 +msgid "Periodic multicast rejoin interval in seconds." +msgstr "周期性组播重新加入间隔(秒)。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:85 +msgid "Player page path" +msgstr "播放器页面路径" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:67 +msgid "R2H token" +msgstr "HTTP 请求认证令牌" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:128 +msgid "RTSP STUN server" +msgstr "RTSP STUN 服务器" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:132 +msgid "RTSP User-Agent" +msgstr "RTSP User-Agent" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:122 +msgid "Restrict FCC UDP listen sockets to specific ports." +msgstr "限制 FCC UDP 监听套接字到特定端口。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:129 +msgid "STUN server for RTSP NAT traversal." +msgstr "用于 RTSP NAT 穿透的 STUN 服务器。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:49 +msgid "Serve max N requests simultaneously." +msgstr "最多同时处理 N 个请求。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:71 +msgid "Set Access-Control-Allow-Origin header to enable CORS." +msgstr "设置 Access-Control-Allow-Origin 头以启用 CORS。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:89 +msgid "Status page path" +msgstr "状态页面路径" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:140 +msgid "UDP receive buffer size" +msgstr "UDP 接收缓冲区大小" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:141 +msgid "UDP socket receive buffer size (in bytes) for multicast/FCC/RTSP." +msgstr "用于组播/FCC/RTSP 的 UDP 套接字接收缓冲区大小(字节)。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:97 +msgid "Upstream FCC interface" +msgstr "上游 FCC 接口" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:103 +msgid "Upstream HTTP interface" +msgstr "上游 HTTP 接口" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:115 +msgid "Upstream RTSP interface" +msgstr "上游 RTSP 接口" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:37 +msgid "Upstream interface" +msgstr "上游接口" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:109 +msgid "Upstream multicast interface" +msgstr "上游组播接口" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:74 +msgid "" +"Use X-Forwarded-For header as client address, also accepts " +"X-Forwarded-Host / X-Forwarded-Proto headers as " +"the base URL for M3U playlist conversion." +msgstr "" +"使用 X-Forwarded-For 头作为客户端地址,并接受 X-Forwarded-" +"Host / X-Forwarded-Proto 头作为 M3U 播放列表中的地址前" +"缀。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:133 +msgid "User-Agent header for upstream RTSP requests." +msgstr "用于上游 RTSP 请求的 User-Agent 头。" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:145 +msgid "Video snapshot" +msgstr "视频快照" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:32 +msgid "Warn" +msgstr "警告" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:61 +msgid "Web settings" +msgstr "Web 设置" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:53 +msgid "Worker processes" +msgstr "工作进程" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:58 +msgid "Zero-copy on send" +msgstr "零拷贝发送" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:12 +#: applications/luci-app-rtp2httpd/root/usr/share/luci/menu.d/luci-app-rtp2httpd.json:3 +msgid "rtp2httpd" +msgstr "rtp2httpd" + +#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:29 +msgid "syslog severity level." +msgstr "系统日志级别。" diff --git a/luci-app-rtp2httpd/root/usr/share/luci/menu.d/luci-app-rtp2httpd.json b/luci-app-rtp2httpd/root/usr/share/luci/menu.d/luci-app-rtp2httpd.json new file mode 100644 index 00000000..51445fc8 --- /dev/null +++ b/luci-app-rtp2httpd/root/usr/share/luci/menu.d/luci-app-rtp2httpd.json @@ -0,0 +1,13 @@ +{ + "admin/services/rtp2httpd": { + "title": "rtp2httpd", + "action": { + "type": "view", + "path": "rtp2httpd" + }, + "depends": { + "acl": [ "luci-app-rtp2httpd" ], + "fs": { "/etc/config/rtp2httpd": "file" } + } + } +} diff --git a/luci-app-rtp2httpd/root/usr/share/rpcd/acl.d/luci-app-rtp2httpd.json b/luci-app-rtp2httpd/root/usr/share/rpcd/acl.d/luci-app-rtp2httpd.json new file mode 100644 index 00000000..e055ce0b --- /dev/null +++ b/luci-app-rtp2httpd/root/usr/share/rpcd/acl.d/luci-app-rtp2httpd.json @@ -0,0 +1,11 @@ +{ + "luci-app-rtp2httpd": { + "description": "Grant UCI access for luci-app-rtp2httpd", + "read": { + "uci": [ "rtp2httpd" ] + }, + "write": { + "uci": [ "rtp2httpd" ] + } + } +} diff --git a/natflow/Makefile b/natflow/Makefile index 0539bef0..0ce9b73e 100644 --- a/natflow/Makefile +++ b/natflow/Makefile @@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk PKG_NAME:=natflow PKG_VERSION:=20260313 -PKG_RELEASE:=5 +PKG_RELEASE:=6 PKG_SOURCE:=$(PKG_VERSION).tar.xz PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git PKG_SOURCE_PROTO:=git -PKG_SOURCE_VERSION:=0659b45f42a8bc80bc9bf17537aa6ef379a92faf +PKG_SOURCE_VERSION:=499d98ccb21b83c8b626d51d96009f2d570b1270 PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_MAINTAINER:=Chen Minqiang PKG_LICENSE:=GPL-2.0