💋 Sync 2026-04-30 20:35:45

This commit is contained in:
github-actions[bot] 2026-04-30 20:35:45 +08:00
parent 70052617ed
commit a2af50b5c1
21 changed files with 1273 additions and 151 deletions

222
ipv6-neigh/Cargo.lock generated
View File

@ -2,12 +2,65 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "anstyle" name = "anstyle"
version = "1.0.14" version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" 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]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.102" version = "1.0.102"
@ -104,6 +157,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.7" version = "4.6.7"
@ -135,6 +194,15 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
]
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.5"
@ -146,6 +214,29 @@ dependencies = [
"syn", "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]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@ -322,6 +413,7 @@ name = "hickory-proto"
version = "0.26.0-beta.3" version = "0.26.0-beta.3"
source = "git+https://github.com/hickory-dns/hickory-dns.git?rev=34a4cca1a707b642052b427938b7d1e8aaa71e3a#34a4cca1a707b642052b427938b7d1e8aaa71e3a" source = "git+https://github.com/hickory-dns/hickory-dns.git?rev=34a4cca1a707b642052b427938b7d1e8aaa71e3a#34a4cca1a707b642052b427938b7d1e8aaa71e3a"
dependencies = [ dependencies = [
"bitflags",
"data-encoding", "data-encoding",
"idna", "idna",
"ipnet", "ipnet",
@ -329,7 +421,9 @@ dependencies = [
"once_cell", "once_cell",
"rand", "rand",
"ring", "ring",
"rustls-pki-types",
"thiserror 2.0.18", "thiserror 2.0.18",
"time",
"tinyvec", "tinyvec",
"tracing", "tracing",
"url", "url",
@ -461,8 +555,10 @@ name = "ip-neigh"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"env_logger",
"futures", "futures",
"hickory-proto", "hickory-proto",
"log",
"netlink-packet-core", "netlink-packet-core",
"netlink-packet-route", "netlink-packet-route",
"netlink-sys", "netlink-sys",
@ -478,12 +574,42 @@ version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.18" version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" 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]] [[package]]
name = "jni" name = "jni"
version = "0.22.4" version = "0.22.4"
@ -660,6 +786,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "num-conv"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.4" version = "1.21.4"
@ -670,6 +802,12 @@ dependencies = [
"portable-atomic", "portable-atomic",
] ]
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.5" version = "0.12.5"
@ -717,6 +855,15 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" 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]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.5" version = "0.1.5"
@ -726,6 +873,12 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.37" version = "0.2.37"
@ -786,6 +939,35 @@ dependencies = [
"bitflags", "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]] [[package]]
name = "ring" name = "ring"
version = "0.17.14" version = "0.17.14"
@ -826,6 +1008,15 @@ dependencies = [
"semver", "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]] [[package]]
name = "same-file" name = "same-file"
version = "1.0.6" version = "1.0.6"
@ -1024,6 +1215,25 @@ dependencies = [
"syn", "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]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.3" version = "0.8.3"
@ -1144,6 +1354,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.5.0" version = "2.5.0"
@ -1453,6 +1669,12 @@ dependencies = [
"synstructure", "synstructure",
] ]
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"
version = "0.2.4" version = "0.2.4"

View File

@ -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"] } hickory-proto = { git = "https://github.com/hickory-dns/hickory-dns.git", rev = "34a4cca1a707b642052b427938b7d1e8aaa71e3a", features = ["dnssec-ring"] }
ubus = "0.1.6" ubus = "0.1.6"
serde_json = "1.0.108" serde_json = "1.0.108"
log = "0.4"
env_logger = "0.11"
[profile.release] [profile.release]
opt-level = 3 opt-level = 3

View File

@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=ipv6-neigh PKG_NAME:=ipv6-neigh
PKG_VERSION:=0.1.0 PKG_VERSION:=0.1.0
PKG_RELEASE:=2 PKG_RELEASE:=3
PKG_BUILD_DEPENDS:=rust/host PKG_BUILD_DEPENDS:=rust/host
PKG_BUILD_PARALLEL:=1 PKG_BUILD_PARALLEL:=1

View File

@ -1,10 +1,22 @@
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; 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::op::{Message, ResponseCode, update_message};
use hickory_proto::rr::{Name, RData, RecordSet, RecordType, TSigner}; use hickory_proto::rr::{Name, RData, RecordSet, RecordType, TSigner};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream; 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<dyn std::error::Error>> {
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. /// DNS dynamic update client that sends RFC 2136 updates over TCP to the hickory-dns server.
pub(crate) struct DnsUpdater { pub(crate) struct DnsUpdater {
@ -35,7 +47,10 @@ impl DnsUpdater {
let mut rrset = RecordSet::with_ttl(name, RecordType::AAAA, ttl); let mut rrset = RecordSet::with_ttl(name, RecordType::AAAA, ttl);
rrset.add_rdata(RData::AAAA(addr.into())); rrset.add_rdata(RData::AAAA(addr.into()));
let msg = update_message::append(rrset, self.zone.clone(), false, false); 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(()) Ok(())
} }
@ -57,7 +72,10 @@ impl DnsUpdater {
let mut rrset = RecordSet::with_ttl(name, RecordType::A, ttl); let mut rrset = RecordSet::with_ttl(name, RecordType::A, ttl);
rrset.add_rdata(RData::A(addr.into())); rrset.add_rdata(RData::A(addr.into()));
let msg = update_message::append(rrset, self.zone.clone(), false, false); 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(()) Ok(())
} }
@ -72,7 +90,8 @@ impl DnsUpdater {
let mut rrset = RecordSet::new(name, RecordType::AAAA, 0); let mut rrset = RecordSet::new(name, RecordType::AAAA, 0);
rrset.add_rdata(RData::AAAA(addr.into())); rrset.add_rdata(RData::AAAA(addr.into()));
let msg = update_message::delete_by_rdata(rrset, self.zone.clone(), false); 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(()) Ok(())
} }
@ -86,7 +105,8 @@ impl DnsUpdater {
let mut rrset = RecordSet::new(name, RecordType::A, 0); let mut rrset = RecordSet::new(name, RecordType::A, 0);
rrset.add_rdata(RData::A(addr.into())); rrset.add_rdata(RData::A(addr.into()));
let msg = update_message::delete_by_rdata(rrset, self.zone.clone(), false); 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(()) Ok(())
} }
@ -98,15 +118,23 @@ impl DnsUpdater {
let bytes = msg.to_vec()?; let bytes = msg.to_vec()?;
let len = u16::try_from(bytes.len())?; let len = u16::try_from(bytes.len())?;
let mut stream = TcpStream::connect(self.server_addr).await?; let result = timeout(TCP_TIMEOUT, async {
stream.write_all(&len.to_be_bytes()).await?; let mut stream = TcpStream::connect(self.server_addr).await?;
stream.write_all(&bytes).await?; stream.write_all(&len.to_be_bytes()).await?;
stream.flush().await?; stream.write_all(&bytes).await?;
stream.flush().await?;
let resp_len = stream.read_u16().await? as usize; let resp_len = stream.read_u16().await? as usize;
let mut resp_buf = vec![0u8; resp_len]; let mut resp_buf = vec![0u8; resp_len];
stream.read_exact(&mut resp_buf).await?; stream.read_exact(&mut resp_buf).await?;
Ok(Message::from_vec(&resp_buf)?) Ok::<_, Box<dyn std::error::Error>>(Message::from_vec(&resp_buf)?)
})
.await
.map_err(|_| -> Box<dyn std::error::Error> {
format!("DNS TCP timeout after {}s connecting to {}", TCP_TIMEOUT.as_secs(), self.server_addr).into()
})??;
Ok(result)
} }
} }

