mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 04:13:56 +08:00
🌴 Sync 2026-06-02 21:18:52
This commit is contained in:
1 parent
ba4ce217ec
commit
f4c336b200
25 files changed
+3094
-2893
No files matched your search
+1
-1
@@ -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:=18
|
PKG_RELEASE:=19
|
||||||
|
|
||||||
PKG_BUILD_DEPENDS:=rust/host
|
PKG_BUILD_DEPENDS:=rust/host
|
||||||
PKG_BUILD_PARALLEL:=1
|
PKG_BUILD_PARALLEL:=1
|
||||||
|
|||||||
+138
-107
@@ -1,3 +1,4 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
@@ -8,6 +9,8 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
|
|
||||||
|
use crate::filter::if_ipv6_in_private_subnet;
|
||||||
|
|
||||||
const TCP_TIMEOUT: Duration = Duration::from_secs(5);
|
const TCP_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
fn check_response(response: &Message, allowed: &[ResponseCode]) -> Result<(), Box<dyn std::error::Error>> {
|
fn check_response(response: &Message, allowed: &[ResponseCode]) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@@ -20,6 +23,10 @@ fn check_response(response: &Message, allowed: &[ResponseCode]) -> Result<(), Bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
|
/// TCP connections are reused across calls to reduce syscall overhead.
|
||||||
|
///
|
||||||
|
/// Uses `RefCell` for interior mutability — safe because the entire program runs on a
|
||||||
|
/// single-threaded tokio runtime (`current_thread`).
|
||||||
pub(crate) struct DnsUpdater {
|
pub(crate) struct DnsUpdater {
|
||||||
server_addr: SocketAddr,
|
server_addr: SocketAddr,
|
||||||
zone: Name,
|
zone: Name,
|
||||||
@@ -28,11 +35,20 @@ pub(crate) struct DnsUpdater {
|
|||||||
ipv4_ptr_zones: Vec<(u32, u8, Name)>,
|
ipv4_ptr_zones: Vec<(u32, u8, Name)>,
|
||||||
/// Reverse zone for ULA IPv6 (d.f.ip6.arpa)
|
/// Reverse zone for ULA IPv6 (d.f.ip6.arpa)
|
||||||
ula_ptr_zone: Option<Name>,
|
ula_ptr_zone: Option<Name>,
|
||||||
|
/// Reusable TCP connection (lazy connect + auto-reconnect on failure).
|
||||||
|
conn: RefCell<Option<TcpStream>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DnsUpdater {
|
impl DnsUpdater {
|
||||||
pub fn new(server_addr: SocketAddr, zone: Name, signer: TSigner) -> Self {
|
pub fn new(server_addr: SocketAddr, zone: Name, signer: TSigner) -> Self {
|
||||||
Self { server_addr, zone, signer, ipv4_ptr_zones: vec![], ula_ptr_zone: None }
|
Self {
|
||||||
|
server_addr,
|
||||||
|
zone,
|
||||||
|
signer,
|
||||||
|
ipv4_ptr_zones: vec![],
|
||||||
|
ula_ptr_zone: None,
|
||||||
|
conn: RefCell::new(None),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure reverse PTR zones.
|
/// Configure reverse PTR zones.
|
||||||
@@ -49,6 +65,74 @@ impl DnsUpdater {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- generic helpers (eliminates upsert_a/upsert_aaaa and delete_a/delete_aaaa duplication) --
|
||||||
|
|
||||||
|
/// Create-or-append a record (A or AAAA). Tries CREATE first; on YXRRSet
|
||||||
|
/// (name already exists) falls back to APPEND so multiple addresses per host work.
|
||||||
|
async fn upsert_record(
|
||||||
|
&self,
|
||||||
|
hostname: &str,
|
||||||
|
rtype: RecordType,
|
||||||
|
rdata: RData,
|
||||||
|
ttl: u32,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let name = Name::from_ascii(hostname)?.append_domain(&self.zone)?;
|
||||||
|
let mut rrset = RecordSet::with_ttl(name.clone(), rtype, ttl);
|
||||||
|
rrset.add_rdata(rdata.clone());
|
||||||
|
let msg = update_message::create(rrset, self.zone.clone(), false);
|
||||||
|
let response = self.send_tcp(msg).await?;
|
||||||
|
if response.metadata.response_code == ResponseCode::YXRRSet {
|
||||||
|
let mut rrset = RecordSet::with_ttl(name, rtype, ttl);
|
||||||
|
rrset.add_rdata(rdata);
|
||||||
|
let msg = update_message::append(rrset, self.zone.clone(), false, false);
|
||||||
|
let response = self.send_tcp(msg).await?;
|
||||||
|
check_response(&response, &[])?;
|
||||||
|
} else {
|
||||||
|
check_response(&response, &[])?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a single record by rdata (A or AAAA).
|
||||||
|
async fn delete_record(
|
||||||
|
&self,
|
||||||
|
hostname: &str,
|
||||||
|
rtype: RecordType,
|
||||||
|
rdata: RData,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let name = Name::from_ascii(hostname)?.append_domain(&self.zone)?;
|
||||||
|
let mut rrset = RecordSet::new(name, rtype, 0);
|
||||||
|
rrset.add_rdata(rdata);
|
||||||
|
let msg = update_message::delete_by_rdata(rrset, self.zone.clone(), false);
|
||||||
|
let response = self.send_tcp(msg).await?;
|
||||||
|
check_response(&response, &[])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- public A / AAAA wrappers --
|
||||||
|
|
||||||
|
/// Create or append a AAAA record.
|
||||||
|
pub async fn upsert_aaaa(&self, hostname: &str, addr: Ipv6Addr, ttl: u32) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.upsert_record(hostname, RecordType::AAAA, RData::AAAA(addr.into()), ttl).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create or append an A record.
|
||||||
|
pub async fn upsert_a(&self, hostname: &str, addr: Ipv4Addr, ttl: u32) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.upsert_record(hostname, RecordType::A, RData::A(addr.into()), ttl).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a specific AAAA record.
|
||||||
|
pub async fn delete_aaaa(&self, hostname: &str, addr: Ipv6Addr) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.delete_record(hostname, RecordType::AAAA, RData::AAAA(addr.into())).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete a specific A record.
|
||||||
|
pub async fn delete_a(&self, hostname: &str, addr: Ipv4Addr) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
self.delete_record(hostname, RecordType::A, RData::A(addr.into())).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- PTR --
|
||||||
|
|
||||||
/// Upsert a PTR record. Silently skips IPs with no configured reverse zone.
|
/// Upsert a PTR record. Silently skips IPs with no configured reverse zone.
|
||||||
pub async fn upsert_ptr(
|
pub async fn upsert_ptr(
|
||||||
&self,
|
&self,
|
||||||
@@ -61,20 +145,18 @@ impl DnsUpdater {
|
|||||||
let Some(zone) = self.find_ipv4_ptr_zone(ip).cloned() else { return Ok(()) };
|
let Some(zone) = self.find_ipv4_ptr_zone(ip).cloned() else { return Ok(()) };
|
||||||
(ipv4_ptr_name(ip), zone)
|
(ipv4_ptr_name(ip), zone)
|
||||||
}
|
}
|
||||||
IpAddr::V6(ip) if (ip.segments()[0] & 0xfe00) == 0xfc00 => {
|
IpAddr::V6(ip) if if_ipv6_in_private_subnet(&ip) => {
|
||||||
let Some(zone) = self.ula_ptr_zone.clone() else { return Ok(()) };
|
let Some(zone) = self.ula_ptr_zone.clone() else { return Ok(()) };
|
||||||
(ipv6_ptr_name(ip), zone)
|
(ipv6_ptr_name(ip), zone)
|
||||||
}
|
}
|
||||||
_ => return Ok(()),
|
_ => return Ok(()),
|
||||||
};
|
};
|
||||||
let target = Name::from_ascii(hostname)?.append_domain(&self.zone)?;
|
let target = Name::from_ascii(hostname)?.append_domain(&self.zone)?;
|
||||||
|
|
||||||
// Delete any existing PTR rrset first (replace semantics: one PTR per IP).
|
// Delete any existing PTR rrset first (replace semantics: one PTR per IP).
|
||||||
let del_record = Record::update0(ptr_name.clone(), 0, RecordType::PTR);
|
let del_record = Record::update0(ptr_name.clone(), 0, RecordType::PTR);
|
||||||
let del_msg = update_message::delete_rrset(del_record, rev_zone.clone(), false);
|
let del_msg = update_message::delete_rrset(del_record, rev_zone.clone(), false);
|
||||||
let del_resp = self.send_tcp(del_msg).await?;
|
let del_resp = self.send_tcp(del_msg).await?;
|
||||||
check_response(&del_resp, &[])?;
|
check_response(&del_resp, &[])?;
|
||||||
|
|
||||||
// Add the new PTR.
|
// Add the new PTR.
|
||||||
let mut rrset = RecordSet::with_ttl(ptr_name, RecordType::PTR, ttl);
|
let mut rrset = RecordSet::with_ttl(ptr_name, RecordType::PTR, ttl);
|
||||||
rrset.add_rdata(RData::PTR(PtrRData(target)));
|
rrset.add_rdata(RData::PTR(PtrRData(target)));
|
||||||
@@ -94,7 +176,7 @@ impl DnsUpdater {
|
|||||||
let Some(zone) = self.find_ipv4_ptr_zone(ip).cloned() else { return Ok(()) };
|
let Some(zone) = self.find_ipv4_ptr_zone(ip).cloned() else { return Ok(()) };
|
||||||
(ipv4_ptr_name(ip), zone)
|
(ipv4_ptr_name(ip), zone)
|
||||||
}
|
}
|
||||||
IpAddr::V6(ip) if (ip.segments()[0] & 0xfe00) == 0xfc00 => {
|
IpAddr::V6(ip) if if_ipv6_in_private_subnet(&ip) => {
|
||||||
let Some(zone) = self.ula_ptr_zone.clone() else { return Ok(()) };
|
let Some(zone) = self.ula_ptr_zone.clone() else { return Ok(()) };
|
||||||
(ipv6_ptr_name(ip), zone)
|
(ipv6_ptr_name(ip), zone)
|
||||||
}
|
}
|
||||||
@@ -120,118 +202,72 @@ impl DnsUpdater {
|
|||||||
.map(|(_, _, name)| name)
|
.map(|(_, _, name)| name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create or append a AAAA record.
|
// -- TCP transport (connection-reusing) --
|
||||||
pub async fn upsert_aaaa(
|
|
||||||
&self,
|
|
||||||
hostname: &str,
|
|
||||||
addr: Ipv6Addr,
|
|
||||||
ttl: u32,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let name = Name::from_ascii(hostname)?.append_domain(&self.zone)?;
|
|
||||||
let mut rrset = RecordSet::with_ttl(name.clone(), RecordType::AAAA, ttl);
|
|
||||||
rrset.add_rdata(RData::AAAA(addr.into()));
|
|
||||||
|
|
||||||
let msg = update_message::create(rrset, self.zone.clone(), false);
|
/// Send a DNS message over TCP and read the response.
|
||||||
let response = self.send_tcp(msg).await?;
|
/// Reuses an existing connection when possible; reconnects automatically on failure.
|
||||||
if response.metadata.response_code == ResponseCode::YXRRSet {
|
|
||||||
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);
|
|
||||||
let response = self.send_tcp(msg).await?;
|
|
||||||
check_response(&response, &[])?;
|
|
||||||
} else {
|
|
||||||
check_response(&response, &[])?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create or append an A record.
|
|
||||||
pub async fn upsert_a(
|
|
||||||
&self,
|
|
||||||
hostname: &str,
|
|
||||||
addr: Ipv4Addr,
|
|
||||||
ttl: u32,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let name = Name::from_ascii(hostname)?.append_domain(&self.zone)?;
|
|
||||||
let mut rrset = RecordSet::with_ttl(name.clone(), RecordType::A, ttl);
|
|
||||||
rrset.add_rdata(RData::A(addr.into()));
|
|
||||||
|
|
||||||
let msg = update_message::create(rrset, self.zone.clone(), false);
|
|
||||||
let response = self.send_tcp(msg).await?;
|
|
||||||
if response.metadata.response_code == ResponseCode::YXRRSet {
|
|
||||||
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);
|
|
||||||
let response = self.send_tcp(msg).await?;
|
|
||||||
check_response(&response, &[])?;
|
|
||||||
} else {
|
|
||||||
check_response(&response, &[])?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a specific AAAA record.
|
|
||||||
pub async fn delete_aaaa(
|
|
||||||
&self,
|
|
||||||
hostname: &str,
|
|
||||||
addr: Ipv6Addr,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let name = Name::from_ascii(hostname)?.append_domain(&self.zone)?;
|
|
||||||
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);
|
|
||||||
let response = self.send_tcp(msg).await?;
|
|
||||||
check_response(&response, &[])?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a specific A record.
|
|
||||||
pub async fn delete_a(
|
|
||||||
&self,
|
|
||||||
hostname: &str,
|
|
||||||
addr: Ipv4Addr,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let name = Name::from_ascii(hostname)?.append_domain(&self.zone)?;
|
|
||||||
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);
|
|
||||||
let response = self.send_tcp(msg).await?;
|
|
||||||
check_response(&response, &[])?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Send a DNS message over TCP (2-byte length prefix + message bytes) and read the response.
|
|
||||||
async fn send_tcp(&self, mut msg: Message) -> Result<Message, Box<dyn std::error::Error>> {
|
async fn send_tcp(&self, mut msg: Message) -> Result<Message, Box<dyn std::error::Error>> {
|
||||||
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
|
||||||
msg.finalize(&self.signer, now)?;
|
msg.finalize(&self.signer, now)?;
|
||||||
|
|
||||||
let bytes = msg.to_vec()?;
|
let bytes = msg.to_vec()?;
|
||||||
let len = u16::try_from(bytes.len())?;
|
let len_bytes = u16::try_from(bytes.len())?.to_be_bytes();
|
||||||
|
|
||||||
let result = timeout(TCP_TIMEOUT, async {
|
// Try the existing connection first.
|
||||||
let mut stream = TcpStream::connect(self.server_addr).await?;
|
// Take it out of the RefCell so we don't hold the borrow across await.
|
||||||
stream.write_all(&len.to_be_bytes()).await?;
|
let existing = self.conn.borrow_mut().take();
|
||||||
stream.write_all(&bytes).await?;
|
if let Some(mut stream) = existing {
|
||||||
|
match Self::do_send_recv(&mut stream, &len_bytes, &bytes, self.server_addr).await {
|
||||||
|
Ok(resp) => {
|
||||||
|
// Put the connection back for reuse.
|
||||||
|
*self.conn.borrow_mut() = Some(stream);
|
||||||
|
return Ok(resp);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Connection broken — will reconnect below.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Establish a new connection.
|
||||||
|
let mut stream = timeout(TCP_TIMEOUT, TcpStream::connect(self.server_addr))
|
||||||
|
.await
|
||||||
|
.map_err(|_| -> Box<dyn std::error::Error> {
|
||||||
|
format!("DNS TCP connect timeout after {}s to {}", TCP_TIMEOUT.as_secs(), self.server_addr).into()
|
||||||
|
})??;
|
||||||
|
|
||||||
|
let resp = Self::do_send_recv(&mut stream, &len_bytes, &bytes, self.server_addr).await?;
|
||||||
|
*self.conn.borrow_mut() = Some(stream);
|
||||||
|
Ok(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a length-prefixed DNS message and read the response on an existing stream.
|
||||||
|
async fn do_send_recv(
|
||||||
|
stream: &mut TcpStream,
|
||||||
|
len_bytes: &[u8],
|
||||||
|
msg_bytes: &[u8],
|
||||||
|
server_addr: SocketAddr,
|
||||||
|
) -> Result<Message, Box<dyn std::error::Error>> {
|
||||||
|
timeout(TCP_TIMEOUT, async {
|
||||||
|
stream.write_all(len_bytes).await?;
|
||||||
|
stream.write_all(msg_bytes).await?;
|
||||||
stream.flush().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::<_, Box<dyn std::error::Error>>(Message::from_vec(&resp_buf)?)
|
Ok::<_, Box<dyn std::error::Error>>(Message::from_vec(&resp_buf)?)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|_| -> Box<dyn std::error::Error> {
|
.map_err(|_| -> Box<dyn std::error::Error> {
|
||||||
format!("DNS TCP timeout after {}s connecting to {}", TCP_TIMEOUT.as_secs(), self.server_addr).into()
|
format!("DNS TCP timeout after {}s to {}", TCP_TIMEOUT.as_secs(), server_addr).into()
|
||||||
})??;
|
})?
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- AXFR (uses its own connection -- streaming protocol) --
|
||||||
|
|
||||||
/// Fetch all A and AAAA records in the zone via AXFR (RFC 5936).
|
/// Fetch all A and AAAA records in the zone via AXFR (RFC 5936).
|
||||||
///
|
///
|
||||||
/// Returns a list of `(hostname_label, IpAddr)` pairs — only records whose owner
|
/// Returns a list of `(hostname_label, IpAddr)` pairs -- only records whose owner
|
||||||
/// name is directly under the zone apex (e.g. `foo.lan.` → `"foo"`).
|
/// name is directly under the zone apex (e.g. `foo.lan.` -> `"foo"`).
|
||||||
/// SOA, NS, and apex records are excluded.
|
/// SOA, NS, and apex records are excluded.
|
||||||
///
|
///
|
||||||
/// Requires `axfr_policy = "AllowAll"` (or `"AllowSigned"`) in the server config.
|
/// Requires `axfr_policy = "AllowAll"` (or `"AllowSigned"`) in the server config.
|
||||||
@@ -241,9 +277,9 @@ impl DnsUpdater {
|
|||||||
let query = Query::new(self.zone.clone(), RecordType::AXFR);
|
let query = Query::new(self.zone.clone(), RecordType::AXFR);
|
||||||
let mut msg = Message::query();
|
let mut msg = Message::query();
|
||||||
msg.add_query(query);
|
msg.add_query(query);
|
||||||
|
|
||||||
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 zone = self.zone.clone();
|
let zone = self.zone.clone();
|
||||||
let addr = self.server_addr;
|
let addr = self.server_addr;
|
||||||
|
|
||||||
@@ -264,11 +300,9 @@ impl DnsUpdater {
|
|||||||
let mut buf = vec![0u8; resp_len];
|
let mut buf = vec![0u8; resp_len];
|
||||||
stream.read_exact(&mut buf).await?;
|
stream.read_exact(&mut buf).await?;
|
||||||
let response = Message::from_vec(&buf)?;
|
let response = Message::from_vec(&buf)?;
|
||||||
|
|
||||||
if response.metadata.response_code != ResponseCode::NoError {
|
if response.metadata.response_code != ResponseCode::NoError {
|
||||||
return Err(format!("AXFR error: {:?}", response.metadata.response_code).into());
|
return Err(format!("AXFR error: {:?}", response.metadata.response_code).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
for record in &response.answers {
|
for record in &response.answers {
|
||||||
match &record.data {
|
match &record.data {
|
||||||
RData::SOA(_) => {
|
RData::SOA(_) => {
|
||||||
@@ -287,25 +321,22 @@ impl DnsUpdater {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if soa_count >= 2 {
|
if soa_count >= 2 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<_, Box<dyn std::error::Error>>(records)
|
Ok::<_, Box<dyn std::error::Error>>(records)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|_| -> Box<dyn std::error::Error> {
|
.map_err(|_| -> Box<dyn std::error::Error> {
|
||||||
format!("AXFR timeout after {}s connecting to {}", AXFR_TIMEOUT.as_secs(), self.server_addr).into()
|
format!("AXFR timeout after {}s connecting to {}", AXFR_TIMEOUT.as_secs(), self.server_addr).into()
|
||||||
})??;
|
})??;
|
||||||
|
|
||||||
Ok(records)
|
Ok(records)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the PTR owner name for an IPv4 address.
|
/// Build the PTR owner name for an IPv4 address.
|
||||||
/// e.g. 192.168.3.5 → `5.3.168.192.in-addr.arpa.`
|
/// e.g. 192.168.3.5 -> `5.3.168.192.in-addr.arpa.`
|
||||||
fn ipv4_ptr_name(addr: Ipv4Addr) -> Name {
|
fn ipv4_ptr_name(addr: Ipv4Addr) -> Name {
|
||||||
let o = addr.octets();
|
let o = addr.octets();
|
||||||
Name::from_ascii(&format!("{}.{}.{}.{}.in-addr.arpa.", o[3], o[2], o[1], o[0]))
|
Name::from_ascii(&format!("{}.{}.{}.{}.in-addr.arpa.", o[3], o[2], o[1], o[0]))
|
||||||
@@ -341,7 +372,7 @@ fn ipv4_zone_name(net: Ipv4Addr, prefix_len: u8) -> Name {
|
|||||||
|
|
||||||
/// Extract the single label that precedes the zone apex from a fully-qualified record name.
|
/// Extract the single label that precedes the zone apex from a fully-qualified record name.
|
||||||
///
|
///
|
||||||
/// e.g. `"foo.lan."` with zone `"lan."` → `Some("foo")`.
|
/// e.g. `"foo.lan."` with zone `"lan."` -> `Some("foo")`.
|
||||||
/// Returns `None` for the apex itself or for names not directly under the zone.
|
/// Returns `None` for the apex itself or for names not directly under the zone.
|
||||||
fn extract_hostname(name: &Name, zone: &Name) -> Option<String> {
|
fn extract_hostname(name: &Name, zone: &Name) -> Option<String> {
|
||||||
let n = name.to_ascii().to_lowercase();
|
let n = name.to_ascii().to_lowercase();
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||||
|
|
||||||
use crate::types::LanPrefix;
|
use crate::types::LanPrefix;
|
||||||
|
|
||||||
|
/// Returns true if `addr` is link-local (fe80::/10).
|
||||||
|
/// Centralised here so every module uses the same check.
|
||||||
|
pub(crate) fn is_link_local_ipv6(addr: &Ipv6Addr) -> bool {
|
||||||
|
(addr.segments()[0] & 0xffc0) == 0xfe80
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if `addr` is ULA (fc00::/7).
|
||||||
pub(crate) fn if_ipv6_in_private_subnet(ip: &Ipv6Addr) -> bool {
|
pub(crate) fn if_ipv6_in_private_subnet(ip: &Ipv6Addr) -> bool {
|
||||||
// Check if address is ULA (fc00::/7)
|
|
||||||
// Note: link-local is always filtered earlier by is_link_local_ipv6()
|
|
||||||
(ip.segments()[0] & 0xfe00) == 0xfc00
|
(ip.segments()[0] & 0xfe00) == 0xfc00
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,29 +17,36 @@ pub(crate) fn is_gua_ipv6(addr: &Ipv6Addr) -> bool {
|
|||||||
(addr.segments()[0] & 0xe000) == 0x2000
|
(addr.segments()[0] & 0xe000) == 0x2000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if this IPv6 address should be skipped for LAN purposes.
|
||||||
|
/// Skips link-local always; skips non-ULA when `private_subnet_v6` is set.
|
||||||
|
pub(crate) fn should_skip_v6(addr: &Ipv6Addr, private_subnet_v6: bool) -> bool {
|
||||||
|
if is_link_local_ipv6(addr) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if private_subnet_v6 && !if_ipv6_in_private_subnet(addr) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn if_ipv4_in_private_subnet(ip: &Ipv4Addr) -> bool {
|
pub(crate) fn if_ipv4_in_private_subnet(ip: &Ipv4Addr) -> bool {
|
||||||
let octets = ip.octets();
|
let octets = ip.octets();
|
||||||
|
|
||||||
// 10.0.0.0/8
|
// 10.0.0.0/8
|
||||||
if octets[0] == 10 {
|
if octets[0] == 10 {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 172.16.0.0/12
|
// 172.16.0.0/12
|
||||||
if octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31) {
|
if octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 192.168.0.0/16
|
// 192.168.0.0/16
|
||||||
if octets[0] == 192 && octets[1] == 168 {
|
if octets[0] == 192 && octets[1] == 168 {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 127.0.0.0/8 (loopback)
|
// 127.0.0.0/8 (loopback)
|
||||||
if octets[0] == 127 {
|
if octets[0] == 127 {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+37
-36
@@ -1,5 +1,4 @@
|
|||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
|
|
||||||
use futures::stream::TryStreamExt;
|
use futures::stream::TryStreamExt;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use netlink_packet_route::address::{AddressAttribute, AddressFlags, AddressHeaderFlags, AddressMessage};
|
use netlink_packet_route::address::{AddressAttribute, AddressFlags, AddressHeaderFlags, AddressMessage};
|
||||||
@@ -7,6 +6,7 @@ use rtnetlink::Handle;
|
|||||||
use tokio::time::{self, Duration};
|
use tokio::time::{self, Duration};
|
||||||
|
|
||||||
use crate::db::DnsUpdater;
|
use crate::db::DnsUpdater;
|
||||||
|
use crate::filter::should_skip_v6;
|
||||||
use crate::types::{DEFAULT_TTL, LanPrefix};
|
use crate::types::{DEFAULT_TTL, LanPrefix};
|
||||||
|
|
||||||
/// Check whether an address message is deprecated (preferred lifetime expired).
|
/// Check whether an address message is deprecated (preferred lifetime expired).
|
||||||
@@ -24,6 +24,28 @@ pub(crate) fn is_addr_deprecated(msg: &AddressMessage) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Look up the interface index and collect all address messages for `iface`.
|
||||||
|
/// Shared between `get_active_lan_prefixes` and `register_router_addresses`
|
||||||
|
/// to avoid duplicating the link-lookup + address-enumeration boilerplate.
|
||||||
|
async fn get_iface_addresses(
|
||||||
|
handle: Handle,
|
||||||
|
iface: &str,
|
||||||
|
) -> Result<(u32, Vec<AddressMessage>), String> {
|
||||||
|
let mut links = handle.link().get().match_name(iface.to_owned()).execute();
|
||||||
|
let link = match links.try_next().await {
|
||||||
|
Ok(Some(l)) => l,
|
||||||
|
Ok(None) => return Err(format!("interface {} not found", iface)),
|
||||||
|
Err(e) => return Err(format!("failed to find interface {}: {}", iface, e)),
|
||||||
|
};
|
||||||
|
let ifindex = link.header.index;
|
||||||
|
let mut addr_stream = handle.address().get().set_link_index_filter(ifindex).execute();
|
||||||
|
let mut msgs = Vec::new();
|
||||||
|
while let Ok(Some(msg)) = addr_stream.try_next().await {
|
||||||
|
msgs.push(msg);
|
||||||
|
}
|
||||||
|
Ok((ifindex, msgs))
|
||||||
|
}
|
||||||
|
|
||||||
/// Enumerate non-link-local IPv6 prefixes currently assigned to `iface`.
|
/// Enumerate non-link-local IPv6 prefixes currently assigned to `iface`.
|
||||||
/// Used during startup to filter out neighbour entries from expired/old prefixes.
|
/// Used during startup to filter out neighbour entries from expired/old prefixes.
|
||||||
pub(crate) async fn get_active_lan_prefixes(
|
pub(crate) async fn get_active_lan_prefixes(
|
||||||
@@ -31,29 +53,20 @@ pub(crate) async fn get_active_lan_prefixes(
|
|||||||
iface: &str,
|
iface: &str,
|
||||||
private_subnet_v6: bool,
|
private_subnet_v6: bool,
|
||||||
) -> Vec<LanPrefix> {
|
) -> Vec<LanPrefix> {
|
||||||
let mut links = handle.link().get().match_name(iface.to_owned()).execute();
|
let (_ifindex, msgs) = match get_iface_addresses(handle, iface).await {
|
||||||
let link = match links.try_next().await {
|
Ok(v) => v,
|
||||||
Ok(Some(l)) => l,
|
Err(_) => return vec![],
|
||||||
_ => return vec![],
|
|
||||||
};
|
};
|
||||||
let ifindex = link.header.index;
|
|
||||||
let mut addresses = handle.address().get().set_link_index_filter(ifindex).execute();
|
|
||||||
let mut prefixes = Vec::new();
|
let mut prefixes = Vec::new();
|
||||||
while let Ok(Some(msg)) = addresses.try_next().await {
|
for msg in &msgs {
|
||||||
let prefix_len = msg.header.prefix_len;
|
let prefix_len = msg.header.prefix_len;
|
||||||
// Skip deprecated addresses; they are no longer used for new connections
|
if is_addr_deprecated(msg) {
|
||||||
// and should not be treated as active LAN prefixes.
|
|
||||||
if is_addr_deprecated(&msg) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for attr in &msg.attributes {
|
for attr in &msg.attributes {
|
||||||
if let AddressAttribute::Address(IpAddr::V6(addr)) = attr {
|
if let AddressAttribute::Address(IpAddr::V6(addr)) = attr {
|
||||||
// Skip link-local
|
if should_skip_v6(addr, private_subnet_v6) {
|
||||||
if (addr.segments()[0] & 0xffc0) == 0xfe80 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let is_ula = (addr.segments()[0] & 0xfe00) == 0xfc00;
|
|
||||||
if private_subnet_v6 && !is_ula {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
prefixes.push(LanPrefix { addr: *addr, prefix_len });
|
prefixes.push(LanPrefix { addr: *addr, prefix_len });
|
||||||
@@ -72,22 +85,16 @@ pub(crate) async fn register_router_addresses(
|
|||||||
updater: &DnsUpdater,
|
updater: &DnsUpdater,
|
||||||
private_subnet_v6: bool,
|
private_subnet_v6: bool,
|
||||||
) {
|
) {
|
||||||
let mut links = handle.link().get().match_name(iface.to_owned()).execute();
|
let (_ifindex, msgs) = match get_iface_addresses(handle, iface).await {
|
||||||
let link = match links.try_next().await {
|
Ok(v) => v,
|
||||||
Ok(Some(l)) => l,
|
|
||||||
Ok(None) => {
|
|
||||||
warn!("interface {} not found, skipping router address registration", iface);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("failed to find interface {}: {}", iface, e);
|
warn!("{}, skipping router address registration", e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let ifindex = link.header.index;
|
|
||||||
let mut addresses = handle.address().get().set_link_index_filter(ifindex).execute();
|
for msg in &msgs {
|
||||||
while let Ok(Some(msg)) = addresses.try_next().await {
|
if is_addr_deprecated(msg) {
|
||||||
if is_addr_deprecated(&msg) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for attr in &msg.attributes {
|
for attr in &msg.attributes {
|
||||||
@@ -97,13 +104,7 @@ pub(crate) async fn register_router_addresses(
|
|||||||
};
|
};
|
||||||
match ip {
|
match ip {
|
||||||
IpAddr::V6(addr) => {
|
IpAddr::V6(addr) => {
|
||||||
// Always skip link-local
|
if should_skip_v6(addr, private_subnet_v6) {
|
||||||
if (addr.segments()[0] & 0xffc0) == 0xfe80 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Restrict to ULA only when private_subnet_v6 is set
|
|
||||||
let is_ula = (addr.segments()[0] & 0xfe00) == 0xfc00;
|
|
||||||
if private_subnet_v6 && !is_ula {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for name in std::iter::once(hostname).chain(extra_alias) {
|
for name in std::iter::once(hostname).chain(extra_alias) {
|
||||||
|
|||||||
+166
-242
@@ -1,10 +1,10 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr as StdSocketAddr};
|
use std::net::{Ipv4Addr, SocketAddr as StdSocketAddr};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use futures::stream::TryStreamExt;
|
use futures::stream::TryStreamExt;
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, 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;
|
||||||
@@ -29,10 +29,10 @@ mod types;
|
|||||||
|
|
||||||
use filter::{if_ipv4_in_private_subnet, if_ipv6_in_private_subnet, ipv6_in_prefix, ipv6_passes_active_prefix, is_gua_ipv6};
|
use filter::{if_ipv4_in_private_subnet, if_ipv6_in_private_subnet, ipv6_in_prefix, ipv6_passes_active_prefix, is_gua_ipv6};
|
||||||
use iface::{get_active_lan_prefixes, register_router_addresses, wait_for_dns_server};
|
use iface::{get_active_lan_prefixes, register_router_addresses, wait_for_dns_server};
|
||||||
use probe::{probe_gua_keepalive, probe_registered_neighbours, prune_gua_keepalive, send_icmpv4_echo, send_icmpv6_echo};
|
use probe::{Prober, probe_gua_keepalive, probe_registered_neighbours, prune_gua_keepalive};
|
||||||
use reconcile::{do_delete_dns, process_new_neigh, prune_ula_for_host, reconcile_dns};
|
use reconcile::{do_delete_dns, process_new_neigh, prune_ula_for_host, reconcile_dns};
|
||||||
use types::{
|
use types::{
|
||||||
DEFAULT_TTL, RTNLGRP_NEIGH, GuaKeepaliveEntry, Neigh, RegisteredEntry, nl_mgrp,
|
RTNLGRP_NEIGH, GuaKeepaliveEntry, Neigh, RegisteredEntry, format_mac, nl_mgrp,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
@@ -41,54 +41,68 @@ struct Cli {
|
|||||||
/// Restrict IPv4 neighbours to private subnets only (10/8, 172.16/12, 192.168/16, 127/8)
|
/// Restrict IPv4 neighbours to private subnets only (10/8, 172.16/12, 192.168/16, 127/8)
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
private_subnet_v4: bool,
|
private_subnet_v4: bool,
|
||||||
|
|
||||||
/// Also publish GUA (Global Unicast Address) AAAA records; by default only ULA (fc00::/7) is published
|
/// Also publish GUA (Global Unicast Address) AAAA records; by default only ULA (fc00::/7) is published
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
publish_gua: bool,
|
publish_gua: bool,
|
||||||
|
|
||||||
/// hickory-dns server address for DNS updates (e.g. "[::1]:5335")
|
/// hickory-dns server address for DNS updates (e.g. "[::1]:5335")
|
||||||
#[clap(short, long, default_value = "[::1]:5335")]
|
#[clap(short, long, default_value = "[::1]:5335")]
|
||||||
dns_server: StdSocketAddr,
|
dns_server: StdSocketAddr,
|
||||||
|
|
||||||
/// DNS zone to update (e.g. "lan")
|
/// DNS zone to update (e.g. "lan")
|
||||||
#[clap(short, long, default_value = "lan")]
|
#[clap(short, long, default_value = "lan")]
|
||||||
zone: String,
|
zone: String,
|
||||||
|
|
||||||
/// Path to TSIG key file (raw HMAC secret)
|
/// Path to TSIG key file (raw HMAC secret)
|
||||||
#[clap(short, long, default_value = "/etc/hickory-dns/update.key")]
|
#[clap(short, long, default_value = "/etc/hickory-dns/update.key")]
|
||||||
key_file: std::path::PathBuf,
|
key_file: std::path::PathBuf,
|
||||||
|
|
||||||
/// 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)
|
/// Log level (error, warn, info, debug, trace)
|
||||||
#[clap(short = 'l', long, default_value = "info")]
|
#[clap(short = 'l', long, default_value = "info")]
|
||||||
log_level: String,
|
log_level: String,
|
||||||
|
|
||||||
/// Probe interval in seconds for active reachability checks (0 to disable)
|
/// Probe interval in seconds for active reachability checks (0 to disable)
|
||||||
#[clap(long, default_value = "75")]
|
#[clap(long, default_value = "75")]
|
||||||
probe_interval: u64,
|
probe_interval: u64,
|
||||||
|
|
||||||
/// Network interface to read the router's own addresses from
|
/// Network interface to read the router's own addresses from
|
||||||
#[clap(long, default_value = "br-lan")]
|
#[clap(long, default_value = "br-lan")]
|
||||||
router_iface: String,
|
router_iface: String,
|
||||||
/// Additional DNS name to register router addresses under (e.g. "router" → "router.lan")
|
|
||||||
|
/// Additional DNS name to register router addresses under (e.g. "router" -> "router.lan")
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
router_alias: Option<String>,
|
router_alias: Option<String>,
|
||||||
|
|
||||||
/// Periodically probe the newest GUA per host to maintain NUD/Wi-Fi reachability (without publishing to DNS)
|
/// Periodically probe the newest GUA per host to maintain NUD/Wi-Fi reachability (without publishing to DNS)
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
keepalive_gua: bool,
|
keepalive_gua: bool,
|
||||||
|
|
||||||
/// Probe interval in seconds for GUA keepalive (only used with --keepalive-gua)
|
/// Probe interval in seconds for GUA keepalive (only used with --keepalive-gua)
|
||||||
#[clap(long, default_value = "120")]
|
#[clap(long, default_value = "120")]
|
||||||
keepalive_gua_interval: u64,
|
keepalive_gua_interval: u64,
|
||||||
|
|
||||||
/// Maximum number of GUA addresses to keepalive-probe per host
|
/// Maximum number of GUA addresses to keepalive-probe per host
|
||||||
#[clap(long, default_value = "2")]
|
#[clap(long, default_value = "2")]
|
||||||
keepalive_gua_per_host: usize,
|
keepalive_gua_per_host: usize,
|
||||||
|
|
||||||
/// Maximum number of ULA AAAA records to publish per host (oldest pruned first)
|
/// Maximum number of ULA AAAA records to publish per host (oldest pruned first)
|
||||||
#[clap(long, default_value = "2")]
|
#[clap(long, default_value = "2")]
|
||||||
max_ula_per_host: usize,
|
max_ula_per_host: usize,
|
||||||
|
|
||||||
/// IPv4 subnets for PTR reverse DNS (CIDR notation, e.g. "192.168.3.0/24", repeatable)
|
/// IPv4 subnets for PTR reverse DNS (CIDR notation, e.g. "192.168.3.0/24", repeatable)
|
||||||
#[clap(long, value_name = "SUBNET")]
|
#[clap(long, value_name = "SUBNET")]
|
||||||
ptr_ipv4_subnet: Vec<String>,
|
ptr_ipv4_subnet: Vec<String>,
|
||||||
|
|
||||||
/// Enable PTR reverse DNS for ULA IPv6 addresses (fc00::/7) via d.f.ip6.arpa
|
/// Enable PTR reverse DNS for ULA IPv6 addresses (fc00::/7) via d.f.ip6.arpa
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
ptr_ula: bool,
|
ptr_ula: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn should_skip_route_type(route_type: RouteType) -> bool {
|
fn should_skip_route_type(route_type: RouteType) -> bool {
|
||||||
matches!(route_type, RouteType::Multicast | RouteType::Broadcast | RouteType::Local)
|
matches!(route_type, RouteType::Multicast | RouteType::Broadcast | RouteType::Local)
|
||||||
}
|
}
|
||||||
@@ -103,7 +117,6 @@ fn is_failed_state(state: NeighbourState) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse an IPv4 CIDR string like "192.168.3.0/24" into (network_addr, prefix_len).
|
/// Parse an IPv4 CIDR string like "192.168.3.0/24" into (network_addr, prefix_len).
|
||||||
/// Panics with a clear message on invalid input.
|
|
||||||
fn parse_ipv4_cidr(s: &str) -> (Ipv4Addr, u8) {
|
fn parse_ipv4_cidr(s: &str) -> (Ipv4Addr, u8) {
|
||||||
let (addr_str, prefix_str) = s.split_once('/').unwrap_or_else(|| {
|
let (addr_str, prefix_str) = s.split_once('/').unwrap_or_else(|| {
|
||||||
panic!("invalid --ptr-ipv4-subnet \"{s}\": expected CIDR notation like \"192.168.3.0/24\"")
|
panic!("invalid --ptr-ipv4-subnet \"{s}\": expected CIDR notation like \"192.168.3.0/24\"")
|
||||||
@@ -112,12 +125,125 @@ fn parse_ipv4_cidr(s: &str) -> (Ipv4Addr, u8) {
|
|||||||
panic!("invalid --ptr-ipv4-subnet \"{s}\": \"{addr_str}\" is not a valid IPv4 address")
|
panic!("invalid --ptr-ipv4-subnet \"{s}\": \"{addr_str}\" is not a valid IPv4 address")
|
||||||
});
|
});
|
||||||
let prefix_len: u8 = prefix_str.parse().unwrap_or_else(|_| {
|
let prefix_len: u8 = prefix_str.parse().unwrap_or_else(|_| {
|
||||||
panic!("invalid --ptr-ipv4-subnet \"{s}\": prefix length must be 0–32")
|
panic!("invalid --ptr-ipv4-subnet \"{s}\": prefix length must be 0-32")
|
||||||
});
|
});
|
||||||
assert!(prefix_len <= 32, "invalid --ptr-ipv4-subnet \"{s}\": prefix length {prefix_len} > 32");
|
assert!(prefix_len <= 32, "invalid --ptr-ipv4-subnet \"{s}\": prefix length {prefix_len} > 32");
|
||||||
(addr, prefix_len)
|
(addr, prefix_len)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Register a new REACHABLE neighbour in DNS and add to the registered map.
|
||||||
|
/// Handles ULA pruning and delegates to `process_new_neigh` for the actual DNS update.
|
||||||
|
/// Caller must ensure the (hostname, ip) key is not already in `registered`.
|
||||||
|
async fn try_register_neigh(
|
||||||
|
neigh: &Neigh,
|
||||||
|
hostname: &str,
|
||||||
|
ip_str: &str,
|
||||||
|
registered: &mut HashMap<(String, String), RegisteredEntry>,
|
||||||
|
leases: &HashMap<String, String>,
|
||||||
|
updater: &db::DnsUpdater,
|
||||||
|
max_ula_per_host: usize,
|
||||||
|
private_subnet_v6: bool,
|
||||||
|
) {
|
||||||
|
// Enforce per-host ULA limit before adding.
|
||||||
|
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
|
||||||
|
if if_ipv6_in_private_subnet(addr) {
|
||||||
|
prune_ula_for_host(hostname, max_ula_per_host, registered, updater).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if process_new_neigh(neigh, updater, leases, private_subnet_v6).await {
|
||||||
|
registered.insert(
|
||||||
|
(hostname.to_owned(), ip_str.to_owned()),
|
||||||
|
RegisteredEntry {
|
||||||
|
hostname: hostname.to_owned(),
|
||||||
|
last_confirmed: Instant::now(),
|
||||||
|
ifindex: neigh.ifindex,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inet_to_string(addr: &NeighbourAddress) -> String {
|
||||||
|
match addr {
|
||||||
|
NeighbourAddress::Inet(ip) => ip.to_string(),
|
||||||
|
NeighbourAddress::Inet6(ip) => ip.to_string(),
|
||||||
|
other => format!("{other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_link_local_neigh_addr(addr: &NeighbourAddress) -> bool {
|
||||||
|
matches!(addr, NeighbourAddress::Inet6(ip) if filter::is_link_local_ipv6(ip))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_neighbour_message(neigh: NeighbourMessage, private_subnet_v4: bool) -> Option<Neigh> {
|
||||||
|
let state = neigh.header.state;
|
||||||
|
|
||||||
|
// 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_neigh_addr(&addr) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv4 private-subnet filter stays here (no keepalive concept for IPv4 GUA)
|
||||||
|
if let NeighbourAddress::Inet(ipv4) = &addr {
|
||||||
|
if private_subnet_v4 && !if_ipv4_in_private_subnet(ipv4) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let kind = neigh.header.kind;
|
||||||
|
let ifindex = neigh.header.ifindex;
|
||||||
|
|
||||||
|
let mac_bytes = neigh.attributes.iter().find_map(|attr| match attr {
|
||||||
|
NeighbourAttribute::LinkLayerAddress(mac) => Some(mac.to_owned()),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
|
||||||
|
// FAILED events may arrive without a hardware address (e.g. when ARP/NDP never
|
||||||
|
// succeeded, or the kernel cleared the lladdr on failure). Allow them through
|
||||||
|
// with an empty MAC so the FAILED handler can fall back to IP-based lookup.
|
||||||
|
let mac_str = match mac_bytes {
|
||||||
|
Some(m) => format_mac(&m),
|
||||||
|
None if matches!(state, NeighbourState::Failed) => String::new(),
|
||||||
|
None => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter out empty or all-zero MACs for non-FAILED states
|
||||||
|
// (router own addresses, incomplete entries).
|
||||||
|
if !matches!(state, NeighbourState::Failed) && (mac_str.is_empty() || mac_str == "00:00:00:00:00:00") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Neigh {
|
||||||
|
ifindex,
|
||||||
|
state,
|
||||||
|
kind,
|
||||||
|
inet: addr,
|
||||||
|
mac: mac_str,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn dump_neighbours(handle: Handle, private_subnet_v4: bool) -> Result<Vec<Neigh>, Error> {
|
||||||
|
let mut neighbours = handle.neighbours().get().execute();
|
||||||
|
let mut vec: Vec<Neigh> = Vec::new();
|
||||||
|
while let Some(route) = neighbours.try_next().await? {
|
||||||
|
if let Some(neigh) = parse_neighbour_message(route, private_subnet_v4) {
|
||||||
|
if !should_skip_neigh(&neigh) {
|
||||||
|
vec.push(neigh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(vec)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() -> Result<(), ()> {
|
async fn main() -> Result<(), ()> {
|
||||||
let args = Cli::parse();
|
let args = Cli::parse();
|
||||||
@@ -128,8 +254,6 @@ async fn main() -> Result<(), ()> {
|
|||||||
.parse()
|
.parse()
|
||||||
.expect("invalid log level (use: error, warn, info, debug, trace)"),
|
.expect("invalid log level (use: error, warn, info, debug, trace)"),
|
||||||
)
|
)
|
||||||
// Suppress spurious warnings from netlink_packet_route when the kernel
|
|
||||||
// provides more NLA data than the crate version expects (newer kernel).
|
|
||||||
.filter_module("netlink_packet_route", log::LevelFilter::Error)
|
.filter_module("netlink_packet_route", log::LevelFilter::Error)
|
||||||
.format_timestamp_secs()
|
.format_timestamp_secs()
|
||||||
.init();
|
.init();
|
||||||
@@ -140,8 +264,8 @@ async fn main() -> Result<(), ()> {
|
|||||||
let keepalive_gua_interval = args.keepalive_gua_interval;
|
let keepalive_gua_interval = args.keepalive_gua_interval;
|
||||||
let keepalive_gua_per_host = args.keepalive_gua_per_host;
|
let keepalive_gua_per_host = args.keepalive_gua_per_host;
|
||||||
let max_ula_per_host = args.max_ula_per_host;
|
let max_ula_per_host = args.max_ula_per_host;
|
||||||
let zone = Name::from_ascii(&args.zone).expect("invalid zone name");
|
|
||||||
|
|
||||||
|
let zone = Name::from_ascii(&args.zone).expect("invalid zone name");
|
||||||
let key_data = std::fs::read(&args.key_file)
|
let key_data = std::fs::read(&args.key_file)
|
||||||
.unwrap_or_else(|e| panic!("failed to read TSIG key file {:?}: {e}", args.key_file));
|
.unwrap_or_else(|e| panic!("failed to read TSIG key file {:?}: {e}", args.key_file));
|
||||||
let key_name = Name::from_ascii(&args.key_name).expect("invalid TSIG key name");
|
let key_name = Name::from_ascii(&args.key_name).expect("invalid TSIG key name");
|
||||||
@@ -152,20 +276,24 @@ async fn main() -> Result<(), ()> {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|s| parse_ipv4_cidr(s))
|
.map(|s| parse_ipv4_cidr(s))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let updater = db::DnsUpdater::new(args.dns_server, zone, signer)
|
let updater = db::DnsUpdater::new(args.dns_server, zone, signer)
|
||||||
.with_ptr_zones(&ipv4_ptr_subnets, args.ptr_ula);
|
.with_ptr_zones(&ipv4_ptr_subnets, args.ptr_ula);
|
||||||
|
|
||||||
|
// Create reusable ICMP probe sockets once at startup.
|
||||||
|
let prober = Prober::new().expect("failed to create ICMP probe sockets");
|
||||||
|
|
||||||
// Wait for the DNS server to become available before sending any updates.
|
// Wait for the DNS server to become available before sending any updates.
|
||||||
wait_for_dns_server(args.dns_server).await;
|
wait_for_dns_server(args.dns_server).await;
|
||||||
|
|
||||||
let (mut connection, handle, mut messages) = new_connection().unwrap();
|
let (mut connection, handle, mut messages) = new_connection().unwrap();
|
||||||
// Subscribe to neighbour events on the same connection used for queries.
|
|
||||||
// This avoids a second netlink socket and spawned task.
|
|
||||||
connection
|
connection
|
||||||
.socket_mut()
|
.socket_mut()
|
||||||
.socket_mut()
|
.socket_mut()
|
||||||
.bind(&SocketAddr::new(0, nl_mgrp(RTNLGRP_NEIGH)))
|
.bind(&SocketAddr::new(0, nl_mgrp(RTNLGRP_NEIGH)))
|
||||||
.expect("Failed to bind netlink multicast group");
|
.expect("Failed to bind netlink multicast group");
|
||||||
|
|
||||||
tokio::spawn(connection);
|
tokio::spawn(connection);
|
||||||
|
|
||||||
// Register router's own addresses in DNS
|
// Register router's own addresses in DNS
|
||||||
@@ -189,7 +317,6 @@ async fn main() -> Result<(), ()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load DHCP leases (mac -> hostname) from ubus.
|
// Load DHCP leases (mac -> hostname) from ubus.
|
||||||
// Use spawn_blocking: op::get_lease() makes a synchronous ubus call.
|
|
||||||
let mut leases: HashMap<String, String> = tokio::task::spawn_blocking(|| {
|
let mut leases: HashMap<String, String> = tokio::task::spawn_blocking(|| {
|
||||||
op::get_lease().unwrap_or_default()
|
op::get_lease().unwrap_or_default()
|
||||||
})
|
})
|
||||||
@@ -197,17 +324,9 @@ async fn main() -> Result<(), ()> {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
info!("loaded {} DHCP leases", leases.len());
|
info!("loaded {} DHCP leases", leases.len());
|
||||||
|
|
||||||
// State cache: tracks (mac, ip_string) -> registered entry (hostname, last confirmed time, ifindex)
|
|
||||||
let mut registered: HashMap<(String, String), RegisteredEntry> = HashMap::new();
|
let mut registered: HashMap<(String, String), RegisteredEntry> = HashMap::new();
|
||||||
|
|
||||||
// GUA keepalive: tracks GUA addresses per MAC for periodic probing (not published to DNS).
|
|
||||||
// Key = MAC address, value = list of GUA entries for that host.
|
|
||||||
let mut gua_keepalive: HashMap<String, Vec<GuaKeepaliveEntry>> = HashMap::new();
|
let mut gua_keepalive: HashMap<String, Vec<GuaKeepaliveEntry>> = HashMap::new();
|
||||||
|
|
||||||
// Get active LAN prefixes to filter out neighbours from expired/old prefixes.
|
|
||||||
// If prefix detection fails we log a warning but continue without filtering.
|
|
||||||
// When keepalive_gua is enabled, also include GUA prefixes so we can filter
|
|
||||||
// keepalive targets against active prefixes (pass private_subnet_v6=false).
|
|
||||||
let prefix_filter_v6 = if keepalive_gua { false } else { private_subnet_v6 };
|
let prefix_filter_v6 = if keepalive_gua { false } else { private_subnet_v6 };
|
||||||
let active_prefixes = get_active_lan_prefixes(handle.clone(), &args.router_iface, prefix_filter_v6).await;
|
let active_prefixes = get_active_lan_prefixes(handle.clone(), &args.router_iface, prefix_filter_v6).await;
|
||||||
if active_prefixes.is_empty() {
|
if active_prefixes.is_empty() {
|
||||||
@@ -223,13 +342,7 @@ async fn main() -> Result<(), ()> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dump existing neighbours:
|
// Dump existing neighbours
|
||||||
// - Skip entries whose IP is not within any active LAN prefix (old-prefix residuals).
|
|
||||||
// - Route GUA addresses to keepalive map (if enabled) instead of DNS.
|
|
||||||
// - Register REACHABLE entries immediately.
|
|
||||||
// - Send a probe to STALE/DELAY/PROBE entries; the event loop will register them
|
|
||||||
// once the kernel confirms they are REACHABLE.
|
|
||||||
// - Ignore FAILED and other states.
|
|
||||||
debug!("dumping neighbours");
|
debug!("dumping neighbours");
|
||||||
if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet_v4).await {
|
if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet_v4).await {
|
||||||
for neigh in &neighbours {
|
for neigh in &neighbours {
|
||||||
@@ -237,10 +350,10 @@ async fn main() -> Result<(), ()> {
|
|||||||
if should_skip_neigh(neigh) {
|
if should_skip_neigh(neigh) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Drop IPv6 neighbours that are not within any active LAN prefix.
|
|
||||||
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
|
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
|
||||||
if !active_prefixes.is_empty() && !active_prefixes.iter().any(|p| ipv6_in_prefix(*addr, p)) {
|
if !active_prefixes.is_empty() && !active_prefixes.iter().any(|p| ipv6_in_prefix(*addr, p)) {
|
||||||
debug!("init dump: skipping {} — not in any active LAN prefix", addr);
|
debug!("init dump: skipping {} -- not in any active LAN prefix", addr);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -265,15 +378,12 @@ async fn main() -> Result<(), ()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if matches!(neigh.state, NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) {
|
} else if matches!(neigh.state, NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) {
|
||||||
// Probe stale GUA so the kernel NUD state machine can confirm
|
match prober.send_icmpv6_echo(*addr, neigh.ifindex) {
|
||||||
// reachability; the event loop will add it to gua_keepalive on REACHABLE.
|
|
||||||
match send_icmpv6_echo(*addr, neigh.ifindex) {
|
|
||||||
Ok(()) => debug!("init dump: probing stale GUA {}", addr),
|
Ok(()) => debug!("init dump: probing stale GUA {}", addr),
|
||||||
Err(e) => debug!("init GUA probe failed for {}: {}", addr, e),
|
Err(e) => debug!("init GUA probe failed for {}: {}", addr, e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// GUA addresses are never published to DNS (unless --publish-gua)
|
|
||||||
if private_subnet_v6 {
|
if private_subnet_v6 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -283,95 +393,53 @@ async fn main() -> Result<(), ()> {
|
|||||||
let ip_str = inet_to_string(&neigh.inet);
|
let ip_str = inet_to_string(&neigh.inet);
|
||||||
match neigh.state {
|
match neigh.state {
|
||||||
NeighbourState::Reachable => {
|
NeighbourState::Reachable => {
|
||||||
// Confirmed online — register in DNS immediately.
|
|
||||||
if let Some(hostname) = leases.get(&neigh.mac) {
|
if let Some(hostname) = leases.get(&neigh.mac) {
|
||||||
let key = (hostname.clone(), ip_str.clone());
|
let key = (hostname.clone(), ip_str.clone());
|
||||||
if !registered.contains_key(&key) {
|
if !registered.contains_key(&key) {
|
||||||
// Enforce per-host ULA limit before adding a new one.
|
try_register_neigh(neigh, hostname, &ip_str, &mut registered, &leases, &updater, max_ula_per_host, private_subnet_v6).await;
|
||||||
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
|
|
||||||
if if_ipv6_in_private_subnet(addr) {
|
|
||||||
prune_ula_for_host(hostname, max_ula_per_host, &mut registered, &updater).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
_ => Ok(()),
|
|
||||||
};
|
|
||||||
match result {
|
|
||||||
Ok(()) => {
|
|
||||||
info!("DNS update: added {} -> {:?}", hostname, neigh.inet);
|
|
||||||
let ip = match &neigh.inet {
|
|
||||||
NeighbourAddress::Inet6(addr) => Some(IpAddr::V6(*addr)),
|
|
||||||
NeighbourAddress::Inet(addr) => Some(IpAddr::V4(*addr)),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
if let Some(ip) = ip {
|
|
||||||
if let Err(e) = updater.upsert_ptr(ip, hostname, DEFAULT_TTL).await {
|
|
||||||
warn!("PTR update failed for {}: {}", hostname, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
registered.insert(key, RegisteredEntry {
|
|
||||||
hostname: hostname.clone(),
|
|
||||||
last_confirmed: Instant::now(),
|
|
||||||
ifindex: neigh.ifindex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(e) => error!("DNS update failed for {}: {}", hostname, e),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debug!("no lease for mac {}, skipping DNS update", neigh.mac);
|
debug!("no lease for mac {}, skipping DNS update", neigh.mac);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe => {
|
NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe => {
|
||||||
// Uncertain — probe to trigger NUD verification. If the device
|
|
||||||
// is online, the kernel will emit a REACHABLE NewNeighbour event
|
|
||||||
// which the event loop will pick up and register in DNS.
|
|
||||||
match &neigh.inet {
|
match &neigh.inet {
|
||||||
NeighbourAddress::Inet6(addr) => {
|
NeighbourAddress::Inet6(addr) => {
|
||||||
match send_icmpv6_echo(*addr, neigh.ifindex) {
|
match prober.send_icmpv6_echo(*addr, neigh.ifindex) {
|
||||||
Ok(()) => debug!("init dump: probing stale neighbour {}", addr),
|
Ok(()) => debug!("init dump: probing stale neighbour {}", addr),
|
||||||
Err(e) => debug!("init probe failed for {}: {}", addr, e),
|
Err(e) => debug!("init probe failed for {}: {}", addr, e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NeighbourAddress::Inet(addr) => {
|
NeighbourAddress::Inet(addr) => {
|
||||||
if let Err(e) = send_icmpv4_echo(*addr, neigh.ifindex) {
|
if let Err(e) = prober.send_icmpv4_echo(*addr, neigh.ifindex) {
|
||||||
debug!("init probe failed for {}: {}", addr, e);
|
debug!("init probe failed for {}: {}", addr, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {}
|
||||||
// FAILED and others — skip.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// (netlink event socket already set up above — reuse the same connection)
|
|
||||||
|
|
||||||
// Start periodic probing task
|
|
||||||
let probe_interval = args.probe_interval;
|
let probe_interval = args.probe_interval;
|
||||||
let mut probe_timer = if probe_interval > 0 {
|
let mut probe_timer = if probe_interval > 0 {
|
||||||
time::interval(Duration::from_secs(probe_interval))
|
time::interval(Duration::from_secs(probe_interval))
|
||||||
} else {
|
} else {
|
||||||
// Create a very long interval that effectively disables probing
|
|
||||||
time::interval(Duration::from_secs(u64::MAX / 2))
|
time::interval(Duration::from_secs(u64::MAX / 2))
|
||||||
};
|
};
|
||||||
probe_timer.tick().await; // consume the immediate first tick
|
probe_timer.tick().await;
|
||||||
|
|
||||||
// GUA keepalive timer (separate cadence from DNS probe timer)
|
|
||||||
let mut gua_keepalive_timer = if keepalive_gua && keepalive_gua_interval > 0 {
|
let mut gua_keepalive_timer = if keepalive_gua && keepalive_gua_interval > 0 {
|
||||||
time::interval(Duration::from_secs(keepalive_gua_interval))
|
time::interval(Duration::from_secs(keepalive_gua_interval))
|
||||||
} else {
|
} else {
|
||||||
time::interval(Duration::from_secs(u64::MAX / 2))
|
time::interval(Duration::from_secs(u64::MAX / 2))
|
||||||
};
|
};
|
||||||
gua_keepalive_timer.tick().await; // consume the immediate first tick
|
gua_keepalive_timer.tick().await;
|
||||||
|
|
||||||
let mut lease_refresh_timer = time::interval(Duration::from_secs(60));
|
let mut lease_refresh_timer = time::interval(Duration::from_secs(60));
|
||||||
lease_refresh_timer.tick().await; // consume the immediate first tick
|
lease_refresh_timer.tick().await;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
@@ -379,7 +447,6 @@ async fn main() -> Result<(), ()> {
|
|||||||
let Some((message, _)) = msg_opt else {
|
let Some((message, _)) = msg_opt else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|
||||||
let payload = message.payload;
|
let payload = message.payload;
|
||||||
if let NetlinkPayload::InnerMessage(msg) = payload {
|
if let NetlinkPayload::InnerMessage(msg) = payload {
|
||||||
match msg {
|
match msg {
|
||||||
@@ -390,15 +457,14 @@ async fn main() -> Result<(), ()> {
|
|||||||
if should_skip_neigh(&neigh) {
|
if should_skip_neigh(&neigh) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Drop IPv6 events for addresses not in any active LAN prefix.
|
|
||||||
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
|
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
|
||||||
if !ipv6_passes_active_prefix(*addr, &active_prefixes) {
|
if !ipv6_passes_active_prefix(*addr, &active_prefixes) {
|
||||||
debug!("event: skipping {} — not in any active LAN prefix", addr);
|
debug!("event: skipping {} -- not in any active LAN prefix", addr);
|
||||||
let key = (neigh.mac.clone(), inet_to_string(&neigh.inet));
|
let key = (neigh.mac.clone(), inet_to_string(&neigh.inet));
|
||||||
if let Some(entry) = registered.remove(&key) {
|
if let Some(entry) = registered.remove(&key) {
|
||||||
do_delete_dns(&entry.hostname, &neigh.inet, &updater).await;
|
do_delete_dns(&entry.hostname, &neigh.inet, &updater).await;
|
||||||
}
|
}
|
||||||
// Also remove from GUA keepalive if present
|
|
||||||
if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) {
|
if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) {
|
||||||
entries.retain(|e| e.addr != *addr);
|
entries.retain(|e| e.addr != *addr);
|
||||||
}
|
}
|
||||||
@@ -409,7 +475,6 @@ async fn main() -> Result<(), ()> {
|
|||||||
// Route GUA addresses to keepalive map instead of DNS.
|
// Route GUA addresses to keepalive map instead of DNS.
|
||||||
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
|
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
|
||||||
if is_gua_ipv6(addr) && private_subnet_v6 {
|
if is_gua_ipv6(addr) && private_subnet_v6 {
|
||||||
// GUA not published to DNS — handle keepalive tracking only
|
|
||||||
if keepalive_gua {
|
if keepalive_gua {
|
||||||
if is_failed_state(neigh.state) {
|
if is_failed_state(neigh.state) {
|
||||||
if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) {
|
if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) {
|
||||||
@@ -435,7 +500,6 @@ async fn main() -> Result<(), ()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if matches!(neigh.state, NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) {
|
} else if matches!(neigh.state, NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) {
|
||||||
// Update ifindex only
|
|
||||||
if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) {
|
if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) {
|
||||||
if let Some(e) = entries.iter_mut().find(|e| e.addr == *addr) {
|
if let Some(e) = entries.iter_mut().find(|e| e.addr == *addr) {
|
||||||
e.ifindex = neigh.ifindex;
|
e.ifindex = neigh.ifindex;
|
||||||
@@ -448,20 +512,17 @@ async fn main() -> Result<(), ()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let ip_str = inet_to_string(&neigh.inet);
|
let ip_str = inet_to_string(&neigh.inet);
|
||||||
|
|
||||||
if is_failed_state(neigh.state) {
|
if is_failed_state(neigh.state) {
|
||||||
// Neighbour confirmed unreachable — remove DNS record.
|
|
||||||
// Resolve key by hostname (handles MAC changes: new MAC in leases
|
|
||||||
// can now evict entries registered under an old MAC).
|
|
||||||
// Fall back to scanning registered by IP if MAC is no longer in leases.
|
|
||||||
debug!("Neighbour failed: {:?}", neigh);
|
debug!("Neighbour failed: {:?}", neigh);
|
||||||
let key_opt: Option<(String, String)> = leases
|
let key_opt: Option<(String, String)> = leases
|
||||||
.get(&neigh.mac)
|
.get(&neigh.mac)
|
||||||
.map(|h| (h.clone(), ip_str.clone()))
|
.map(|h| (h.clone(), ip_str.clone()))
|
||||||
.or_else(|| registered.keys().find(|(_, ip)| ip == &ip_str).cloned());
|
.or_else(|| registered.keys().find(|(_, ip)| ip == &ip_str).cloned());
|
||||||
|
|
||||||
if let Some(key) = key_opt {
|
if let Some(key) = key_opt {
|
||||||
if let Some(entry) = registered.remove(&key) {
|
if let Some(entry) = registered.remove(&key) {
|
||||||
if !do_delete_dns(&entry.hostname, &neigh.inet, &updater).await {
|
if !do_delete_dns(&entry.hostname, &neigh.inet, &updater).await {
|
||||||
// DNS delete failed, put back so we retry
|
|
||||||
registered.insert(key, entry);
|
registered.insert(key, entry);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -474,34 +535,16 @@ async fn main() -> Result<(), ()> {
|
|||||||
if let Some(hostname) = leases.get(&neigh.mac) {
|
if let Some(hostname) = leases.get(&neigh.mac) {
|
||||||
let key = (hostname.clone(), ip_str.clone());
|
let key = (hostname.clone(), ip_str.clone());
|
||||||
if let Some(entry) = registered.get_mut(&key) {
|
if let Some(entry) = registered.get_mut(&key) {
|
||||||
// Already registered, just update timestamp and ifindex
|
|
||||||
entry.last_confirmed = Instant::now();
|
entry.last_confirmed = Instant::now();
|
||||||
entry.ifindex = neigh.ifindex;
|
entry.ifindex = neigh.ifindex;
|
||||||
} else {
|
} else {
|
||||||
// New reachable neighbour — register in DNS
|
|
||||||
debug!("New neighbour: {:?}", neigh);
|
debug!("New neighbour: {:?}", neigh);
|
||||||
// Enforce per-host ULA limit before adding.
|
try_register_neigh(&neigh, hostname, &ip_str, &mut registered, &leases, &updater, max_ula_per_host, private_subnet_v6).await;
|
||||||
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
|
|
||||||
if if_ipv6_in_private_subnet(addr) {
|
|
||||||
prune_ula_for_host(hostname, max_ula_per_host, &mut registered, &updater).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if process_new_neigh(&neigh, &updater, &leases, private_subnet_v6).await {
|
|
||||||
registered.insert(key, RegisteredEntry {
|
|
||||||
hostname: hostname.clone(),
|
|
||||||
last_confirmed: Instant::now(),
|
|
||||||
ifindex: neigh.ifindex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debug!("no lease for mac {}, skipping DNS update", neigh.mac);
|
debug!("no lease for mac {}, skipping DNS update", neigh.mac);
|
||||||
}
|
}
|
||||||
} else if matches!(neigh.state, NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) {
|
} else if matches!(neigh.state, NeighbourState::Stale | NeighbourState::Delay | NeighbourState::Probe) {
|
||||||
// Uncertain state: update ifindex only so the next probe uses
|
|
||||||
// the right interface, but do NOT refresh last_confirmed —
|
|
||||||
// STALE/DELAY/PROBE is not confirmed reachable and refreshing
|
|
||||||
// the timestamp would suppress the periodic probe.
|
|
||||||
if let Some(hostname) = leases.get(&neigh.mac) {
|
if let Some(hostname) = leases.get(&neigh.mac) {
|
||||||
let key = (hostname.clone(), ip_str.clone());
|
let key = (hostname.clone(), ip_str.clone());
|
||||||
if let Some(entry) = registered.get_mut(&key) {
|
if let Some(entry) = registered.get_mut(&key) {
|
||||||
@@ -517,7 +560,7 @@ async fn main() -> Result<(), ()> {
|
|||||||
if should_skip_neigh(&neigh) {
|
if should_skip_neigh(&neigh) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Remove from GUA keepalive if applicable
|
|
||||||
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
|
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
|
||||||
if is_gua_ipv6(addr) && keepalive_gua {
|
if is_gua_ipv6(addr) && keepalive_gua {
|
||||||
if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) {
|
if let Some(entries) = gua_keepalive.get_mut(&neigh.mac) {
|
||||||
@@ -529,12 +572,13 @@ async fn main() -> Result<(), ()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ip_str = inet_to_string(&neigh.inet);
|
let ip_str = inet_to_string(&neigh.inet);
|
||||||
// Resolve key by hostname; fall back to scanning by IP if MAC gone from leases.
|
|
||||||
let key_opt: Option<(String, String)> = leases
|
let key_opt: Option<(String, String)> = leases
|
||||||
.get(&neigh.mac)
|
.get(&neigh.mac)
|
||||||
.map(|h| (h.clone(), ip_str.clone()))
|
.map(|h| (h.clone(), ip_str.clone()))
|
||||||
.or_else(|| registered.keys().find(|(_, ip)| ip == &ip_str).cloned());
|
.or_else(|| registered.keys().find(|(_, ip)| ip == &ip_str).cloned());
|
||||||
|
|
||||||
debug!("Del neighbour: {:?}", neigh);
|
debug!("Del neighbour: {:?}", neigh);
|
||||||
if let Some(key) = key_opt {
|
if let Some(key) = key_opt {
|
||||||
registered.remove(&key);
|
registered.remove(&key);
|
||||||
@@ -549,7 +593,7 @@ async fn main() -> Result<(), ()> {
|
|||||||
if probe_interval == 0 {
|
if probe_interval == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
probe_registered_neighbours(®istered).await;
|
probe_registered_neighbours(&prober, ®istered).await;
|
||||||
reconcile_dns(
|
reconcile_dns(
|
||||||
&updater,
|
&updater,
|
||||||
&mut registered,
|
&mut registered,
|
||||||
@@ -557,14 +601,10 @@ async fn main() -> Result<(), ()> {
|
|||||||
&active_prefixes,
|
&active_prefixes,
|
||||||
private_subnet_v4,
|
private_subnet_v4,
|
||||||
private_subnet_v6,
|
private_subnet_v6,
|
||||||
|
&prober,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
// Recover kernel orphans: neighbours that are REACHABLE in the kernel
|
|
||||||
// but absent from `registered` and DNS. This happens when a netlink
|
|
||||||
// NewNeighbour event is lost (receive-buffer overflow) or when a device
|
|
||||||
// transitions to REACHABLE during the startup race window.
|
|
||||||
// reconcile_dns's AXFR pass cannot catch these because the record is
|
|
||||||
// also absent from DNS, so no probe is ever triggered for them.
|
|
||||||
if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet_v4).await {
|
if let Ok(neighbours) = dump_neighbours(handle.clone(), private_subnet_v4).await {
|
||||||
let all_dump_ips: std::collections::HashSet<String> = neighbours
|
let all_dump_ips: std::collections::HashSet<String> = neighbours
|
||||||
.iter()
|
.iter()
|
||||||
@@ -575,11 +615,7 @@ async fn main() -> Result<(), ()> {
|
|||||||
if should_skip_neigh(neigh) {
|
if should_skip_neigh(neigh) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_failed_state(neigh.state) {
|
if is_failed_state(neigh.state) {
|
||||||
// Kernel confirms FAILED — remove from registered + DNS if present.
|
|
||||||
// Mirrors the real-time NewNeighbour(FAILED) handler but catches
|
|
||||||
// cases where that event was lost.
|
|
||||||
let ip_str = inet_to_string(&neigh.inet);
|
let ip_str = inet_to_string(&neigh.inet);
|
||||||
let key_opt: Option<(String, String)> = leases
|
let key_opt: Option<(String, String)> = leases
|
||||||
.get(&neigh.mac)
|
.get(&neigh.mac)
|
||||||
@@ -600,7 +636,6 @@ async fn main() -> Result<(), ()> {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if neigh.state != NeighbourState::Reachable {
|
if neigh.state != NeighbourState::Reachable {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -618,43 +653,16 @@ async fn main() -> Result<(), ()> {
|
|||||||
let Some(hostname) = leases.get(&neigh.mac) else {
|
let Some(hostname) = leases.get(&neigh.mac) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let key = (hostname.clone(), ip_str);
|
let key = (hostname.clone(), ip_str.clone());
|
||||||
if let Some(entry) = registered.get_mut(&key) {
|
if let Some(entry) = registered.get_mut(&key) {
|
||||||
// Already tracked — refresh to suppress spurious probes.
|
|
||||||
entry.last_confirmed = Instant::now();
|
entry.last_confirmed = Instant::now();
|
||||||
entry.ifindex = neigh.ifindex;
|
entry.ifindex = neigh.ifindex;
|
||||||
} else {
|
} else {
|
||||||
debug!("reconcile neigh: kernel orphan {} -> {:?}", hostname, neigh.inet);
|
debug!("reconcile neigh: kernel orphan {} -> {:?}", hostname, neigh.inet);
|
||||||
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
|
try_register_neigh(neigh, hostname, &ip_str, &mut registered, &leases, &updater, max_ula_per_host, private_subnet_v6).await;
|
||||||
if if_ipv6_in_private_subnet(addr) {
|
|
||||||
prune_ula_for_host(
|
|
||||||
hostname,
|
|
||||||
max_ula_per_host,
|
|
||||||
&mut registered,
|
|
||||||
&updater,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if process_new_neigh(neigh, &updater, &leases, private_subnet_v6).await {
|
|
||||||
registered.insert(
|
|
||||||
key,
|
|
||||||
RegisteredEntry {
|
|
||||||
hostname: hostname.clone(),
|
|
||||||
last_confirmed: Instant::now(),
|
|
||||||
ifindex: neigh.ifindex,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sweep registered entries whose IP the kernel has evicted entirely
|
|
||||||
// (not present in the dump at all, not even as FAILED) AND that haven't
|
|
||||||
// been confirmed recently. These are devices whose FAILED event was
|
|
||||||
// lost and which the kernel has since garbage-collected from its table.
|
|
||||||
// Only act if the entry is old enough (> 2× probe interval) to avoid
|
|
||||||
// racing with normal STALE→REACHABLE transitions.
|
|
||||||
let stale_cutoff = Duration::from_secs(probe_interval.saturating_mul(2));
|
let stale_cutoff = Duration::from_secs(probe_interval.saturating_mul(2));
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let to_remove: Vec<(String, String)> = registered
|
let to_remove: Vec<(String, String)> = registered
|
||||||
@@ -688,7 +696,7 @@ async fn main() -> Result<(), ()> {
|
|||||||
if !keepalive_gua || keepalive_gua_interval == 0 {
|
if !keepalive_gua || keepalive_gua_interval == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
probe_gua_keepalive(&gua_keepalive, keepalive_gua_per_host);
|
probe_gua_keepalive(&prober, &gua_keepalive, keepalive_gua_per_host);
|
||||||
prune_gua_keepalive(&mut gua_keepalive, keepalive_gua_interval, keepalive_gua_per_host);
|
prune_gua_keepalive(&mut gua_keepalive, keepalive_gua_interval, keepalive_gua_per_host);
|
||||||
}
|
}
|
||||||
_ = lease_refresh_timer.tick() => {
|
_ = lease_refresh_timer.tick() => {
|
||||||
@@ -705,87 +713,3 @@ async fn main() -> Result<(), ()> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn format_mac(mac: Vec<u8>) -> String {
|
|
||||||
let mut mac_str = String::new();
|
|
||||||
for byte in mac {
|
|
||||||
mac_str.push_str(&format!("{:02x}:", byte));
|
|
||||||
}
|
|
||||||
mac_str.pop();
|
|
||||||
mac_str
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inet_to_string(addr: &NeighbourAddress) -> String {
|
|
||||||
match addr {
|
|
||||||
NeighbourAddress::Inet(ip) => ip.to_string(),
|
|
||||||
NeighbourAddress::Inet6(ip) => ip.to_string(),
|
|
||||||
other => format!("{other:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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_v4: bool) -> Option<Neigh> {
|
|
||||||
let state = neigh.header.state;
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
// IPv4 private-subnet filter stays here (no keepalive concept for IPv4 GUA)
|
|
||||||
if let NeighbourAddress::Inet(ipv4) = &addr {
|
|
||||||
if private_subnet_v4 && !if_ipv4_in_private_subnet(ipv4) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let kind = neigh.header.kind;
|
|
||||||
let ifindex = neigh.header.ifindex;
|
|
||||||
let mac_bytes = neigh.attributes.iter().find_map(|attr| match attr {
|
|
||||||
NeighbourAttribute::LinkLayerAddress(mac) => Some(mac.to_owned()),
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
// FAILED events may arrive without a hardware address (e.g. when ARP/NDP never
|
|
||||||
// succeeded, or the kernel cleared the lladdr on failure). Allow them through
|
|
||||||
// with an empty MAC so the FAILED handler can fall back to IP-based lookup.
|
|
||||||
let mac_str = match mac_bytes {
|
|
||||||
Some(m) => format_mac(m),
|
|
||||||
None if matches!(state, NeighbourState::Failed) => String::new(),
|
|
||||||
None => return None,
|
|
||||||
};
|
|
||||||
// Filter out empty or all-zero MACs for non-FAILED states
|
|
||||||
// (router own addresses, incomplete entries).
|
|
||||||
if !matches!(state, NeighbourState::Failed) && (mac_str.is_empty() || mac_str == "00:00:00:00:00:00") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Some(Neigh {
|
|
||||||
ifindex,
|
|
||||||
state,
|
|
||||||
kind,
|
|
||||||
inet: addr,
|
|
||||||
mac: mac_str,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn dump_neighbours(handle: Handle, private_subnet_v4: bool) -> Result<Vec<Neigh>, Error> {
|
|
||||||
let mut neighbours = handle.neighbours().get().execute();
|
|
||||||
let mut vec: Vec<Neigh> = Vec::new();
|
|
||||||
while let Some(route) = neighbours.try_next().await? {
|
|
||||||
if let Some(neigh) = parse_neighbour_message(route, private_subnet_v4) {
|
|
||||||
if !should_skip_neigh(&neigh) {
|
|
||||||
vec.push(neigh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(vec)
|
|
||||||
}
|
|
||||||
|
|
||||||
+15
-16
@@ -4,21 +4,21 @@ 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 = ubus::Connection::connect(&socket)?;
|
let mut connection = ubus::Connection::connect(&socket)?;
|
||||||
|
|
||||||
let json = connection.call(obj_path, method, "")?;
|
let json = connection.call(obj_path, method, "")?;
|
||||||
let parsed: Value = serde_json::from_str(&json)?;
|
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"
|
/// Decode a hex string (e.g. "44237cdcb75b") to raw bytes and format via
|
||||||
fn format_mac_from_hex(hex: &str) -> String {
|
/// the shared `types::format_mac`, producing "44:23:7c:dc:b7:5b".
|
||||||
hex.as_bytes()
|
fn hex_to_mac(hex: &str) -> String {
|
||||||
|
let bytes: Vec<u8> = hex
|
||||||
|
.as_bytes()
|
||||||
.chunks(2)
|
.chunks(2)
|
||||||
.filter_map(|chunk| std::str::from_utf8(chunk).ok())
|
.filter_map(|c| u8::from_str_radix(std::str::from_utf8(c).ok()?, 16).ok())
|
||||||
.collect::<Vec<&str>>()
|
.collect();
|
||||||
.join(":")
|
crate::types::format_mac(&bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sanitize a DHCP hostname into a valid DNS label.
|
/// Sanitize a DHCP hostname into a valid DNS label.
|
||||||
@@ -53,7 +53,9 @@ fn mac_from_duid(duid: &str) -> Option<String> {
|
|||||||
if !duid.chars().all(|c| c.is_ascii_hexdigit()) {
|
if !duid.chars().all(|c| c.is_ascii_hexdigit()) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let duid_type = u16::from_str_radix(duid.get(0..4)?, 16).ok()?;
|
let duid_type = u16::from_str_radix(duid.get(0..4)?, 16).ok()?;
|
||||||
|
|
||||||
// DUID-LLT (1): type(4) + hw_type(4) + time(8) = 16 hex chars before MAC
|
// DUID-LLT (1): type(4) + hw_type(4) + time(8) = 16 hex chars before MAC
|
||||||
// DUID-LL (3): type(4) + hw_type(4) = 8 hex chars before MAC
|
// DUID-LL (3): type(4) + hw_type(4) = 8 hex chars before MAC
|
||||||
let mac_offset = match duid_type {
|
let mac_offset = match duid_type {
|
||||||
@@ -61,14 +63,9 @@ fn mac_from_duid(duid: &str) -> Option<String> {
|
|||||||
3 => 8,
|
3 => 8,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mac_hex = duid.get(mac_offset..mac_offset + 12)?;
|
let mac_hex = duid.get(mac_offset..mac_offset + 12)?;
|
||||||
let mac = mac_hex
|
Some(hex_to_mac(mac_hex))
|
||||||
.as_bytes()
|
|
||||||
.chunks(2)
|
|
||||||
.map(|chunk| std::str::from_utf8(chunk).unwrap())
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join(":");
|
|
||||||
Some(mac)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// get mac to hostname mapping
|
// get mac to hostname mapping
|
||||||
@@ -77,14 +74,16 @@ pub fn get_lease() -> Result<HashMap<String, String>, Box<dyn std::error::Error>
|
|||||||
let leases = call_ubus("dhcp", "ipv4leases")?;
|
let leases = call_ubus("dhcp", "ipv4leases")?;
|
||||||
let devices = leases["device"].as_object()
|
let devices = leases["device"].as_object()
|
||||||
.ok_or("missing 'device' in ipv4leases response")?;
|
.ok_or("missing 'device' in ipv4leases response")?;
|
||||||
|
|
||||||
let mut result = HashMap::new();
|
let mut result = HashMap::new();
|
||||||
|
|
||||||
for (_device, leases) in devices {
|
for (_device, leases) in devices {
|
||||||
let leases = leases["leases"].as_array()
|
let leases = leases["leases"].as_array()
|
||||||
.ok_or("missing 'leases' array in device")?;
|
.ok_or("missing 'leases' array in device")?;
|
||||||
for lease in leases {
|
for lease in leases {
|
||||||
let Some(raw_mac) = lease["mac"].as_str() else { continue };
|
let Some(raw_mac) = lease["mac"].as_str() else { continue };
|
||||||
let Some(hostname) = lease["hostname"].as_str() else { continue };
|
let Some(hostname) = lease["hostname"].as_str() else { continue };
|
||||||
let mac = format_mac_from_hex(raw_mac);
|
let mac = hex_to_mac(raw_mac);
|
||||||
let hostname = sanitize_hostname(hostname);
|
let hostname = sanitize_hostname(hostname);
|
||||||
if hostname.is_empty() {
|
if hostname.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
+45
-34
@@ -9,14 +9,6 @@ use tokio::time::Duration;
|
|||||||
|
|
||||||
use crate::types::{GuaKeepaliveEntry, RegisteredEntry};
|
use crate::types::{GuaKeepaliveEntry, RegisteredEntry};
|
||||||
|
|
||||||
fn set_ipv6_unicast_if(socket: &Socket, ifindex: u32) -> std::io::Result<()> {
|
|
||||||
socket.bind_device_by_index_v6(NonZeroU32::new(ifindex))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_ip_unicast_if(socket: &Socket, ifindex: u32) -> std::io::Result<()> {
|
|
||||||
socket.bind_device_by_index_v4(NonZeroU32::new(ifindex))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_icmpv4_checksum(packet: &mut [u8]) {
|
fn compute_icmpv4_checksum(packet: &mut [u8]) {
|
||||||
packet[2] = 0;
|
packet[2] = 0;
|
||||||
packet[3] = 0;
|
packet[3] = 0;
|
||||||
@@ -32,36 +24,54 @@ fn compute_icmpv4_checksum(packet: &mut [u8]) {
|
|||||||
packet[2..4].copy_from_slice(&checksum.to_be_bytes());
|
packet[2..4].copy_from_slice(&checksum.to_be_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn send_icmpv6_echo(addr: Ipv6Addr, ifindex: u32) -> std::io::Result<()> {
|
/// Reusable ICMP probe sockets. Created once at startup and shared across all
|
||||||
let socket = Socket::new(Domain::IPV6, Type::RAW, Some(Protocol::ICMPV6))?;
|
/// probe call-sites, avoiding the overhead of creating and destroying a raw
|
||||||
socket.set_nonblocking(true)?;
|
/// socket for every single probe packet.
|
||||||
// Bind outgoing packet to the specific interface via IPV6_UNICAST_IF so the
|
pub(crate) struct Prober {
|
||||||
// kernel NUD state machine updates the correct neighbour entry.
|
v6: Socket,
|
||||||
set_ipv6_unicast_if(&socket, ifindex)?;
|
v4: Socket,
|
||||||
// ICMPv6 Echo Request: type=128, code=0, checksum=0 (kernel computes for RAW ICMPV6), id=0, seq=1
|
|
||||||
let packet: [u8; 8] = [128, 0, 0, 0, 0, 0, 0, 1];
|
|
||||||
let dest = SockAddr::from(std::net::SocketAddrV6::new(addr, 0, 0, 0));
|
|
||||||
socket.send_to(&packet, &dest)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn send_icmpv4_echo(addr: Ipv4Addr, ifindex: u32) -> std::io::Result<()> {
|
impl Prober {
|
||||||
let socket = Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))?;
|
pub fn new() -> std::io::Result<Self> {
|
||||||
socket.set_nonblocking(true)?;
|
let v6 = Socket::new(Domain::IPV6, Type::RAW, Some(Protocol::ICMPV6))?;
|
||||||
// Bind outgoing packet to the specific interface via IP_UNICAST_IF.
|
v6.set_nonblocking(true)?;
|
||||||
set_ip_unicast_if(&socket, ifindex)?;
|
|
||||||
// ICMPv4 Echo Request: type=8, code=0, checksum (computed), id=0, seq=1
|
let v4 = Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))?;
|
||||||
let mut packet: [u8; 8] = [8, 0, 0, 0, 0, 0, 0, 1];
|
v4.set_nonblocking(true)?;
|
||||||
compute_icmpv4_checksum(&mut packet);
|
|
||||||
let dest = SockAddr::from(std::net::SocketAddrV4::new(addr, 0));
|
Ok(Self { v6, v4 })
|
||||||
socket.send_to(&packet, &dest)?;
|
}
|
||||||
Ok(())
|
|
||||||
|
pub fn send_icmpv6_echo(&self, addr: Ipv6Addr, ifindex: u32) -> std::io::Result<()> {
|
||||||
|
// Re-bind outgoing interface before each send (lightweight setsockopt).
|
||||||
|
self.v6.bind_device_by_index_v6(NonZeroU32::new(ifindex))?;
|
||||||
|
|
||||||
|
// ICMPv6 Echo Request: type=128, code=0, checksum=0 (kernel computes), id=0, seq=1
|
||||||
|
let packet: [u8; 8] = [128, 0, 0, 0, 0, 0, 0, 1];
|
||||||
|
let dest = SockAddr::from(std::net::SocketAddrV6::new(addr, 0, 0, 0));
|
||||||
|
self.v6.send_to(&packet, &dest)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_icmpv4_echo(&self, addr: Ipv4Addr, ifindex: u32) -> std::io::Result<()> {
|
||||||
|
// Re-bind outgoing interface before each send (lightweight setsockopt).
|
||||||
|
self.v4.bind_device_by_index_v4(NonZeroU32::new(ifindex))?;
|
||||||
|
|
||||||
|
// ICMPv4 Echo Request: type=8, code=0, checksum (computed), id=0, seq=1
|
||||||
|
let mut packet: [u8; 8] = [8, 0, 0, 0, 0, 0, 0, 1];
|
||||||
|
compute_icmpv4_checksum(&mut packet);
|
||||||
|
let dest = SockAddr::from(std::net::SocketAddrV4::new(addr, 0));
|
||||||
|
self.v4.send_to(&packet, &dest)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send ICMPv6/ICMPv4 Echo Requests to all registered neighbours that haven't been
|
/// Send ICMPv6/ICMPv4 Echo Requests to all registered neighbours that haven't been
|
||||||
/// confirmed recently. This forces the kernel NUD state machine to verify reachability,
|
/// confirmed recently. This forces the kernel NUD state machine to verify reachability,
|
||||||
/// generating NewNeighbour events with the resulting state (Reachable or Failed).
|
/// generating NewNeighbour events with the resulting state (Reachable or Failed).
|
||||||
pub(crate) async fn probe_registered_neighbours(
|
pub(crate) async fn probe_registered_neighbours(
|
||||||
|
prober: &Prober,
|
||||||
registered: &HashMap<(String, String), RegisteredEntry>,
|
registered: &HashMap<(String, String), RegisteredEntry>,
|
||||||
) {
|
) {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
@@ -71,11 +81,11 @@ pub(crate) async fn probe_registered_neighbours(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Ok(addr) = ip_str.parse::<Ipv6Addr>() {
|
if let Ok(addr) = ip_str.parse::<Ipv6Addr>() {
|
||||||
if let Err(e) = send_icmpv6_echo(addr, entry.ifindex) {
|
if let Err(e) = prober.send_icmpv6_echo(addr, entry.ifindex) {
|
||||||
debug!("probe failed for {}: {}", ip_str, e);
|
debug!("probe failed for {}: {}", ip_str, e);
|
||||||
}
|
}
|
||||||
} else if let Ok(addr) = ip_str.parse::<Ipv4Addr>() {
|
} else if let Ok(addr) = ip_str.parse::<Ipv4Addr>() {
|
||||||
if let Err(e) = send_icmpv4_echo(addr, entry.ifindex) {
|
if let Err(e) = prober.send_icmpv4_echo(addr, entry.ifindex) {
|
||||||
debug!("probe failed for {}: {}", ip_str, e);
|
debug!("probe failed for {}: {}", ip_str, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,6 +96,7 @@ pub(crate) async fn probe_registered_neighbours(
|
|||||||
/// Only the top `per_host` addresses (by most-recently-seen) are probed per MAC.
|
/// Only the top `per_host` addresses (by most-recently-seen) are probed per MAC.
|
||||||
/// This does NOT publish any records to DNS.
|
/// This does NOT publish any records to DNS.
|
||||||
pub(crate) fn probe_gua_keepalive(
|
pub(crate) fn probe_gua_keepalive(
|
||||||
|
prober: &Prober,
|
||||||
gua_keepalive: &HashMap<String, Vec<GuaKeepaliveEntry>>,
|
gua_keepalive: &HashMap<String, Vec<GuaKeepaliveEntry>>,
|
||||||
per_host: usize,
|
per_host: usize,
|
||||||
) {
|
) {
|
||||||
@@ -101,7 +112,7 @@ pub(crate) fn probe_gua_keepalive(
|
|||||||
if now.duration_since(e.last_confirmed) < Duration::from_secs(30) {
|
if now.duration_since(e.last_confirmed) < Duration::from_secs(30) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match send_icmpv6_echo(e.addr, e.ifindex) {
|
match prober.send_icmpv6_echo(e.addr, e.ifindex) {
|
||||||
Ok(()) => debug!("GUA keepalive probe: {} ({}) -> {}", e.hostname, mac, e.addr),
|
Ok(()) => debug!("GUA keepalive probe: {} ({}) -> {}", e.hostname, mac, e.addr),
|
||||||
Err(err) => debug!("GUA keepalive probe failed for {}: {}", e.addr, err),
|
Err(err) => debug!("GUA keepalive probe failed for {}: {}", e.addr, err),
|
||||||
}
|
}
|
||||||
@@ -109,7 +120,7 @@ pub(crate) fn probe_gua_keepalive(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove GUA keepalive entries that haven't been confirmed REACHABLE within 3× the
|
/// Remove GUA keepalive entries that haven't been confirmed REACHABLE within 3x the
|
||||||
/// keepalive interval, and drop excess entries beyond `per_host` (oldest first).
|
/// keepalive interval, and drop excess entries beyond `per_host` (oldest first).
|
||||||
pub(crate) fn prune_gua_keepalive(
|
pub(crate) fn prune_gua_keepalive(
|
||||||
gua_keepalive: &mut HashMap<String, Vec<GuaKeepaliveEntry>>,
|
gua_keepalive: &mut HashMap<String, Vec<GuaKeepaliveEntry>>,
|
||||||
|
|||||||
+22
-22
@@ -7,7 +7,7 @@ use netlink_packet_route::neighbour::NeighbourAddress;
|
|||||||
|
|
||||||
use crate::db::DnsUpdater;
|
use crate::db::DnsUpdater;
|
||||||
use crate::filter::{if_ipv4_in_private_subnet, if_ipv6_in_private_subnet, ipv6_in_prefix, is_gua_ipv6};
|
use crate::filter::{if_ipv4_in_private_subnet, if_ipv6_in_private_subnet, ipv6_in_prefix, is_gua_ipv6};
|
||||||
use crate::probe::{send_icmpv4_echo, send_icmpv6_echo};
|
use crate::probe::Prober;
|
||||||
use crate::types::{LanPrefix, Neigh, RegisteredEntry, DEFAULT_TTL};
|
use crate::types::{LanPrefix, Neigh, RegisteredEntry, DEFAULT_TTL};
|
||||||
|
|
||||||
pub(crate) async fn process_new_neigh(
|
pub(crate) async fn process_new_neigh(
|
||||||
@@ -20,6 +20,7 @@ pub(crate) async fn process_new_neigh(
|
|||||||
debug!("no lease for mac {}, skipping DNS update", neigh.mac);
|
debug!("no lease for mac {}, skipping DNS update", neigh.mac);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Guard: never publish GUA to DNS when private_subnet_v6 is set.
|
// Guard: never publish GUA to DNS when private_subnet_v6 is set.
|
||||||
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
|
if let NeighbourAddress::Inet6(addr) = &neigh.inet {
|
||||||
if private_subnet_v6 && is_gua_ipv6(addr) {
|
if private_subnet_v6 && is_gua_ipv6(addr) {
|
||||||
@@ -27,11 +28,13 @@ pub(crate) async fn process_new_neigh(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 false,
|
_ => return false,
|
||||||
};
|
};
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
info!("DNS update: added {} -> {:?}", hostname, neigh.inet);
|
info!("DNS update: added {} -> {:?}", hostname, neigh.inet);
|
||||||
@@ -63,6 +66,7 @@ pub(crate) async fn do_delete_dns(
|
|||||||
NeighbourAddress::Inet(addr) => updater.delete_a(hostname, *addr).await,
|
NeighbourAddress::Inet(addr) => updater.delete_a(hostname, *addr).await,
|
||||||
_ => return true,
|
_ => return true,
|
||||||
};
|
};
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
info!("DNS update: removed {} -> {:?}", hostname, inet);
|
info!("DNS update: removed {} -> {:?}", hostname, inet);
|
||||||
@@ -101,42 +105,40 @@ pub(crate) async fn prune_ula_for_host(
|
|||||||
.map(|(k, e)| (k.clone(), e.last_confirmed))
|
.map(|(k, e)| (k.clone(), e.last_confirmed))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if ula_keys.len() < max {
|
let initial_count = ula_keys.len();
|
||||||
return ula_keys.len();
|
if initial_count < max {
|
||||||
|
return initial_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort oldest first (by last_confirmed asc).
|
// Sort oldest first (by last_confirmed asc).
|
||||||
ula_keys.sort_by_key(|(_, ts)| *ts);
|
ula_keys.sort_by_key(|(_, ts)| *ts);
|
||||||
|
|
||||||
// Remove excess (keep the newest `max - 1` so there's room for the new one).
|
// Remove excess (keep the newest `max - 1` so there's room for the new one).
|
||||||
let to_remove = ula_keys.len() - (max.saturating_sub(1));
|
let to_remove = initial_count - (max.saturating_sub(1));
|
||||||
|
let mut actually_removed = 0;
|
||||||
for (key, _) in ula_keys.into_iter().take(to_remove) {
|
for (key, _) in ula_keys.into_iter().take(to_remove) {
|
||||||
if let Some(entry) = registered.remove(&key) {
|
if let Some(entry) = registered.remove(&key) {
|
||||||
let addr: Ipv6Addr = key.1.parse().unwrap();
|
let addr: Ipv6Addr = key.1.parse().unwrap();
|
||||||
let inet = NeighbourAddress::Inet6(addr);
|
let inet = NeighbourAddress::Inet6(addr);
|
||||||
do_delete_dns(&entry.hostname, &inet, updater).await;
|
do_delete_dns(&entry.hostname, &inet, updater).await;
|
||||||
info!("ULA pruning: removed oldest {} -> {} for host {}", entry.hostname, key.1, host);
|
info!("ULA pruning: removed oldest {} -> {} for host {}", entry.hostname, key.1, host);
|
||||||
|
actually_removed += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return count after pruning.
|
// Return count after pruning (computed, no second scan needed).
|
||||||
registered
|
initial_count - actually_removed
|
||||||
.iter()
|
|
||||||
.filter(|((h, ip_str), _)| {
|
|
||||||
h == host && ip_str.parse::<Ipv6Addr>().map_or(false, |a| if_ipv6_in_private_subnet(&a))
|
|
||||||
})
|
|
||||||
.count()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reconcile the in-memory `registered` map against the live DNS zone obtained via AXFR.
|
/// Reconcile the in-memory `registered` map against the live DNS zone obtained via AXFR.
|
||||||
///
|
///
|
||||||
/// Two corrections are made on each call:
|
/// Two corrections are made on each call:
|
||||||
/// 1. **DNS orphans** – records present in DNS but absent from `registered`.
|
/// 1. **DNS orphans** -- records present in DNS but absent from `registered`.
|
||||||
/// These are either stale leftovers from a previous run or records from a prefix that
|
/// These are either stale leftovers from a previous run or records from a prefix that
|
||||||
/// is no longer active. Records that fail prefix/subnet filtering are deleted from DNS
|
/// is no longer active. Records that fail prefix/subnet filtering are deleted from DNS
|
||||||
/// immediately; records that pass filtering are probed with ICMP so the kernel NUD
|
/// immediately; records that pass filtering are probed with ICMP so the kernel NUD
|
||||||
/// state machine can confirm reachability and re-populate `registered` via the event loop.
|
/// state machine can confirm reachability and re-populate `registered` via the event loop.
|
||||||
/// 2. **Registered orphans** – entries in `registered` that are missing from DNS (e.g.
|
/// 2. **Registered orphans** -- entries in `registered` that are missing from DNS (e.g.
|
||||||
/// because hickory-dns restarted and lost its in-memory state). These are re-pushed
|
/// because hickory-dns restarted and lost its in-memory state). These are re-pushed
|
||||||
/// via DNS UPDATE so the zone stays consistent.
|
/// via DNS UPDATE so the zone stays consistent.
|
||||||
pub(crate) async fn reconcile_dns(
|
pub(crate) async fn reconcile_dns(
|
||||||
@@ -146,6 +148,7 @@ pub(crate) async fn reconcile_dns(
|
|||||||
active_prefixes: &[LanPrefix],
|
active_prefixes: &[LanPrefix],
|
||||||
private_subnet_v4: bool,
|
private_subnet_v4: bool,
|
||||||
private_subnet_v6: bool,
|
private_subnet_v6: bool,
|
||||||
|
prober: &Prober,
|
||||||
) {
|
) {
|
||||||
use std::collections::{HashMap as Map, HashSet};
|
use std::collections::{HashMap as Map, HashSet};
|
||||||
|
|
||||||
@@ -182,7 +185,7 @@ pub(crate) async fn reconcile_dns(
|
|||||||
if passes {
|
if passes {
|
||||||
dns_ips.insert(ip.to_string(), hostname.clone());
|
dns_ips.insert(ip.to_string(), hostname.clone());
|
||||||
} else {
|
} else {
|
||||||
// Stale record (wrong prefix / subnet) — remove from DNS.
|
// Stale record (wrong prefix / subnet) -- remove from DNS.
|
||||||
let result = match ip {
|
let result = match ip {
|
||||||
IpAddr::V6(addr) => updater.delete_aaaa(hostname, *addr).await,
|
IpAddr::V6(addr) => updater.delete_aaaa(hostname, *addr).await,
|
||||||
IpAddr::V4(addr) => updater.delete_a(hostname, *addr).await,
|
IpAddr::V4(addr) => updater.delete_a(hostname, *addr).await,
|
||||||
@@ -204,15 +207,12 @@ pub(crate) async fn reconcile_dns(
|
|||||||
registered.keys().map(|(_, ip)| ip.as_str()).collect();
|
registered.keys().map(|(_, ip)| ip.as_str()).collect();
|
||||||
|
|
||||||
// --- DNS orphans (in DNS but not in registered) ---
|
// --- DNS orphans (in DNS but not in registered) ---
|
||||||
// Delete the orphan immediately — DNS TTL only controls resolver caches, not
|
|
||||||
// authoritative record lifetime, so records do NOT auto-expire.
|
|
||||||
// After deletion we probe the address: if the device is still alive the kernel
|
|
||||||
// will emit a REACHABLE NewNeighbour event which re-registers it in DNS.
|
|
||||||
for (ip_str, hostname) in &dns_ips {
|
for (ip_str, hostname) in &dns_ips {
|
||||||
if registered_ips.contains(ip_str.as_str()) {
|
if registered_ips.contains(ip_str.as_str()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
info!("reconcile: DNS orphan {} -> {}, deleting and probing", hostname, ip_str);
|
info!("reconcile: DNS orphan {} -> {}, deleting and probing", hostname, ip_str);
|
||||||
|
|
||||||
let del_result = match ip_str.parse::<IpAddr>() {
|
let del_result = match ip_str.parse::<IpAddr>() {
|
||||||
Ok(IpAddr::V6(addr)) => updater.delete_aaaa(hostname, addr).await,
|
Ok(IpAddr::V6(addr)) => updater.delete_aaaa(hostname, addr).await,
|
||||||
Ok(IpAddr::V4(addr)) => updater.delete_a(hostname, addr).await,
|
Ok(IpAddr::V4(addr)) => updater.delete_a(hostname, addr).await,
|
||||||
@@ -229,15 +229,16 @@ pub(crate) async fn reconcile_dns(
|
|||||||
}
|
}
|
||||||
Err(e) => warn!("reconcile: failed to delete orphan {} {}: {}", hostname, ip_str, e),
|
Err(e) => warn!("reconcile: failed to delete orphan {} {}: {}", hostname, ip_str, e),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Probe so alive devices trigger a REACHABLE event and re-register.
|
// Probe so alive devices trigger a REACHABLE event and re-register.
|
||||||
match ip_str.parse::<IpAddr>() {
|
match ip_str.parse::<IpAddr>() {
|
||||||
Ok(IpAddr::V6(addr)) => {
|
Ok(IpAddr::V6(addr)) => {
|
||||||
if let Err(e) = send_icmpv6_echo(addr, 0) {
|
if let Err(e) = prober.send_icmpv6_echo(addr, 0) {
|
||||||
debug!("reconcile: probe failed for {}: {}", addr, e);
|
debug!("reconcile: probe failed for {}: {}", addr, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(IpAddr::V4(addr)) => {
|
Ok(IpAddr::V4(addr)) => {
|
||||||
if let Err(e) = send_icmpv4_echo(addr, 0) {
|
if let Err(e) = prober.send_icmpv4_echo(addr, 0) {
|
||||||
debug!("reconcile: probe failed for {}: {}", addr, e);
|
debug!("reconcile: probe failed for {}: {}", addr, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,14 +247,13 @@ pub(crate) async fn reconcile_dns(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Registered orphans (in registered but not in DNS) ---
|
// --- Registered orphans (in registered but not in DNS) ---
|
||||||
// hickory-dns may have restarted and lost its journal; re-push the record.
|
|
||||||
// Use the hostname stored in the entry — independent of the current lease table.
|
|
||||||
for ((_, ip_str), entry) in registered.iter_mut() {
|
for ((_, ip_str), entry) in registered.iter_mut() {
|
||||||
if dns_ips.contains_key(ip_str.as_str()) {
|
if dns_ips.contains_key(ip_str.as_str()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let hostname = &entry.hostname;
|
let hostname = &entry.hostname;
|
||||||
debug!("reconcile: registered orphan {} -> {}, re-pushing", hostname, ip_str);
|
debug!("reconcile: registered orphan {} -> {}, re-pushing", hostname, ip_str);
|
||||||
|
|
||||||
let result = match ip_str.parse::<IpAddr>() {
|
let result = match ip_str.parse::<IpAddr>() {
|
||||||
Ok(IpAddr::V6(addr)) => updater.upsert_aaaa(hostname, addr, DEFAULT_TTL).await,
|
Ok(IpAddr::V6(addr)) => updater.upsert_aaaa(hostname, addr, DEFAULT_TTL).await,
|
||||||
Ok(IpAddr::V4(addr)) => updater.upsert_a(hostname, addr, DEFAULT_TTL).await,
|
Ok(IpAddr::V4(addr)) => updater.upsert_a(hostname, addr, DEFAULT_TTL).await,
|
||||||
|
|||||||
+13
-2
@@ -1,6 +1,6 @@
|
|||||||
|
use std::fmt::Write;
|
||||||
use std::net::Ipv6Addr;
|
use std::net::Ipv6Addr;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use netlink_packet_route::neighbour::{NeighbourAddress, NeighbourState};
|
use netlink_packet_route::neighbour::{NeighbourAddress, NeighbourState};
|
||||||
use netlink_packet_route::route::RouteType;
|
use netlink_packet_route::route::RouteType;
|
||||||
|
|
||||||
@@ -14,6 +14,17 @@ pub(crate) const fn nl_mgrp(group: u32) -> u32 {
|
|||||||
if group == 0 { 0 } else { 1 << (group - 1) }
|
if group == 0 { 0 } else { 1 << (group - 1) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format raw MAC bytes as colon-separated hex string, e.g. "44:23:7c:dc:b7:5b".
|
||||||
|
/// Shared across modules to avoid duplicate formatting logic.
|
||||||
|
pub(crate) fn format_mac(mac: &[u8]) -> String {
|
||||||
|
let mut s = String::with_capacity(mac.len() * 3);
|
||||||
|
for (i, byte) in mac.iter().enumerate() {
|
||||||
|
if i > 0 { s.push(':'); }
|
||||||
|
write!(s, "{:02x}", byte).unwrap();
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct Neigh {
|
pub(crate) struct Neigh {
|
||||||
pub(crate) ifindex: u32,
|
pub(crate) ifindex: u32,
|
||||||
@@ -41,7 +52,7 @@ pub(crate) struct GuaKeepaliveEntry {
|
|||||||
pub(crate) hostname: String,
|
pub(crate) hostname: String,
|
||||||
pub(crate) addr: Ipv6Addr,
|
pub(crate) addr: Ipv6Addr,
|
||||||
pub(crate) ifindex: u32,
|
pub(crate) ifindex: u32,
|
||||||
/// When this GUA was first observed — used to select the "newest" address per host.
|
/// When this GUA was first observed -- used to select the "newest" address per host.
|
||||||
pub(crate) first_seen: Instant,
|
pub(crate) first_seen: Instant,
|
||||||
/// Last time this address was confirmed REACHABLE by the kernel.
|
/// Last time this address was confirmed REACHABLE by the kernel.
|
||||||
pub(crate) last_confirmed: Instant,
|
pub(crate) last_confirmed: Instant,
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ const inbound_type = [
|
|||||||
['shadowsocks', _('Shadowsocks') + ' - ' + _('TCP/UDP')],
|
['shadowsocks', _('Shadowsocks') + ' - ' + _('TCP/UDP')],
|
||||||
['mieru', _('Mieru') + ' - ' + _('TCP/UDP')],
|
['mieru', _('Mieru') + ' - ' + _('TCP/UDP')],
|
||||||
['sudoku', _('Sudoku') + ' - ' + _('TCP')],
|
['sudoku', _('Sudoku') + ' - ' + _('TCP')],
|
||||||
|
['snell', _('Snell') + ' - ' + _('TCP')],
|
||||||
['vmess', _('VMess') + ' - ' + _('TCP')],
|
['vmess', _('VMess') + ' - ' + _('TCP')],
|
||||||
['vless', _('VLESS') + ' - ' + _('TCP')],
|
['vless', _('VLESS') + ' - ' + _('TCP')],
|
||||||
['trojan', _('Trojan') + ' - ' + _('TCP')],
|
['trojan', _('Trojan') + ' - ' + _('TCP')],
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ function renderListeners(s, uciconfig, isClient) {
|
|||||||
o = s.taboption('field_general', form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
|
o = s.taboption('field_general', form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
|
||||||
o.value('', _('Disable'));
|
o.value('', _('Disable'));
|
||||||
o.value('salamander', _('Salamander'));
|
o.value('salamander', _('Salamander'));
|
||||||
|
o.value('gecko', _('Gecko'));
|
||||||
o.depends('type', 'hysteria2');
|
o.depends('type', 'hysteria2');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
@@ -204,6 +205,16 @@ function renderListeners(s, uciconfig, isClient) {
|
|||||||
o.depends({type: 'hysteria2', hysteria_obfs_type: /.+/});
|
o.depends({type: 'hysteria2', hysteria_obfs_type: /.+/});
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
|
o = s.taboption('field_general', form.Value, 'hysteria_obfs_min_packet_size', _('Obfuscate minimum packet size'));
|
||||||
|
o.placeholder = '512'
|
||||||
|
o.depends('hysteria_obfs_type', 'gecko');
|
||||||
|
o.modalonly = true;
|
||||||
|
|
||||||
|
o = s.taboption('field_general', form.Value, 'hysteria_obfs_max_packet_size', _('Obfuscate maximum packet size'));
|
||||||
|
o.placeholder = '1200'
|
||||||
|
o.depends('hysteria_obfs_type', 'gecko');
|
||||||
|
o.modalonly = true;
|
||||||
|
|
||||||
o = s.taboption('field_general', form.Value, 'hysteria_masquerade', _('Masquerade'),
|
o = s.taboption('field_general', form.Value, 'hysteria_masquerade', _('Masquerade'),
|
||||||
_('HTTP3 server behavior when authentication fails.<br/>A 404 page will be returned if empty.'));
|
_('HTTP3 server behavior when authentication fails.<br/>A 404 page will be returned if empty.'));
|
||||||
o.placeholder = 'file:///var/www or http://127.0.0.1:8080'
|
o.placeholder = 'file:///var/www or http://127.0.0.1:8080'
|
||||||
@@ -440,6 +451,24 @@ function renderListeners(s, uciconfig, isClient) {
|
|||||||
o.depends('sudoku_http_mask', '1');
|
o.depends('sudoku_http_mask', '1');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
|
/* Snell fields */
|
||||||
|
o = s.taboption('field_general', hm.GenValue, 'snell_psk', _('Pre-shared key'));
|
||||||
|
o.password = true;
|
||||||
|
o.rmempty = false;
|
||||||
|
o.validate = hm.validateAuthPassword;
|
||||||
|
o.depends('type', 'snell');
|
||||||
|
o.modalonly = true;
|
||||||
|
|
||||||
|
o = s.taboption('field_general', form.ListValue, 'snell_version', _('Version'));
|
||||||
|
o.value('1', _('v1'));
|
||||||
|
o.value('2', _('v2'));
|
||||||
|
o.value('3', _('v3'));
|
||||||
|
o.value('4', _('v4'));
|
||||||
|
o.value('5', _('v5'));
|
||||||
|
o.default = '4';
|
||||||
|
o.depends('type', 'snell');
|
||||||
|
o.modalonly = true;
|
||||||
|
|
||||||
/* Tuic fields */
|
/* Tuic fields */
|
||||||
o = s.taboption('field_general', hm.GenValue, 'uuid', _('UUID'));
|
o = s.taboption('field_general', hm.GenValue, 'uuid', _('UUID'));
|
||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
@@ -539,6 +568,14 @@ function renderListeners(s, uciconfig, isClient) {
|
|||||||
o.value('http', _('HTTP'));
|
o.value('http', _('HTTP'));
|
||||||
o.value('tls', _('TLS'));
|
o.value('tls', _('TLS'));
|
||||||
o.depends('plugin', 'obfs');
|
o.depends('plugin', 'obfs');
|
||||||
|
o.depends('type', 'snell');
|
||||||
|
o.modalonly = true;
|
||||||
|
|
||||||
|
o = s.taboption('field_general', form.Value, 'plugin_opts_host', _('Plugin: ') + _('Host that supports TLS 1.3'));
|
||||||
|
o.datatype = 'hostname';
|
||||||
|
o.placeholder = 'cloud.tencent.com';
|
||||||
|
o.rmempty = false;
|
||||||
|
o.depends('type', 'snell');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
o = s.taboption('field_general', form.Value, 'plugin_opts_handshake_dest', _('Plugin: ') + _('Handshake target that supports TLS 1.3'));
|
o = s.taboption('field_general', form.Value, 'plugin_opts_handshake_dest', _('Plugin: ') + _('Handshake target that supports TLS 1.3'));
|
||||||
@@ -605,8 +642,8 @@ function renderListeners(s, uciconfig, isClient) {
|
|||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
o = s.taboption('field_general', form.Flag, 'udp', _('UDP'));
|
o = s.taboption('field_general', form.Flag, 'udp', _('UDP'));
|
||||||
o.default = o.disabled;
|
o.default = o.enabled;
|
||||||
o.depends({type: /^(socks|mixed|shadowsocks)$/});
|
o.depends({type: /^(socks|mixed|shadowsocks|snell)$/});
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
/* Vless Encryption fields */
|
/* Vless Encryption fields */
|
||||||
@@ -855,6 +892,12 @@ function renderListeners(s, uciconfig, isClient) {
|
|||||||
// @ 下面支持填写针对server-url的TLS配置(sni, skip-cert-verify, fingerprint, certificate, private-key, alpn)
|
// @ 下面支持填写针对server-url的TLS配置(sni, skip-cert-verify, fingerprint, certificate, private-key, alpn)
|
||||||
|
|
||||||
/* TLS fields */
|
/* TLS fields */
|
||||||
|
o = s.taboption('field_general', form.Flag, 'allow_insecure', _('Allow insecure connections'),
|
||||||
|
_('Only applicable when %s are used as a frontend.').format('nginx/caddy'));
|
||||||
|
o.default = o.disabled;
|
||||||
|
o.depends({type: /^(vless|trojan|anytls)$/});
|
||||||
|
o.modalonly = true;
|
||||||
|
|
||||||
o = s.taboption('field_general', form.Flag, 'tls', _('TLS'));
|
o = s.taboption('field_general', form.Flag, 'tls', _('TLS'));
|
||||||
o.default = o.disabled;
|
o.default = o.disabled;
|
||||||
o.validate = function(section_id, value) {
|
o.validate = function(section_id, value) {
|
||||||
@@ -906,7 +949,7 @@ function renderListeners(s, uciconfig, isClient) {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
o.depends({type: /^(http|socks|mixed|vmess|vless|trojan|anytls|tuic|hysteria2|hysteria2-realm|trusttunnel)$/});
|
o.depends({allow_insecure: '0', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|tuic|hysteria2|hysteria2-realm|trusttunnel)$/});
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
o = s.taboption('field_tls', form.DynamicList, 'tls_alpn', _('TLS ALPN'),
|
o = s.taboption('field_tls', form.DynamicList, 'tls_alpn', _('TLS ALPN'),
|
||||||
|
|||||||
@@ -298,6 +298,7 @@ return view.extend({
|
|||||||
so = ss.taboption('field_general', form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
|
so = ss.taboption('field_general', form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
|
||||||
so.value('', _('Disable'));
|
so.value('', _('Disable'));
|
||||||
so.value('salamander', _('Salamander'));
|
so.value('salamander', _('Salamander'));
|
||||||
|
so.value('gecko', _('Gecko'));
|
||||||
so.depends('type', 'hysteria2');
|
so.depends('type', 'hysteria2');
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
@@ -309,6 +310,16 @@ return view.extend({
|
|||||||
so.depends({type: 'hysteria2', hysteria_obfs_type: /.+/});
|
so.depends({type: 'hysteria2', hysteria_obfs_type: /.+/});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
|
so = ss.taboption('field_general', form.Value, 'hysteria_obfs_min_packet_size', _('Obfuscate minimum packet size'));
|
||||||
|
so.placeholder = '512'
|
||||||
|
so.depends('hysteria_obfs_type', 'gecko');
|
||||||
|
so.modalonly = true;
|
||||||
|
|
||||||
|
so = ss.taboption('field_general', form.Value, 'hysteria_obfs_max_packet_size', _('Obfuscate maximum packet size'));
|
||||||
|
so.placeholder = '1200'
|
||||||
|
so.depends('hysteria_obfs_type', 'gecko');
|
||||||
|
so.modalonly = true;
|
||||||
|
|
||||||
/* SSH fields */
|
/* SSH fields */
|
||||||
so = ss.taboption('field_general', form.TextValue, 'ssh_priv_key', _('Priv-key'));
|
so = ss.taboption('field_general', form.TextValue, 'ssh_priv_key', _('Priv-key'));
|
||||||
so.depends('type', 'ssh');
|
so.depends('type', 'ssh');
|
||||||
@@ -508,10 +519,17 @@ return view.extend({
|
|||||||
so.value('1', _('v1'));
|
so.value('1', _('v1'));
|
||||||
so.value('2', _('v2'));
|
so.value('2', _('v2'));
|
||||||
so.value('3', _('v3'));
|
so.value('3', _('v3'));
|
||||||
so.default = '3';
|
so.value('4', _('v4'));
|
||||||
|
so.value('5', _('v5'));
|
||||||
|
so.default = '4';
|
||||||
so.depends('type', 'snell');
|
so.depends('type', 'snell');
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
|
so = ss.taboption('field_general', form.Flag, 'snell_reuse', _('Connection reuse'));
|
||||||
|
so.default = so.disabled;
|
||||||
|
so.depends({type: 'snell', snell_version: /^(4|5)$/});
|
||||||
|
so.modalonly = true;
|
||||||
|
|
||||||
/* TUIC fields */
|
/* TUIC fields */
|
||||||
so = ss.taboption('field_general', form.Value, 'uuid', _('UUID'));
|
so = ss.taboption('field_general', form.Value, 'uuid', _('UUID'));
|
||||||
so.rmempty = false;
|
so.rmempty = false;
|
||||||
@@ -875,6 +893,7 @@ return view.extend({
|
|||||||
so = ss.taboption('field_general', form.Flag, 'udp', _('UDP'));
|
so = ss.taboption('field_general', form.Flag, 'udp', _('UDP'));
|
||||||
so.default = so.disabled;
|
so.default = so.disabled;
|
||||||
so.depends({type: /^(direct|socks5|ss|mieru|vmess|vless|trojan|anytls|trusttunnel|masque|wireguard)$/});
|
so.depends({type: /^(direct|socks5|ss|mieru|vmess|vless|trojan|anytls|trusttunnel|masque|wireguard)$/});
|
||||||
|
so.depends({type: 'snell', snell_version: /^(3|4|5)$/});
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
|
|
||||||
so = ss.taboption('field_general', form.Flag, 'uot', _('UoT'),
|
so = ss.taboption('field_general', form.Flag, 'uot', _('UoT'),
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -511,6 +511,8 @@ uci.foreach(uciconf, ucinode, (cfg) => {
|
|||||||
down: cfg.hysteria_down_mbps ? cfg.hysteria_down_mbps + ' Mbps' : null,
|
down: cfg.hysteria_down_mbps ? cfg.hysteria_down_mbps + ' Mbps' : null,
|
||||||
obfs: cfg.hysteria_obfs_type,
|
obfs: cfg.hysteria_obfs_type,
|
||||||
"obfs-password": cfg.hysteria_obfs_password,
|
"obfs-password": cfg.hysteria_obfs_password,
|
||||||
|
"obfs-min-packet-size": strToInt(cfg.hysteria_obfs_min_packet_size),
|
||||||
|
"obfs-max-packet-size": strToInt(cfg.hysteria_obfs_max_packet_size),
|
||||||
"realm-opts": cfg.hysteria2_realm === '1' ? {
|
"realm-opts": cfg.hysteria2_realm === '1' ? {
|
||||||
enable: true,
|
enable: true,
|
||||||
"server-url": cfg.hysteria2_realm_server_url,
|
"server-url": cfg.hysteria2_realm_server_url,
|
||||||
@@ -562,6 +564,7 @@ uci.foreach(uciconf, ucinode, (cfg) => {
|
|||||||
/* Snell */
|
/* Snell */
|
||||||
psk: cfg.snell_psk,
|
psk: cfg.snell_psk,
|
||||||
version: cfg.snell_version,
|
version: cfg.snell_version,
|
||||||
|
reuse: strToBool(cfg.snell_reuse),
|
||||||
"obfs-opts": cfg.type === 'snell' ? {
|
"obfs-opts": cfg.type === 'snell' ? {
|
||||||
mode: cfg.plugin_opts_obfsmode,
|
mode: cfg.plugin_opts_obfsmode,
|
||||||
host: cfg.plugin_opts_host,
|
host: cfg.plugin_opts_host,
|
||||||
|
|||||||
@@ -250,6 +250,8 @@ export function parseListener(cfg, isClient, label) {
|
|||||||
"ignore-client-bandwidth": strToBool(cfg.hysteria_ignore_client_bandwidth),
|
"ignore-client-bandwidth": strToBool(cfg.hysteria_ignore_client_bandwidth),
|
||||||
obfs: cfg.hysteria_obfs_type,
|
obfs: cfg.hysteria_obfs_type,
|
||||||
"obfs-password": cfg.hysteria_obfs_password,
|
"obfs-password": cfg.hysteria_obfs_password,
|
||||||
|
"obfs-min-packet-size": strToInt(cfg.hysteria_obfs_min_packet_size),
|
||||||
|
"obfs-max-packet-size": strToInt(cfg.hysteria_obfs_max_packet_size),
|
||||||
masquerade: cfg.hysteria_masquerade,
|
masquerade: cfg.hysteria_masquerade,
|
||||||
"realm-opts": cfg.hysteria2_realm === '1' ? {
|
"realm-opts": cfg.hysteria2_realm === '1' ? {
|
||||||
enable: true,
|
enable: true,
|
||||||
@@ -300,6 +302,14 @@ export function parseListener(cfg, isClient, label) {
|
|||||||
} : {}),
|
} : {}),
|
||||||
fallback: (cfg.sudoku_http_mask === '0') ? null : cfg.sudoku_fallback,
|
fallback: (cfg.sudoku_http_mask === '0') ? null : cfg.sudoku_fallback,
|
||||||
|
|
||||||
|
/* Snell */
|
||||||
|
psk: cfg.snell_psk,
|
||||||
|
version: cfg.snell_version,
|
||||||
|
"obfs-opts": cfg.type === 'snell' ? {
|
||||||
|
mode: cfg.plugin_opts_obfsmode,
|
||||||
|
host: cfg.plugin_opts_host,
|
||||||
|
} : null,
|
||||||
|
|
||||||
/* Tuic */
|
/* Tuic */
|
||||||
"max-idle-time": durationToSecond(cfg.tuic_max_idle_time),
|
"max-idle-time": durationToSecond(cfg.tuic_max_idle_time),
|
||||||
"authentication-timeout": durationToSecond(cfg.tuic_authentication_timeout),
|
"authentication-timeout": durationToSecond(cfg.tuic_authentication_timeout),
|
||||||
@@ -350,10 +360,10 @@ export function parseListener(cfg, isClient, label) {
|
|||||||
"congestion-controller": cfg.congestion_controller,
|
"congestion-controller": cfg.congestion_controller,
|
||||||
"bbr-profile": cfg.bbr_profile,
|
"bbr-profile": cfg.bbr_profile,
|
||||||
network: cfg.network,
|
network: cfg.network,
|
||||||
udp: strToBool(cfg.udp),
|
udp: cfg.udp === '0' ? false : true,
|
||||||
|
|
||||||
/* TLS fields */
|
/* TLS fields */
|
||||||
...(cfg.tls === '1' ? {
|
...(cfg.allow_insecure === '1' ? { "allow-insecure": true } : cfg.tls === '1' ? {
|
||||||
alpn: cfg.tls_alpn,
|
alpn: cfg.tls_alpn,
|
||||||
...(cfg.tls_reality === '1' ? {
|
...(cfg.tls_reality === '1' ? {
|
||||||
"reality-config": {
|
"reality-config": {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-passwall
|
PKG_NAME:=luci-app-passwall
|
||||||
PKG_VERSION:=26.5.20
|
PKG_VERSION:=26.6.2
|
||||||
PKG_RELEASE:=146
|
PKG_RELEASE:=147
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-passwall2
|
PKG_NAME:=luci-app-passwall2
|
||||||
PKG_VERSION:=26.5.19
|
PKG_VERSION:=26.5.19
|
||||||
PKG_RELEASE:=47
|
PKG_RELEASE:=48
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
@@ -1684,12 +1684,13 @@ function gen_config(var)
|
|||||||
blockTypes = (api.compare_versions(xray_version, "<", "26.4.25")) and { 65 } or nil, -- Todo is to remove it
|
blockTypes = (api.compare_versions(xray_version, "<", "26.4.25")) and { 65 } or nil, -- Todo is to remove it
|
||||||
rules = (api.compare_versions(xray_version, ">", "26.4.17")) and {
|
rules = (api.compare_versions(xray_version, ">", "26.4.17")) and {
|
||||||
{
|
{
|
||||||
qtype = "1,28",
|
qType = "1,28",
|
||||||
action = "hijack"
|
action = "hijack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
qtype = 65,
|
qType = 65,
|
||||||
action = "reject",
|
action = "return",
|
||||||
|
rCode = 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action = "direct"
|
action = "direct"
|
||||||
@@ -1708,11 +1709,12 @@ function gen_config(var)
|
|||||||
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 {
|
||||||
{
|
{
|
||||||
qtype = "1,28",
|
qType = "1,28",
|
||||||
action = "hijack"
|
action = "hijack"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action = "reject"
|
action = "return",
|
||||||
|
rCode = 0
|
||||||
}
|
}
|
||||||
} or nil
|
} or nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ LUCI_DEPENDS:=+luci-nginx +quickfile
|
|||||||
|
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_VERSION:=1.0.0
|
PKG_VERSION:=1.0.0
|
||||||
PKG_RELEASE:=5
|
PKG_RELEASE:=6
|
||||||
PKG_MAINTAINER:=sbwml <admin@cooluc.com>
|
PKG_MAINTAINER:=sbwml <admin@cooluc.com>
|
||||||
|
|
||||||
include $(TOPDIR)/feeds/luci/luci.mk
|
include $(TOPDIR)/feeds/luci/luci.mk
|
||||||
|
|||||||
@@ -5,9 +5,6 @@
|
|||||||
"action": {
|
"action": {
|
||||||
"type": "view",
|
"type": "view",
|
||||||
"path": "system/quickfile"
|
"path": "system/quickfile"
|
||||||
},
|
|
||||||
"depends": {
|
|
||||||
"acl": [ "luci-app-quickfile" ]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=mihomo-alpha
|
PKG_NAME:=mihomo-alpha
|
||||||
PKG_VERSION:=2026.05.19
|
PKG_VERSION:=2026.05.30
|
||||||
PKG_RELEASE:=9
|
PKG_RELEASE:=10
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||||
PKG_SOURCE_PROTO:=git
|
PKG_SOURCE_PROTO:=git
|
||||||
PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git
|
PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git
|
||||||
PKG_SOURCE_VERSION:=a52a27ed563970b79e50ced79f15100abf0bcc85
|
PKG_SOURCE_VERSION:=fc8c5a24b16991f98cd736950c17d1aa306a5041
|
||||||
PKG_MIRROR_HASH:=skip
|
PKG_MIRROR_HASH:=skip
|
||||||
|
|
||||||
PKG_LICENSE:=GPL3.0+
|
PKG_LICENSE:=GPL3.0+
|
||||||
@@ -18,7 +18,7 @@ PKG_BUILD_DEPENDS:=golang/host
|
|||||||
PKG_BUILD_PARALLEL:=1
|
PKG_BUILD_PARALLEL:=1
|
||||||
PKG_BUILD_FLAGS:=no-mips16
|
PKG_BUILD_FLAGS:=no-mips16
|
||||||
|
|
||||||
PKG_BUILD_VERSION:=alpha-a52a27ed
|
PKG_BUILD_VERSION:=alpha-fc8c5a24
|
||||||
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
|
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
|
||||||
|
|
||||||
GO_PKG:=github.com/metacubex/mihomo
|
GO_PKG:=github.com/metacubex/mihomo
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=mihomo-meta
|
PKG_NAME:=mihomo-meta
|
||||||
PKG_VERSION:=1.19.25
|
PKG_VERSION:=1.19.26
|
||||||
PKG_RELEASE:=5
|
PKG_RELEASE:=6
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||||
PKG_SOURCE_PROTO:=git
|
PKG_SOURCE_PROTO:=git
|
||||||
PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git
|
PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git
|
||||||
PKG_SOURCE_VERSION:=v1.19.25
|
PKG_SOURCE_VERSION:=v1.19.26
|
||||||
PKG_MIRROR_HASH:=skip
|
PKG_MIRROR_HASH:=skip
|
||||||
|
|
||||||
PKG_LICENSE:=GPL3.0+
|
PKG_LICENSE:=GPL3.0+
|
||||||
@@ -18,7 +18,7 @@ PKG_BUILD_DEPENDS:=golang/host
|
|||||||
PKG_BUILD_PARALLEL:=1
|
PKG_BUILD_PARALLEL:=1
|
||||||
PKG_BUILD_FLAGS:=no-mips16
|
PKG_BUILD_FLAGS:=no-mips16
|
||||||
|
|
||||||
PKG_BUILD_VERSION:=v1.19.25
|
PKG_BUILD_VERSION:=v1.19.26
|
||||||
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
|
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
|
||||||
|
|
||||||
GO_PKG:=github.com/metacubex/mihomo
|
GO_PKG:=github.com/metacubex/mihomo
|
||||||
|
|||||||
+2
-2
@@ -5,8 +5,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=mihomo
|
PKG_NAME:=mihomo
|
||||||
PKG_VERSION:=1.19.25
|
PKG_VERSION:=1.19.26
|
||||||
PKG_RELEASE:=9
|
PKG_RELEASE:=10
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/metacubex/mihomo/tar.gz/v$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/metacubex/mihomo/tar.gz/v$(PKG_VERSION)?
|
||||||
|
|||||||
Reference in new issue
Block a user