View File

@ -1,8 +1,9 @@
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr as StdSocketAddr}; use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr as StdSocketAddr};
use futures::stream::StreamExt; use futures::stream::StreamExt;
use futures::stream::TryStreamExt; use futures::stream::TryStreamExt;
use log::{debug, error, info, warn};
use netlink_packet_core::NetlinkPayload; use netlink_packet_core::NetlinkPayload;
use netlink_packet_route::RouteNetlinkMessage; use netlink_packet_route::RouteNetlinkMessage;
use netlink_packet_route::neighbour::NeighbourMessage; use netlink_packet_route::neighbour::NeighbourMessage;
@ -47,6 +48,9 @@ struct Cli {
/// TSIG key name /// TSIG key name
#[clap(short = 'n', long, default_value = "update-key.")] #[clap(short = 'n', long, default_value = "update-key.")]
key_name: String, key_name: String,
/// Log level (error, warn, info, debug, trace)
#[clap(short = 'l', long, default_value = "info")]
log_level: String,
} }
#[derive(Debug)] #[derive(Debug)]
@ -95,52 +99,70 @@ fn if_ipv4_in_private_subnet(ip: &Ipv4Addr) -> bool {
false false
} }
async fn process_new_neigh(neigh: &Neigh, updater: &db::DnsUpdater, leases: &HashMap<String, String>) { async fn process_new_neigh(neigh: &Neigh, updater: &db::DnsUpdater, leases: &HashMap<String, String>) -> bool {
let Some(hostname) = leases.get(&neigh.mac) else { let Some(hostname) = leases.get(&neigh.mac) else {
eprintln!("no lease for mac {}, skipping DNS update", neigh.mac); debug!("no lease for mac {}, skipping DNS update", neigh.mac);
return; return true;
}; };
let result = match &neigh.inet { let result = match &neigh.inet {
NeighbourAddress::Inet6(addr) => updater.upsert_aaaa(hostname, *addr, DEFAULT_TTL).await, NeighbourAddress::Inet6(addr) => updater.upsert_aaaa(hostname, *addr, DEFAULT_TTL).await,
NeighbourAddress::Inet(addr) => updater.upsert_a(hostname, *addr, DEFAULT_TTL).await, NeighbourAddress::Inet(addr) => updater.upsert_a(hostname, *addr, DEFAULT_TTL).await,
_ => return, _ => return true,
}; };
match result { match result {
Ok(()) => println!("DNS update: added {} -> {:?}", hostname, neigh.inet), Ok(()) => {
Err(e) => eprintln!("DNS update failed for {}: {}", hostname, e), 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<String, String>) { async fn process_del_neigh(neigh: &Neigh, updater: &db::DnsUpdater, leases: &HashMap<String, String>) -> bool {
let Some(hostname) = leases.get(&neigh.mac) else { let Some(hostname) = leases.get(&neigh.mac) else {
return; return true;
}; };
let result = match &neigh.inet { let result = match &neigh.inet {
NeighbourAddress::Inet6(addr) => updater.delete_aaaa(hostname, *addr).await, NeighbourAddress::Inet6(addr) => updater.delete_aaaa(hostname, *addr).await,
NeighbourAddress::Inet(addr) => updater.delete_a(hostname, *addr).await, NeighbourAddress::Inet(addr) => updater.delete_a(hostname, *addr).await,
_ => return, _ => return true,
}; };
match result { match result {
Ok(()) => println!("DNS update: removed {} -> {:?}", hostname, neigh.inet), Ok(()) => {
Err(e) => eprintln!("DNS delete failed for {}: {}", hostname, e), 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 { fn should_skip_route_type(route_type: RouteType) -> bool {
match route_type { matches!(route_type, RouteType::Multicast | RouteType::Broadcast | RouteType::Local)
RouteType::Multicast => true,
RouteType::Broadcast => true,
_ => false,
}
} }
fn is_multicast_or_broadcast(neigh: &Neigh) -> bool { fn should_skip_neigh(neigh: &Neigh) -> bool {
return is_multicast_or_broadcast_route_type(neigh.kind); should_skip_route_type(neigh.kind)
} }
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), ()> { async fn main() -> Result<(), ()> {
let args = Cli::parse(); 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 private_subnet = args.private_subnet;
let zone = Name::from_ascii(&args.zone).expect("invalid zone name"); 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(); let (connection, handle, _) = new_connection().unwrap();
tokio::spawn(connection); 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 // Load DHCP leases (mac -> hostname) from ubus
let mut leases = op::get_lease().unwrap_or_default(); 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 // Dump existing neighbours and register them
println!("dumping neighbours"); debug!("dumping neighbours");
if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet).await { if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet).await {
for neigh in &neighbours { for neigh in &neighbours {
println!("{:?}", neigh); debug!("{:?}", neigh);
if !is_multicast_or_broadcast(neigh) { if !should_skip_neigh(neigh) {
process_new_neigh(neigh, &updater, &leases).await; 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) = let (mut conn, mut _handle, mut messages) =
new_connection().map_err(|e| format!("{e}")).unwrap(); new_connection().map_err(|e| format!("{e}")).unwrap();
@ -194,7 +227,7 @@ async fn main() -> Result<(), ()> {
if event_count % 50 == 0 { if event_count % 50 == 0 {
if let Ok(new_leases) = op::get_lease() { if let Ok(new_leases) = op::get_lease() {
leases = new_leases; 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 { let Some(neigh) = parse_neighbour_message(new_neigh, private_subnet) else {
continue; continue;
}; };
if is_multicast_or_broadcast(&neigh) { if should_skip_neigh(&neigh) {
continue; continue;
} }
println!("New neighbour: {:?}", neigh); // Only act on Reachable state to avoid redundant updates
process_new_neigh(&neigh, &updater, &leases).await; 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) => { RouteNetlinkMessage::DelNeighbour(del_neigh) => {
let Some(neigh) = parse_neighbour_message(del_neigh, private_subnet) else { let Some(neigh) = parse_neighbour_message(del_neigh, private_subnet) else {
continue; continue;
}; };
if is_multicast_or_broadcast(&neigh) { if should_skip_neigh(&neigh) {
continue; continue;
} }
println!("Del neighbour: {:?}", neigh); let key = (neigh.mac.clone(), inet_to_string(&neigh.inet));
process_del_neigh(&neigh, &updater, &leases).await; 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<u8>) -> String {
mac_str mac_str
} }
async fn dump_addresses(handle: Handle, link: Option<String>) -> Result<(), Error> { fn inet_to_string(addr: &NeighbourAddress) -> String {
let mut request = handle.link().get(); match addr {
if let Some(link) = link { NeighbourAddress::Inet(ip) => ip.to_string(),
request = request.match_name(link); 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(); let mut links = request.execute();
if let Some(link) = links.try_next().await? { if let Some(link) = links.try_next().await? {
@ -251,24 +310,33 @@ async fn dump_addresses(handle: Handle, link: Option<String>) -> Result<(), Erro
.set_link_index_filter(link.header.index) .set_link_index_filter(link.header.index)
.execute(); .execute();
while let Some(msg) = addresses.try_next().await? { while let Some(msg) = addresses.try_next().await? {
println!("{msg:?}"); debug!("{msg:?}");
} }
Ok(()) Ok(())
} else { } else {
eprintln!("link not found"); warn!("link not found");
Ok(()) 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<Neigh> { fn parse_neighbour_message(neigh: NeighbourMessage, private_subnet: bool) -> Option<Neigh> {
let state = neigh.header.state; let state = neigh.header.state;
if state == NeighbourState::Permanent { // Filter out static and incomplete entries
if matches!(state, NeighbourState::Permanent | NeighbourState::Noarp) {
return None; return None;
} }
let addr: NeighbourAddress = neigh.attributes.iter().find_map(|attr| match attr { let addr: NeighbourAddress = neigh.attributes.iter().find_map(|attr| match attr {
NeighbourAttribute::Destination(inet) => Some(inet.to_owned()), NeighbourAttribute::Destination(inet) => Some(inet.to_owned()),
_ => None, _ => None,
})?; })?;
// Link-local IPv6 addresses are interface-scoped and useless in DNS
if is_link_local_ipv6(&addr) {
return None;
}
if private_subnet { if private_subnet {
match addr { match addr {
NeighbourAddress::Inet(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()), NeighbourAttribute::LinkLocalAddress(mac) => Some(mac.to_owned()),
_ => None, _ => 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 { Some(Neigh {
ifindex, ifindex,
state, state,
kind, kind,
inet: addr, inet: addr,
mac: format_mac(mac), mac: mac_str,
}) })
} }
@ -304,7 +377,7 @@ async fn dump_neighbours(handle: Handle, private_subnet: bool) -> Result<Vec<Nei
let mut vec: Vec<Neigh> = Vec::new(); let mut vec: Vec<Neigh> = Vec::new();
while let Some(route) = neighbours.try_next().await? { while let Some(route) = neighbours.try_next().await? {
if let Some(neigh) = parse_neighbour_message(route, private_subnet) { if let Some(neigh) = parse_neighbour_message(route, private_subnet) {
if !is_multicast_or_broadcast(&neigh) { if !should_skip_neigh(&neigh) {
vec.push(neigh); vec.push(neigh);
} }
} }

View File

@ -5,32 +5,65 @@ use std::path::Path;
pub fn call_ubus(obj_path: &str, method: &str) -> Result<Value, Box<dyn std::error::Error>> { pub fn call_ubus(obj_path: &str, method: &str) -> Result<Value, Box<dyn std::error::Error>> {
let socket = Path::new("/var/run/ubus/ubus.sock"); let socket = Path::new("/var/run/ubus/ubus.sock");
let mut connection = match ubus::Connection::connect(&socket) { let mut connection = ubus::Connection::connect(&socket)?;
Ok(connection) => connection,
Err(err) => {
return Err(Box::new(err));
}
};
let json = connection.call(obj_path, method, "").unwrap(); let json = connection.call(obj_path, method, "")?;
let parsed: Value = serde_json::from_str(&json).unwrap(); let parsed: Value = serde_json::from_str(&json)?;
Ok(parsed) 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::<Vec<&str>>()
.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::<Vec<&str>>()
.join("-");
// Truncate to 63 chars (max DNS label length)
collapsed.chars().take(63).collect()
}
// get mac to hostname mapping // get mac to hostname mapping
pub fn get_lease() -> Result<HashMap<String, String>, Box<dyn std::error::Error>> { pub fn get_lease() -> Result<HashMap<String, String>, Box<dyn std::error::Error>> {
// dhcpv6 does not use mac address, so we only need to get ipv4 leases // dhcpv6 does not use mac address, so we only need to get ipv4 leases
let ip4lease = call_ubus("dhcp", "ipv4leases"); let leases = call_ubus("dhcp", "ipv4leases")?;
let leases = ip4lease.unwrap(); let devices = leases["device"].as_object()
let devices = leases["device"].as_object().unwrap(); .ok_or("missing 'device' in ipv4leases response")?;
let mut result = HashMap::new(); let mut result = HashMap::new();
// hard code for (_device, leases) in devices {
for (device, leases) in devices { let leases = leases["leases"].as_array()
let suffix = if device == "phy1-ap0" { ".iot" } else { ".lan" }; .ok_or("missing 'leases' array in device")?;
let leases = leases["leases"].as_array().unwrap();
for lease in leases { for lease in leases {
let mac = lease["mac"].as_str().unwrap().to_string(); let Some(raw_mac) = lease["mac"].as_str() else { continue };
let hostname = lease["hostname"].as_str().unwrap().to_string() + suffix; 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); result.insert(mac, hostname);
} }
} }

View File

@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.4.15 PKG_VERSION:=26.4.15
PKG_RELEASE:=103 PKG_RELEASE:=104
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \

View File

@ -509,7 +509,7 @@ o = s:option(ListValue, "xray_dns_mode", translate("Request protocol"))
o.default = "tcp" o.default = "tcp"
o:value("tcp", "TCP") o:value("tcp", "TCP")
o:value("udp", "UDP") 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("dns_mode", "xray")
o.cfgvalue = function(self, section) o.cfgvalue = function(self, section)
return m:get(section, "v2ray_dns_mode") 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({dns_mode = "dns2socks"})
o:depends({xray_dns_mode = "udp"}) o:depends({xray_dns_mode = "udp"})
o:depends({xray_dns_mode = "tcp"}) o:depends({xray_dns_mode = "tcp"})
o:depends({xray_dns_mode = "tcp+doh"})
o:depends({singbox_dns_mode = "udp"}) o:depends({singbox_dns_mode = "udp"})
o:depends({singbox_dns_mode = "tcp"}) o:depends({singbox_dns_mode = "tcp"})
@ -589,7 +588,7 @@ o.validate = function(self, value, t)
end end
return nil, translate("DoH request address") .. " " .. translate("Format must be:") .. " URL,IP" return nil, translate("DoH request address") .. " " .. translate("Format must be:") .. " URL,IP"
end 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 = "doh"})
o:depends({singbox_dns_mode = "http3"}) o:depends({singbox_dns_mode = "http3"})

View File

@ -379,7 +379,7 @@ o = s:taboption("DNS", ListValue, "xray_dns_mode", translate("Remote DNS") .. "
o.default = "tcp" o.default = "tcp"
o:value("tcp", "TCP") o:value("tcp", "TCP")
o:value("udp", "UDP") 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("dns_mode", "xray")
o:depends("smartdns_dns_mode", "xray") o:depends("smartdns_dns_mode", "xray")
o.cfgvalue = function(self, section) o.cfgvalue = function(self, section)
@ -438,7 +438,6 @@ o:depends({dns_mode = "tcp"})
o:depends({dns_mode = "udp"}) o:depends({dns_mode = "udp"})
o:depends({xray_dns_mode = "udp"}) o:depends({xray_dns_mode = "udp"})
o:depends({xray_dns_mode = "tcp"}) o:depends({xray_dns_mode = "tcp"})
o:depends({xray_dns_mode = "tcp+doh"})
o:depends({singbox_dns_mode = "udp"}) o:depends({singbox_dns_mode = "udp"})
o:depends({singbox_dns_mode = "tcp"}) 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/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:value("https://doh.libredns.gr/ads,116.202.176.26", "116.202.176.26 (LibreDNS-NoAds)")
o.validate = doh_validate 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 = "doh"})
o:depends({singbox_dns_mode = "http3"}) o:depends({singbox_dns_mode = "http3"})

View File

@ -1498,7 +1498,7 @@ function gen_config(var)
}) })
end 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 if not routing then
routing = { routing = {
domainStrategy = "IPOnDemand", domainStrategy = "IPOnDemand",
@ -1659,19 +1659,9 @@ function gen_config(var)
table.insert(outbounds, { table.insert(outbounds, {
tag = "dns-out", tag = "dns-out",
protocol = "dns", protocol = "dns",
proxySettings = dns_outbound_tag and {
tag = dns_outbound_tag
} or nil,
settings = { 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 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 { rules = (api.compare_versions(xray_version, ">", "26.4.17")) and {{ action = "hijack" }} or nil
{
action = ((node and node.protocol == "_shunt") or remote_dns_fake or dns.clientIP) and "hijack" or "direct"
}
} or nil
} }
}) })

View File

@ -181,9 +181,6 @@ msgstr "%s 不能为空。"
msgid "Filter Mode" msgid "Filter Mode"
msgstr "过滤模式" msgstr "过滤模式"
msgid "A/AAAA type"
msgstr "A/AAAA 类型"
msgid "TCP node must be '%s' type to use FakeDNS." msgid "TCP node must be '%s' type to use FakeDNS."
msgstr "TCP 节点必须是 '%s' 类型才能使用 FakeDNS。" msgstr "TCP 节点必须是 '%s' 类型才能使用 FakeDNS。"

View File

@ -181,6 +181,7 @@ run_singbox() {
[ "$remote_dns_protocol" = "http3" ] && json_add_string "remote_dns_http3" "1" [ "$remote_dns_protocol" = "http3" ] && json_add_string "remote_dns_http3" "1"
;; ;;
esac esac
[ -n "$remote_dns_client_ip" ] && json_add_string "remote_dns_client_ip" "${remote_dns_client_ip}" [ -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" [ "$remote_fakedns" = "1" ] && json_add_string "remote_dns_fake" "1"
[ -n "$no_run" ] && json_add_string "no_run" "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}" [ -n "$dns_cache" ] && json_add_string "dns_cache" "${dns_cache}"
case "$remote_dns_protocol" in case "$remote_dns_protocol" in
udp) udp|tcp)
local _dns=$(get_first_dns remote_dns_udp_server 53 | sed 's/#/:/g') 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_address=$(echo ${_dns} | awk -F ':' '{print $1}')
local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}') local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}')
json_add_string "remote_dns_udp_server" "${_dns_address}" json_add_string "remote_dns_${_proto}_server" "${_dns_address}"
json_add_string "remote_dns_udp_port" "${_dns_port}" json_add_string "remote_dns_${_proto}_port" "${_dns_port}"
;; ;;
tcp|tcp+doh) doh)
local _dns=$(get_first_dns remote_dns_tcp_server 53 | sed 's/#/:/g') local _doh_url _doh_host _doh_port _doh_bootstrap
local _dns_address=$(echo ${_dns} | awk -F ':' '{print $1}') parse_doh "$remote_dns_doh" _doh_url _doh_host _doh_port _doh_bootstrap
local _dns_port=$(echo ${_dns} | awk -F ':' '{print $2}') [ -n "$_doh_bootstrap" ] && json_add_string "remote_dns_doh_ip" "${_doh_bootstrap}"
json_add_string "remote_dns_tcp_server" "${_dns_address}" json_add_string "remote_dns_doh_port" "${_doh_port}"
json_add_string "remote_dns_tcp_port" "${_dns_port}" json_add_string "remote_dns_doh_url" "${_doh_url}"
[ "$remote_dns_protocol" = "tcp+doh" ] && { json_add_string "remote_dns_doh_host" "${_doh_host}"
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 esac
json_add_string "loglevel" "$loglevel" json_add_string "loglevel" "$loglevel"
[ -n "$no_run" ] && json_add_string "no_run" "1" [ -n "$no_run" ] && json_add_string "no_run" "1"
local _json_arg="$(json_dump)" local _json_arg="$(json_dump)"
@ -754,7 +750,7 @@ run_redir() {
local v2ray_dns_mode=$(config_t_get global v2ray_dns_mode tcp) local v2ray_dns_mode=$(config_t_get global v2ray_dns_mode tcp)
[ "${DNS_MODE}" != "sing-box" ] && [ "$protocol" = "_shunt" ] && { [ "${DNS_MODE}" != "sing-box" ] && [ "$protocol" = "_shunt" ] && {
DNS_MODE="sing-box" 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" ] && { [ "$protocol" = "_shunt" ] && {
@ -793,7 +789,7 @@ run_redir() {
resolve_dns_log="Sing-Box DNS(127.0.0.1#${resolve_dns_port}) -> ${_proto}://${REMOTE_DNS}" resolve_dns_log="Sing-Box DNS(127.0.0.1#${resolve_dns_port}) -> ${_proto}://${REMOTE_DNS}"
;; ;;
doh|http3) 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}" _args="${_args} remote_dns_doh=${remote_dns_doh}"
resolve_dns_log="Sing-Box DNS(127.0.0.1#${resolve_dns_port}) -> ${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) local v2ray_dns_mode=$(config_t_get global v2ray_dns_mode tcp)
[ "${DNS_MODE}" != "xray" ] && [ "$protocol" = "_shunt" ] && { [ "${DNS_MODE}" != "xray" ] && [ "$protocol" = "_shunt" ] && {
DNS_MODE="xray" 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" ] && { [ "$protocol" = "_shunt" ] && {
local geoip_path="${V2RAY_LOCATION_ASSET%*/}/geoip.dat" local geoip_path="${V2RAY_LOCATION_ASSET%*/}/geoip.dat"
@ -866,19 +864,15 @@ run_redir() {
_args="${_args} remote_dns_protocol=${v2ray_dns_mode}" _args="${_args} remote_dns_protocol=${v2ray_dns_mode}"
case "$v2ray_dns_mode" in case "$v2ray_dns_mode" in
udp) udp|tcp)
_args="${_args} remote_dns_udp_server=${REMOTE_DNS}" local _proto="$v2ray_dns_mode"
resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> udp://${REMOTE_DNS}" _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) doh)
_args="${_args} remote_dns_tcp_server=${REMOTE_DNS}" local remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query")
if [ "$v2ray_dns_mode" = "tcp+doh" ]; then _args="${_args} remote_dns_doh=${remote_dns_doh}"
remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query") resolve_dns_log="Xray DNS(127.0.0.1#${resolve_dns_port}) -> ${remote_dns_doh}"
_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
;; ;;
esac esac
local remote_fakedns=$(config_t_get global remote_fakedns 0) 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}" echolog " - Sing-Box DNS(${TUN_DNS}) -> ${_proto}://${REMOTE_DNS}"
;; ;;
doh|http3) 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}" _args="${_args} remote_dns_doh=${remote_dns_doh}"
echolog " - Sing-Box DNS(${TUN_DNS}) -> ${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}" [ -n "${_remote_dns_client_ip}" ] && _args="${_args} remote_dns_client_ip=${_remote_dns_client_ip}"
TCP_PROXY_DNS=1 TCP_PROXY_DNS=1
local v2ray_dns_mode=$(config_t_get global v2ray_dns_mode tcp) 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} dns_listen_port=${NEXT_DNS_LISTEN_PORT}"
_args="${_args} remote_dns_protocol=${v2ray_dns_mode}" _args="${_args} remote_dns_protocol=${v2ray_dns_mode}"
case "$v2ray_dns_mode" in case "$v2ray_dns_mode" in
udp) udp|tcp)
_args="${_args} remote_dns_udp_server=${REMOTE_DNS}" local _proto="$v2ray_dns_mode"
echolog " - Xray DNS(${TUN_DNS}) -> udp://${REMOTE_DNS}" _args="${_args} remote_dns_${_proto}_server=${REMOTE_DNS}"
echolog " - Xray DNS(${TUN_DNS}) -> ${_proto}://${REMOTE_DNS}"
;; ;;
tcp|tcp+doh) doh)
_args="${_args} remote_dns_tcp_server=${REMOTE_DNS}" local remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query")
if [ "$v2ray_dns_mode" = "tcp+doh" ]; then _args="${_args} remote_dns_doh=${remote_dns_doh}"
remote_dns_doh=$(config_t_get global remote_dns_doh "https://1.1.1.1/dns-query") echolog " - Xray DNS(${TUN_DNS}) -> ${remote_dns_doh}"
_args="${_args} remote_dns_doh=${remote_dns_doh}"
echolog " - Xray DNS(${TUN_DNS}) -> (${remote_dns_doh})(A/AAAA) + tcp://${REMOTE_DNS}"
local _doh_url _doh_host _doh_port _doh_bootstrap local _doh_url _doh_host _doh_port _doh_bootstrap
parse_doh "$remote_dns_doh" _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}" [ -n "${_doh_bootstrap}" ] && REMOTE_DNS="${REMOTE_DNS},${_doh_bootstrap}#${_doh_port}"
else
echolog " - Xray DNS(${TUN_DNS}) -> tcp://${REMOTE_DNS}"
fi
;; ;;
esac esac
_args="${_args} dns_socks_address=127.0.0.1 dns_socks_port=${tcp_node_socks_port}" _args="${_args} dns_socks_address=127.0.0.1 dns_socks_port=${tcp_node_socks_port}"
@ -1664,12 +1656,14 @@ acl_app() {
else else
local type=$(echo $(config_n_get $tcp_node type) | tr 'A-Z' 'a-z') local type=$(echo $(config_n_get $tcp_node type) | tr 'A-Z' 'a-z')
local protocol=$(config_n_get $tcp_node protocol) 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" ] && { ([ "$type" = "sing-box" ] || [ "$type" = "xray" ]) && [ "$protocol" = "_shunt" ] && [ "$type" != "$dns_mode" ] && {
dns_mode=$type 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_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}" 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}"
} }

View File

@ -1940,11 +1940,12 @@ local function update_node(manual)
end end
end end
if domain_strategy then if domain_strategy then
local ds = domain_strategy
if vvv == "sing-box" then if vvv == "sing-box" then
local map = { UseIPv4v6 = "prefer_ipv4", UseIPv6v4 = "prefer_ipv6", UseIPv4 = "ipv4_only", UseIPv6 = "ipv6_only" } 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 end
uci:set(appname, cfgid, "domain_strategy", domain_strategy) uci:set(appname, cfgid, "domain_strategy", ds)
end end
end end
-- 订阅组链式代理 -- 订阅组链式代理

View File

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

View File

@ -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 <code>SO_REUSEPORT</code>.'));
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 <code>MSG_ZEROCOPY</code> 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 <code>Access-Control-Allow-Origin</code> header to enable CORS.'));
o = s.taboption('web', form.Flag, 'xff', _('Accept X-Forwarded-For header'),
_('Use <code>X-Forwarded-For</code> header as client address, also accepts <code>X-Forwarded-Host</code> / <code>X-Forwarded-Proto</code> 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 <code>snapshot=1</code> 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();
}
});

View File

@ -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 "
"<code>snapshot=1</code> query parameter."
msgstr ""
#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:59
msgid ""
"Enable zero-copy send with <code>MSG_ZEROCOPY</code> 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 <code>SO_REUSEPORT</code>."
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 <code>Access-Control-Allow-Origin</code> 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 <code>X-Forwarded-For</code> header as client address, also accepts "
"<code>X-Forwarded-Host</code> / <code>X-Forwarded-Proto</code> 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 ""

1
luci-app-rtp2httpd/po/zh-cn Symbolic link
View File

@ -0,0 +1 @@
zh_Hans

View File

@ -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 "
"<code>snapshot=1</code> query parameter."
msgstr ""
"启用视频快照功能。客户端可以通过 <code>snapshot=1</code> 查询参数请求快照。"
#: applications/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js:59
msgid ""
"Enable zero-copy send with <code>MSG_ZEROCOPY</code> for better performance."
msgstr "启用 <code>MSG_ZEROCOPY</code> 零拷贝发送以提高性能。"
#: 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 <code>SO_REUSEPORT</code>."
msgstr "使用 <code>SO_REUSEPORT</code> 端口复用的工作进程数量。"
#: 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 <code>Access-Control-Allow-Origin</code> header to enable CORS."
msgstr "设置 <code>Access-Control-Allow-Origin</code> 头以启用 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 <code>X-Forwarded-For</code> header as client address, also accepts "
"<code>X-Forwarded-Host</code> / <code>X-Forwarded-Proto</code> headers as "
"the base URL for M3U playlist conversion."
msgstr ""
"使用 <code>X-Forwarded-For</code> 头作为客户端地址,并接受 <code>X-Forwarded-"
"Host</code> / <code>X-Forwarded-Proto</code> 头作为 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 "系统日志级别。"

View File

@ -0,0 +1,13 @@
{
"admin/services/rtp2httpd": {
"title": "rtp2httpd",
"action": {
"type": "view",
"path": "rtp2httpd"
},
"depends": {
"acl": [ "luci-app-rtp2httpd" ],
"fs": { "/etc/config/rtp2httpd": "file" }
}
}
}

View File

@ -0,0 +1,11 @@
{
"luci-app-rtp2httpd": {
"description": "Grant UCI access for luci-app-rtp2httpd",
"read": {
"uci": [ "rtp2httpd" ]
},
"write": {
"uci": [ "rtp2httpd" ]
}
}
}

View File

@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=natflow PKG_NAME:=natflow
PKG_VERSION:=20260313 PKG_VERSION:=20260313
PKG_RELEASE:=5 PKG_RELEASE:=6
PKG_SOURCE:=$(PKG_VERSION).tar.xz PKG_SOURCE:=$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=0659b45f42a8bc80bc9bf17537aa6ef379a92faf PKG_SOURCE_VERSION:=499d98ccb21b83c8b626d51d96009f2d570b1270
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION) PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com> PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
PKG_LICENSE:=GPL-2.0 PKG_LICENSE:=GPL-2.0