crates/netup: red mínima Rust-nativa (Etapa C pieza 5)
netup = binario hammer-propio (sync, sólo libc, sin tokio) que configura la red: levanta el link (netlink RTM_NEWLINK), negocia un lease DHCPv4 a mano sobre UDP (DISCOVER/OFFER/REQUEST/ACK con flag broadcast), y aplica IP + ruta default + /etc/resolv.conf (RTM_NEWADDR/RTM_NEWROUTE). Hand-roll de netlink y DHCP porque no hay cliente DHCP Rust "maduro" para adoptar al estilo ripgrep, y el workspace es 100% sync (rtnetlink arrastraría tokio). Reemplaza el `ip` estático de busybox. Validado in-VM contra el DHCP de QEMU slirp: ✓ lease 10.0.2.15 + ruta + DNS + egress TCP. Autodetecta la NIC (primera no-loopback) y espera carrier por sysfs. Infra de prueba: - scripts/drive-netup.py: bootea la VM, corre netup y verifica lease/NAT/DNS. SLIRP=1 ⇒ red user-mode (cero setup de host); si no, tap del lab. - scripts/labnet.sh: lab de LAN real opcional (tap directo + dnsmasq + NAT) para fidelidad de hardware; no requerido para validar netup. - scripts/netdiag.sh: diagnóstico del camino DHCP del lab (tcpdump+nft+dnsmasq). Lección: para validar el CÓDIGO conviene slirp primero (cero plomería de host); la LAN real (firewall INPUT, sutilezas del bridge, perms) es fidelidad posterior. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Generated
+7
@@ -884,6 +884,13 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "netup"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
|
||||
@@ -9,6 +9,7 @@ members = [
|
||||
"crates/hammer-agent",
|
||||
"crates/hammer-cli",
|
||||
"crates/hammerd",
|
||||
"crates/netup",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "netup"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Configuración de red mínima Rust-nativa: link up + cliente DHCPv4 + addr/route/resolv vía netlink."
|
||||
|
||||
[[bin]]
|
||||
name = "netup"
|
||||
path = "src/main.rs"
|
||||
|
||||
# Solo libc: netlink (RTM_NEWLINK/NEWADDR/NEWROUTE) y DHCP los hacemos a mano sobre la ABI estable del
|
||||
# kernel, sin tokio ni crates de netlink (el workspace es 100% sync). Cero deps nuevas que vendorar ⇒
|
||||
# build hermético y reproducible, mismo estilo que hammerd/hammer-overlay.
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
@@ -0,0 +1,217 @@
|
||||
//! Cliente DHCPv4 mínimo y SÍNCRONO: DISCOVER → OFFER → REQUEST → ACK sobre un UDP socket bindeado a
|
||||
//! la interfaz (SO_BINDTODEVICE) con broadcast. El servidor responde en broadcast (ponemos el flag
|
||||
//! BROADCAST), así no hace falta un raw/AF_PACKET socket para recibir sin IP todavía.
|
||||
|
||||
use std::io;
|
||||
use std::net::{Ipv4Addr, SocketAddrV4, UdpSocket};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
|
||||
const OP_REQUEST: u8 = 1;
|
||||
const HTYPE_ETHER: u8 = 1;
|
||||
const HLEN_ETHER: u8 = 6;
|
||||
const MAGIC: [u8; 4] = [99, 130, 83, 99]; // 0x63825363
|
||||
const FLAG_BROADCAST: u16 = 0x8000;
|
||||
|
||||
// Opciones DHCP.
|
||||
const OPT_SUBNET: u8 = 1;
|
||||
const OPT_ROUTER: u8 = 3;
|
||||
const OPT_DNS: u8 = 6;
|
||||
const OPT_REQ_IP: u8 = 50;
|
||||
const OPT_LEASE: u8 = 51;
|
||||
const OPT_MSG_TYPE: u8 = 53;
|
||||
const OPT_SERVER_ID: u8 = 54;
|
||||
const OPT_PARAM_LIST: u8 = 55;
|
||||
const OPT_END: u8 = 255;
|
||||
|
||||
// Tipos de mensaje (opción 53).
|
||||
const DHCPDISCOVER: u8 = 1;
|
||||
const DHCPOFFER: u8 = 2;
|
||||
const DHCPREQUEST: u8 = 3;
|
||||
const DHCPACK: u8 = 5;
|
||||
|
||||
/// El resultado de la negociación: lo que aplicaremos por netlink + resolv.conf.
|
||||
pub struct Lease {
|
||||
pub ip: [u8; 4],
|
||||
pub prefix: u8,
|
||||
pub router: Option<[u8; 4]>,
|
||||
pub dns: Vec<[u8; 4]>,
|
||||
pub lease_secs: u32,
|
||||
pub server: [u8; 4],
|
||||
}
|
||||
|
||||
/// xid (transaction id) desde /dev/urandom: el servidor lo refleja, lo usamos para casar la respuesta.
|
||||
fn random_xid() -> io::Result<u32> {
|
||||
let mut b = [0u8; 4];
|
||||
let mut f = std::fs::File::open("/dev/urandom")?;
|
||||
io::Read::read_exact(&mut f, &mut b)?;
|
||||
Ok(u32::from_ne_bytes(b))
|
||||
}
|
||||
|
||||
/// Construye un paquete DHCP (cabecera BOOTP de 236 B + magic cookie + opciones).
|
||||
fn build(msg_type: u8, xid: u32, mac: [u8; 6], extra: &[(u8, Vec<u8>)]) -> Vec<u8> {
|
||||
let mut p = vec![0u8; 236];
|
||||
p[0] = OP_REQUEST;
|
||||
p[1] = HTYPE_ETHER;
|
||||
p[2] = HLEN_ETHER;
|
||||
// hops=0, secs=0, ciaddr/yiaddr/siaddr/giaddr=0 (ya en cero)
|
||||
p[4..8].copy_from_slice(&xid.to_be_bytes());
|
||||
p[10..12].copy_from_slice(&FLAG_BROADCAST.to_be_bytes());
|
||||
p[28..34].copy_from_slice(&mac); // chaddr
|
||||
p.extend_from_slice(&MAGIC);
|
||||
// opción 53 (tipo de mensaje) primero.
|
||||
p.extend_from_slice(&[OPT_MSG_TYPE, 1, msg_type]);
|
||||
for (code, data) in extra {
|
||||
p.push(*code);
|
||||
p.push(data.len() as u8);
|
||||
p.extend_from_slice(data);
|
||||
}
|
||||
// opción 55: lista de parámetros que pedimos.
|
||||
p.extend_from_slice(&[OPT_PARAM_LIST, 4, OPT_SUBNET, OPT_ROUTER, OPT_DNS, OPT_LEASE]);
|
||||
p.push(OPT_END);
|
||||
p
|
||||
}
|
||||
|
||||
/// Recorre las opciones TLV tras la cabecera+cookie. Devuelve (msg_type, opciones por código).
|
||||
fn parse_options(pkt: &[u8]) -> Option<(u8, std::collections::HashMap<u8, Vec<u8>>)> {
|
||||
if pkt.len() < 240 || pkt[236..240] != MAGIC {
|
||||
return None;
|
||||
}
|
||||
let mut opts = std::collections::HashMap::new();
|
||||
let mut i = 240;
|
||||
while i < pkt.len() {
|
||||
let code = pkt[i];
|
||||
if code == OPT_END {
|
||||
break;
|
||||
}
|
||||
if code == 0 {
|
||||
i += 1; // pad
|
||||
continue;
|
||||
}
|
||||
if i + 1 >= pkt.len() {
|
||||
break;
|
||||
}
|
||||
let len = pkt[i + 1] as usize;
|
||||
if i + 2 + len > pkt.len() {
|
||||
break;
|
||||
}
|
||||
opts.insert(code, pkt[i + 2..i + 2 + len].to_vec());
|
||||
i += 2 + len;
|
||||
}
|
||||
let mt = opts.get(&OPT_MSG_TYPE).and_then(|v| v.first().copied())?;
|
||||
Some((mt, opts))
|
||||
}
|
||||
|
||||
fn ipv4(v: &[u8]) -> Option<[u8; 4]> {
|
||||
if v.len() >= 4 {
|
||||
Some([v[0], v[1], v[2], v[3]])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn prefix_from_mask(mask: [u8; 4]) -> u8 {
|
||||
mask.iter().map(|b| b.count_ones() as u8).sum()
|
||||
}
|
||||
|
||||
/// Negocia un lease en `ifname`. `mac` es la MAC de la interfaz (para chaddr).
|
||||
pub fn run(ifname: &str, mac: [u8; 6]) -> io::Result<Lease> {
|
||||
let sock = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 68))?;
|
||||
sock.set_broadcast(true)?;
|
||||
sock.set_read_timeout(Some(std::time::Duration::from_secs(5)))?;
|
||||
// SO_BINDTODEVICE: amarra el socket a la interfaz (sin IP todavía no hay ruta que elija salida).
|
||||
let cname = std::ffi::CString::new(ifname).unwrap();
|
||||
let r = unsafe {
|
||||
libc::setsockopt(
|
||||
sock.as_raw_fd(),
|
||||
libc::SOL_SOCKET,
|
||||
libc::SO_BINDTODEVICE,
|
||||
cname.as_ptr() as *const libc::c_void,
|
||||
(ifname.len() + 1) as libc::socklen_t,
|
||||
)
|
||||
};
|
||||
if r < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
let bcast = SocketAddrV4::new(Ipv4Addr::BROADCAST, 67);
|
||||
let xid = random_xid()?;
|
||||
|
||||
// DISCOVER → OFFER (reintentos: las VMs/servidores pueden tardar en contestar el primer paquete).
|
||||
let discover = build(DHCPDISCOVER, xid, mac, &[]);
|
||||
let mut buf = [0u8; 2048];
|
||||
let offer = recv_matching(&sock, &discover, &bcast, xid, DHCPOFFER, &mut buf, 4)?;
|
||||
// yiaddr (la IP ofrecida) vive en la cabecera BOOTP; recv_matching lo dejó en LAST_YIADDR.
|
||||
let yi = LAST_YIADDR.with(|c| *c.borrow());
|
||||
let server = offer
|
||||
.get(&OPT_SERVER_ID)
|
||||
.and_then(|v| ipv4(v))
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "OFFER sin server-id"))?;
|
||||
|
||||
// REQUEST (pidiendo el yiaddr ofrecido y citando el server-id) → ACK.
|
||||
let request = build(
|
||||
DHCPREQUEST,
|
||||
xid,
|
||||
mac,
|
||||
&[(OPT_REQ_IP, yi.to_vec()), (OPT_SERVER_ID, server.to_vec())],
|
||||
);
|
||||
let ack = recv_matching(&sock, &request, &bcast, xid, DHCPACK, &mut buf, 4)?;
|
||||
let ack_ip = LAST_YIADDR.with(|c| *c.borrow());
|
||||
|
||||
let prefix = ack
|
||||
.get(&OPT_SUBNET)
|
||||
.and_then(|v| ipv4(v))
|
||||
.map(prefix_from_mask)
|
||||
.unwrap_or(24);
|
||||
let router = ack.get(&OPT_ROUTER).and_then(|v| ipv4(v));
|
||||
let dns = ack
|
||||
.get(&OPT_DNS)
|
||||
.map(|v| v.chunks_exact(4).map(|c| [c[0], c[1], c[2], c[3]]).collect())
|
||||
.unwrap_or_default();
|
||||
let lease_secs = ack
|
||||
.get(&OPT_LEASE)
|
||||
.and_then(|v| if v.len() >= 4 { Some(u32::from_be_bytes([v[0], v[1], v[2], v[3]])) } else { None })
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(Lease { ip: ack_ip, prefix, router, dns, lease_secs, server })
|
||||
}
|
||||
|
||||
// yiaddr (la IP ofrecida) vive en la cabecera BOOTP, no en las opciones; lo guardamos al parsear cada
|
||||
// paquete para que `run` lo recupere sin reestructurar el flujo.
|
||||
thread_local! {
|
||||
static LAST_YIADDR: std::cell::RefCell<[u8; 4]> = const { std::cell::RefCell::new([0; 4]) };
|
||||
}
|
||||
|
||||
/// Manda `out` y espera un paquete con el xid y el tipo esperados, reintentando `tries` veces.
|
||||
fn recv_matching(
|
||||
sock: &UdpSocket,
|
||||
out: &[u8],
|
||||
dst: &SocketAddrV4,
|
||||
xid: u32,
|
||||
want_type: u8,
|
||||
buf: &mut [u8],
|
||||
tries: u32,
|
||||
) -> io::Result<std::collections::HashMap<u8, Vec<u8>>> {
|
||||
for _ in 0..tries {
|
||||
sock.send_to(out, dst)?;
|
||||
loop {
|
||||
match sock.recv_from(buf) {
|
||||
Ok((n, _)) => {
|
||||
let pkt = &buf[..n];
|
||||
if n < 240 || u32::from_be_bytes([pkt[4], pkt[5], pkt[6], pkt[7]]) != xid {
|
||||
continue; // no es nuestra transacción
|
||||
}
|
||||
if let Some((mt, opts)) = parse_options(pkt) {
|
||||
if mt == want_type {
|
||||
LAST_YIADDR.with(|c| c.borrow_mut().copy_from_slice(&pkt[16..20])); // yiaddr
|
||||
return Ok(opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut => {
|
||||
break // timeout → reintentar el send
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(io::Error::new(io::ErrorKind::TimedOut, format!("sin respuesta DHCP (tipo {want_type})")))
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//! netup — configuración de red mínima Rust-nativa para el userland de hammer (Etapa C).
|
||||
//!
|
||||
//! Reemplaza el `ip` de busybox + la IP estática hardcodeada del driver de rebuild por un binario
|
||||
//! propio: levanta la interfaz, negocia un lease DHCPv4 y aplica IP/ruta/DNS vía netlink — todo
|
||||
//! síncrono, sin tokio, dependiendo sólo de libc (ABI estable del kernel). Como hammerd/arje, es
|
||||
//! código hammer-propio: no hay un cliente DHCP Rust "maduro" para adoptar al estilo ripgrep.
|
||||
//!
|
||||
//! Uso: `netup [interfaz]` (sin argumento, autodetecta la primera interfaz no-loopback).
|
||||
|
||||
mod dhcp;
|
||||
mod netlink;
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("netup: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let iface = match std::env::args().nth(1) {
|
||||
Some(a) => a,
|
||||
None => detect_iface().ok_or("no encuentro ninguna interfaz no-loopback en /sys/class/net")?,
|
||||
};
|
||||
let ifindex = read_sysfs_int(&iface, "ifindex")? as i32;
|
||||
let mac = read_mac(&iface)?;
|
||||
println!("netup: interfaz {iface} (index {ifindex}, mac {})", fmt_mac(mac));
|
||||
|
||||
let mut nl = netlink::Netlink::open()?;
|
||||
nl.link_up(ifindex)?;
|
||||
// Esperar el carrier (virtio/e1000 lo levantan en ms, pero damos margen).
|
||||
wait_carrier(&iface, std::time::Duration::from_secs(5));
|
||||
|
||||
let lease = dhcp::run(&iface, mac)?;
|
||||
println!(
|
||||
"netup: lease {} /{} gw {} dns {} ({}s) de {}",
|
||||
fmt_ip(lease.ip),
|
||||
lease.prefix,
|
||||
lease.router.map(fmt_ip).unwrap_or_else(|| "-".into()),
|
||||
if lease.dns.is_empty() { "-".into() } else { lease.dns.iter().map(|d| fmt_ip(*d)).collect::<Vec<_>>().join(",") },
|
||||
lease.lease_secs,
|
||||
fmt_ip(lease.server),
|
||||
);
|
||||
|
||||
nl.add_addr(ifindex, lease.ip, lease.prefix)?;
|
||||
if let Some(gw) = lease.router {
|
||||
nl.add_default_route(ifindex, gw)?;
|
||||
}
|
||||
if !lease.dns.is_empty() {
|
||||
write_resolv_conf(&lease.dns)?;
|
||||
}
|
||||
println!("netup: red configurada en {iface}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Primera interfaz de /sys/class/net que no sea loopback (type 772 = ARPHRD_LOOPBACK).
|
||||
fn detect_iface() -> Option<String> {
|
||||
let mut names: Vec<String> = std::fs::read_dir("/sys/class/net")
|
||||
.ok()?
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name().to_string_lossy().into_owned())
|
||||
.filter(|n| n != "lo")
|
||||
.filter(|n| read_sysfs_int(n, "type").map(|t| t != 772).unwrap_or(true))
|
||||
.collect();
|
||||
names.sort(); // determinismo (eth0 < eth1)
|
||||
names.into_iter().next()
|
||||
}
|
||||
|
||||
fn read_sysfs_int(iface: &str, attr: &str) -> Result<i64, Box<dyn std::error::Error>> {
|
||||
let s = std::fs::read_to_string(format!("/sys/class/net/{iface}/{attr}"))?;
|
||||
Ok(s.trim().parse()?)
|
||||
}
|
||||
|
||||
fn read_mac(iface: &str) -> Result<[u8; 6], Box<dyn std::error::Error>> {
|
||||
let s = std::fs::read_to_string(format!("/sys/class/net/{iface}/address"))?;
|
||||
let bytes: Vec<u8> = s
|
||||
.trim()
|
||||
.split(':')
|
||||
.filter_map(|h| u8::from_str_radix(h, 16).ok())
|
||||
.collect();
|
||||
if bytes.len() != 6 {
|
||||
return Err(format!("MAC inesperada para {iface}: {}", s.trim()).into());
|
||||
}
|
||||
Ok([bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5]])
|
||||
}
|
||||
|
||||
/// Espera hasta `timeout` a que /sys/class/net/<if>/carrier sea 1 (link físico/virtio listo).
|
||||
fn wait_carrier(iface: &str, timeout: std::time::Duration) {
|
||||
let start = std::time::Instant::now();
|
||||
while start.elapsed() < timeout {
|
||||
if let Ok(c) = std::fs::read_to_string(format!("/sys/class/net/{iface}/carrier")) {
|
||||
if c.trim() == "1" {
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
|
||||
fn write_resolv_conf(dns: &[[u8; 4]]) -> std::io::Result<()> {
|
||||
let body: String = dns.iter().map(|d| format!("nameserver {}\n", fmt_ip(*d))).collect();
|
||||
std::fs::write("/etc/resolv.conf", body)
|
||||
}
|
||||
|
||||
fn fmt_ip(ip: [u8; 4]) -> String {
|
||||
format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3])
|
||||
}
|
||||
|
||||
fn fmt_mac(m: [u8; 6]) -> String {
|
||||
format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", m[0], m[1], m[2], m[3], m[4], m[5])
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
//! Netlink (NETLINK_ROUTE) mínimo y SÍNCRONO sobre libc: levantar un link, añadir una dirección IPv4 y
|
||||
//! una ruta por defecto. Hand-roll de los mensajes RTM_* (ABI estable del kernel) en vez de tirar de
|
||||
//! `rtnetlink` (async/tokio) — el workspace es 100% sync y así no agregamos deps que vendorar.
|
||||
|
||||
use std::io;
|
||||
use std::mem::size_of;
|
||||
use std::os::unix::io::RawFd;
|
||||
|
||||
// --- constantes de la ABI netlink (estables) ---
|
||||
const RTM_NEWLINK: u16 = 16;
|
||||
const RTM_NEWADDR: u16 = 20;
|
||||
const RTM_NEWROUTE: u16 = 24;
|
||||
const NLM_F_REQUEST: u16 = 0x001;
|
||||
const NLM_F_ACK: u16 = 0x004;
|
||||
const NLM_F_REPLACE: u16 = 0x100;
|
||||
const NLM_F_CREATE: u16 = 0x400;
|
||||
const NLMSG_ERROR: u16 = 0x2;
|
||||
const IFA_ADDRESS: u16 = 1;
|
||||
const IFA_LOCAL: u16 = 2;
|
||||
const RTA_OIF: u16 = 4;
|
||||
const RTA_GATEWAY: u16 = 5;
|
||||
const RT_SCOPE_UNIVERSE: u8 = 0;
|
||||
const RT_TABLE_MAIN: u8 = 254;
|
||||
const RTPROT_BOOT: u8 = 3;
|
||||
const RTN_UNICAST: u8 = 1;
|
||||
|
||||
// libc trae nlmsghdr/nlmsgerr/ifinfomsg pero NO estos tres de rtnetlink; los definimos (layout ABI).
|
||||
#[repr(C)]
|
||||
struct Rtattr {
|
||||
rta_len: u16,
|
||||
rta_type: u16,
|
||||
}
|
||||
#[repr(C)]
|
||||
struct Ifaddrmsg {
|
||||
ifa_family: u8,
|
||||
ifa_prefixlen: u8,
|
||||
ifa_flags: u8,
|
||||
ifa_scope: u8,
|
||||
ifa_index: u32,
|
||||
}
|
||||
#[repr(C)]
|
||||
struct Rtmsg {
|
||||
rtm_family: u8,
|
||||
rtm_dst_len: u8,
|
||||
rtm_src_len: u8,
|
||||
rtm_tos: u8,
|
||||
rtm_table: u8,
|
||||
rtm_protocol: u8,
|
||||
rtm_scope: u8,
|
||||
rtm_type: u8,
|
||||
rtm_flags: u32,
|
||||
}
|
||||
|
||||
fn nlmsg_align(len: usize) -> usize {
|
||||
(len + 3) & !3
|
||||
}
|
||||
|
||||
unsafe fn as_bytes<T>(p: &T) -> &[u8] {
|
||||
std::slice::from_raw_parts(p as *const T as *const u8, size_of::<T>())
|
||||
}
|
||||
|
||||
/// Socket netlink + número de secuencia para los requests.
|
||||
pub struct Netlink {
|
||||
fd: RawFd,
|
||||
seq: u32,
|
||||
}
|
||||
|
||||
impl Drop for Netlink {
|
||||
fn drop(&mut self) {
|
||||
unsafe { libc::close(self.fd) };
|
||||
}
|
||||
}
|
||||
|
||||
impl Netlink {
|
||||
pub fn open() -> io::Result<Self> {
|
||||
let fd = unsafe {
|
||||
libc::socket(
|
||||
libc::AF_NETLINK,
|
||||
libc::SOCK_RAW | libc::SOCK_CLOEXEC,
|
||||
libc::NETLINK_ROUTE,
|
||||
)
|
||||
};
|
||||
if fd < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
let mut sa: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
|
||||
sa.nl_family = libc::AF_NETLINK as u16;
|
||||
let r = unsafe {
|
||||
libc::bind(
|
||||
fd,
|
||||
&sa as *const _ as *const libc::sockaddr,
|
||||
size_of::<libc::sockaddr_nl>() as u32,
|
||||
)
|
||||
};
|
||||
if r < 0 {
|
||||
let e = io::Error::last_os_error();
|
||||
unsafe { libc::close(fd) };
|
||||
return Err(e);
|
||||
}
|
||||
Ok(Self { fd, seq: 0 })
|
||||
}
|
||||
|
||||
/// Empaqueta nlmsghdr + cuerpo + atributos, lo manda al kernel y espera el ACK (NLMSG_ERROR con
|
||||
/// error==0). `body` es el struct de familia (ifinfomsg/ifaddrmsg/rtmsg); `attrs` los rtattr.
|
||||
fn request(&mut self, msg_type: u16, flags: u16, body: &[u8], attrs: &[(u16, Vec<u8>)]) -> io::Result<()> {
|
||||
self.seq += 1;
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(256);
|
||||
buf.resize(size_of::<libc::nlmsghdr>(), 0); // hueco para el header, se rellena al final
|
||||
buf.extend_from_slice(body);
|
||||
while buf.len() % 4 != 0 {
|
||||
buf.push(0);
|
||||
}
|
||||
for (atype, data) in attrs {
|
||||
let rta_len = (size_of::<Rtattr>() + data.len()) as u16;
|
||||
let rta = Rtattr { rta_len, rta_type: *atype };
|
||||
buf.extend_from_slice(unsafe { as_bytes(&rta) });
|
||||
buf.extend_from_slice(data);
|
||||
let pad = nlmsg_align(rta_len as usize) - rta_len as usize;
|
||||
buf.extend(std::iter::repeat(0u8).take(pad));
|
||||
}
|
||||
let hdr = libc::nlmsghdr {
|
||||
nlmsg_len: buf.len() as u32,
|
||||
nlmsg_type: msg_type,
|
||||
nlmsg_flags: flags,
|
||||
nlmsg_seq: self.seq,
|
||||
nlmsg_pid: 0,
|
||||
};
|
||||
buf[..size_of::<libc::nlmsghdr>()].copy_from_slice(unsafe { as_bytes(&hdr) });
|
||||
|
||||
let mut k: libc::sockaddr_nl = unsafe { std::mem::zeroed() };
|
||||
k.nl_family = libc::AF_NETLINK as u16; // nl_pid 0 ⇒ el kernel
|
||||
let sent = unsafe {
|
||||
libc::sendto(
|
||||
self.fd,
|
||||
buf.as_ptr() as *const libc::c_void,
|
||||
buf.len(),
|
||||
0,
|
||||
&k as *const _ as *const libc::sockaddr,
|
||||
size_of::<libc::sockaddr_nl>() as u32,
|
||||
)
|
||||
};
|
||||
if sent < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
self.recv_ack()
|
||||
}
|
||||
|
||||
fn recv_ack(&mut self) -> io::Result<()> {
|
||||
let mut rbuf = [0u8; 8192];
|
||||
let n = unsafe {
|
||||
libc::recv(
|
||||
self.fd,
|
||||
rbuf.as_mut_ptr() as *mut libc::c_void,
|
||||
rbuf.len(),
|
||||
0,
|
||||
)
|
||||
};
|
||||
if n < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
let n = n as usize;
|
||||
if n < size_of::<libc::nlmsghdr>() {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "respuesta netlink truncada"));
|
||||
}
|
||||
let hdr: libc::nlmsghdr = unsafe { std::ptr::read_unaligned(rbuf.as_ptr() as *const _) };
|
||||
if hdr.nlmsg_type == NLMSG_ERROR {
|
||||
// nlmsgerr.error: 0 = éxito, negativo = -errno.
|
||||
let err: libc::nlmsgerr =
|
||||
unsafe { std::ptr::read_unaligned(rbuf.as_ptr().add(size_of::<libc::nlmsghdr>()) as *const _) };
|
||||
if err.error != 0 {
|
||||
return Err(io::Error::from_raw_os_error(-err.error));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Levanta el link (equivalente a `ip link set <if> up`).
|
||||
pub fn link_up(&mut self, ifindex: i32) -> io::Result<()> {
|
||||
let mut ifi: libc::ifinfomsg = unsafe { std::mem::zeroed() };
|
||||
ifi.ifi_family = libc::AF_UNSPEC as u8;
|
||||
ifi.ifi_index = ifindex;
|
||||
ifi.ifi_flags = libc::IFF_UP as u32;
|
||||
ifi.ifi_change = libc::IFF_UP as u32;
|
||||
self.request(RTM_NEWLINK, NLM_F_REQUEST | NLM_F_ACK, unsafe { as_bytes(&ifi) }, &[])
|
||||
}
|
||||
|
||||
/// Añade una dirección IPv4 con su prefijo (equivalente a `ip addr add ip/prefix dev <if>`).
|
||||
pub fn add_addr(&mut self, ifindex: i32, ip: [u8; 4], prefix: u8) -> io::Result<()> {
|
||||
let mut ifa: Ifaddrmsg = unsafe { std::mem::zeroed() };
|
||||
ifa.ifa_family = libc::AF_INET as u8;
|
||||
ifa.ifa_prefixlen = prefix;
|
||||
ifa.ifa_scope = RT_SCOPE_UNIVERSE;
|
||||
ifa.ifa_index = ifindex as u32;
|
||||
self.request(
|
||||
RTM_NEWADDR,
|
||||
NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_REPLACE,
|
||||
unsafe { as_bytes(&ifa) },
|
||||
&[(IFA_LOCAL, ip.to_vec()), (IFA_ADDRESS, ip.to_vec())],
|
||||
)
|
||||
}
|
||||
|
||||
/// Añade la ruta por defecto (equivalente a `ip route add default via <gw> dev <if>`).
|
||||
pub fn add_default_route(&mut self, ifindex: i32, gw: [u8; 4]) -> io::Result<()> {
|
||||
let mut rtm: Rtmsg = unsafe { std::mem::zeroed() };
|
||||
rtm.rtm_family = libc::AF_INET as u8;
|
||||
rtm.rtm_table = RT_TABLE_MAIN;
|
||||
rtm.rtm_protocol = RTPROT_BOOT;
|
||||
rtm.rtm_scope = RT_SCOPE_UNIVERSE;
|
||||
rtm.rtm_type = RTN_UNICAST;
|
||||
self.request(
|
||||
RTM_NEWROUTE,
|
||||
NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE,
|
||||
unsafe { as_bytes(&rtm) },
|
||||
&[(RTA_GATEWAY, gw.to_vec()), (RTA_OIF, (ifindex as u32).to_ne_bytes().to_vec())],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
# drive-netup.py — bootea la VM con la NIC conectada al tap del lab (scripts/labnet.sh) y corre `netup`
|
||||
# + chequeos de conectividad real (DHCP de dnsmasq, NAT, DNS). Driver no-interactivo: espera la shell
|
||||
# de arje-zero, manda el test, captura hasta el marcador y emite veredicto.
|
||||
#
|
||||
# Prerequisito: sudo sh scripts/labnet.sh up (bridge hmbr0 + tap hmtap0 + dnsmasq + NAT)
|
||||
#
|
||||
# Variables: KERNEL (def store/*-linux/boot/bzImage) DISK (def work/hammer-disk.img)
|
||||
# TAP (def hmtap0) NICDEV (def e1000) MEM (def 4096) KVM (1 si /dev/kvm)
|
||||
# DEADLINE (def 180s) LOG (def work/netup-run.log)
|
||||
import os, pty, select, subprocess, sys, time, re, glob
|
||||
|
||||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
|
||||
def absp(p):
|
||||
return p if os.path.isabs(p) else os.path.join(ROOT, p)
|
||||
|
||||
|
||||
KERNEL = os.environ.get("KERNEL") or (glob.glob(os.path.join(ROOT, "store/*-linux/boot/bzImage")) + [""])[0]
|
||||
DISK = absp(os.environ.get("DISK", "work/hammer-disk.img"))
|
||||
TAP = os.environ.get("TAP", "hmtap0")
|
||||
NICDEV = os.environ.get("NICDEV", "e1000")
|
||||
MEM = os.environ.get("MEM", "4096")
|
||||
DEADLINE = int(os.environ.get("DEADLINE", "180"))
|
||||
LOG = absp(os.environ.get("LOG", "work/netup-run.log"))
|
||||
WANT_KVM = os.environ.get("KVM", "1") == "1"
|
||||
|
||||
for p, what in [(KERNEL, "kernel"), (DISK, "imagen de disco")]:
|
||||
if not p or not os.path.exists(p):
|
||||
sys.exit(f"no existe {what}: {p}")
|
||||
|
||||
accel = ["-cpu", "Broadwell"]
|
||||
if WANT_KVM and os.access("/dev/kvm", os.W_OK):
|
||||
accel = ["-enable-kvm", "-cpu", "host"]
|
||||
|
||||
# SLIRP=1 ⇒ red user-mode de QEMU (DHCP/DNS/NAT virtuales, cero setup de host). Si no, tap del lab.
|
||||
if os.environ.get("SLIRP", "0") == "1":
|
||||
netdev = ["-netdev", "user,id=n0"]
|
||||
else:
|
||||
netdev = ["-netdev", f"tap,id=n0,ifname={TAP},script=no,downscript=no"]
|
||||
QEMU = (["qemu-system-x86_64", "-m", MEM, "-no-reboot", "-nographic"] + accel +
|
||||
["-kernel", KERNEL,
|
||||
"-drive", f"file={DISK},if=virtio,format=raw",
|
||||
"-append", "console=ttyS0 root=/dev/vda1 rw rdinit=/sbin/init"] +
|
||||
netdev + ["-device", f"{NICDEV},netdev=n0"])
|
||||
|
||||
# El test que corre dentro de la VM: netup (autodetecta la NIC) + conectividad NAT + DNS.
|
||||
# Los marcadores se CONSTRUYEN en runtime (printf '%s') para que NO aparezcan literales en el eco del
|
||||
# comando tecleado — si no, el driver los matchearía en el eco y saldría antes de que el test corra.
|
||||
TEST = os.environ.get("TESTCMD") or (
|
||||
"netup; "
|
||||
"ping -c2 -W2 8.8.8.8 >/dev/null 2>&1 && printf 'PING_%s\\n' OK || printf 'PING_%s\\n' FAIL; "
|
||||
"nslookup google.com >/dev/null 2>&1 && printf 'DNS_%s\\n' OK || printf 'DNS_%s\\n' FAIL; "
|
||||
"printf 'TEST_%s\\n' DONE"
|
||||
)
|
||||
TEST = TEST + "\n"
|
||||
|
||||
print(f"==> kernel {KERNEL}\n==> disco {DISK}\n==> tap {TAP} ({NICDEV}) mem {MEM} kvm {'sí' if accel[0]=='-enable-kvm' else 'no'}")
|
||||
ansi = re.compile(rb'\x1b\[[0-9;?]*[a-zA-Z]')
|
||||
master, slave = pty.openpty()
|
||||
p = subprocess.Popen(QEMU, stdin=slave, stdout=slave, stderr=slave, close_fds=True)
|
||||
os.close(slave)
|
||||
log = open(LOG, "wb")
|
||||
buf = b""
|
||||
sent = False
|
||||
start = time.time()
|
||||
|
||||
while True:
|
||||
if time.time() - start > DEADLINE:
|
||||
log.write(b"\n[DRIVER] DEADLINE\n")
|
||||
break
|
||||
r, _, _ = select.select([master], [], [], 5)
|
||||
if r:
|
||||
try:
|
||||
data = os.read(master, 8192)
|
||||
except OSError:
|
||||
break
|
||||
if not data:
|
||||
break
|
||||
log.write(data)
|
||||
log.flush()
|
||||
buf = ansi.sub(b'', buf + data)[-20000:]
|
||||
if not sent and (b"\n~ #" in buf or b"\r~ #" in buf):
|
||||
time.sleep(2)
|
||||
os.write(master, TEST.encode())
|
||||
sent = True
|
||||
log.write(b"\n[DRIVER] enviado test netup\n")
|
||||
if sent and b"TEST_DONE" in buf:
|
||||
time.sleep(1)
|
||||
try:
|
||||
log.write(os.read(master, 8192))
|
||||
except Exception:
|
||||
pass
|
||||
break
|
||||
elif p.poll() is not None:
|
||||
break
|
||||
|
||||
for fn in (p.terminate, p.kill):
|
||||
try:
|
||||
fn(); time.sleep(1)
|
||||
except Exception:
|
||||
pass
|
||||
log.close()
|
||||
|
||||
blob = ansi.sub(b'', open(LOG, "rb").read())
|
||||
configured = b"red configurada" in blob # netup imprime esto sólo si todo salió bien
|
||||
ping = b"PING_OK" in blob
|
||||
dns = b"DNS_OK" in blob
|
||||
lease = re.search(rb"netup: lease (\d+\.\d+\.\d+\.\d+)", blob)
|
||||
print(f"\nDRIVER done sent={sent} elapsed={int(time.time()-start)}s")
|
||||
print(f" netup ok : {configured}")
|
||||
print(f" lease : {lease.group(1).decode() if lease else '—'}")
|
||||
print(f" ping NAT : {ping}")
|
||||
print(f" DNS : {dns}")
|
||||
ok = configured and lease and ping and dns
|
||||
print("RESULTADO:", "✓ RED OK (lease + NAT + DNS)" if ok else "✗ revisá el log: " + LOG)
|
||||
sys.exit(0 if ok else 1)
|
||||
@@ -0,0 +1,88 @@
|
||||
#!/bin/sh
|
||||
# labnet.sh — lab de red para probar `netup` (y luego OpenSSH) in-VM con DHCP/DNS/NAT REALES, sin poner
|
||||
# la VM en la LAN física (el host sale por WiFi, que no se puede bridgear a L2).
|
||||
#
|
||||
# Modelo TAP DIRECTO (sin bridge): para UNA VM no hace falta bridge. El host pone su IP en el tap
|
||||
# (192.168.100.1/24) y corre dnsmasq escuchando ahí; la VM y el host son pares L2 sobre el tap. Esto
|
||||
# evita la sutileza del Linux bridge (broadcast entregado localmente desde un puerto con skb_iif =
|
||||
# puerto, que el SO_BINDTODEVICE de dnsmasq al bridge no matchea ⇒ no recibía el DISCOVER).
|
||||
#
|
||||
# Da DHCP+DNS reales (dnsmasq) + salida a internet por NAT hacia el uplink. Ejercita exactamente los
|
||||
# code paths de netup: netlink real, DHCP DISCOVER/OFFER/REQUEST/ACK reales, ruta + DNS reales.
|
||||
#
|
||||
# REQUIERE ROOT. Levantar: sudo sh scripts/labnet.sh up Bajar: sudo sh scripts/labnet.sh down
|
||||
# El tap queda owned por $LAB_USER ⇒ QEMU (no-root) lo abre.
|
||||
#
|
||||
# Variables: TAP (def hmtap0) SUBNET (def 192.168.100) UPLINK (def wlan0)
|
||||
# LAB_USER (dueño del tap, def $SUDO_USER) DNS_UP (upstream DNS, def 192.168.1.1)
|
||||
set -eu
|
||||
TAP="${TAP:-hmtap0}"
|
||||
SUBNET="${SUBNET:-192.168.100}"
|
||||
UPLINK="${UPLINK:-wlan0}"
|
||||
LAB_USER="${LAB_USER:-${SUDO_USER:-root}}"
|
||||
DNS_UP="${DNS_UP:-192.168.1.1}"
|
||||
PIDF="/run/hmdnsmasq.pid"
|
||||
|
||||
[ "$(id -u)" = 0 ] || { echo "labnet: requiere root (sudo sh scripts/labnet.sh $*)" >&2; exit 1; }
|
||||
|
||||
fw_clean() { # quita las reglas "hmlab" que insertamos en el firewall del host
|
||||
for ch in input forward; do
|
||||
while h=$(nft -a list chain inet filter "$ch" 2>/dev/null | awk '/comment "hmlab"/{print $NF; exit}'); [ -n "${h:-}" ]; do
|
||||
nft delete rule inet filter "$ch" handle "$h" 2>/dev/null || break
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
up() {
|
||||
echo "==> tap $TAP ($SUBNET.1/24, owner $LAB_USER) — modelo tap directo, sin bridge"
|
||||
# auto-limpieza de estado viejo (bridge de versiones previas + tap que pudo quedar enslaved)
|
||||
ip link del hmbr0 2>/dev/null || true
|
||||
ip link del "$TAP" 2>/dev/null || true
|
||||
ip tuntap add dev "$TAP" mode tap user "$LAB_USER"
|
||||
ip addr replace "$SUBNET.1/24" dev "$TAP"
|
||||
ip link set "$TAP" up
|
||||
|
||||
echo "==> forwarding + NAT (nftables) $SUBNET.0/24 → $UPLINK"
|
||||
sysctl -wq net.ipv4.ip_forward=1
|
||||
nft list table ip hmnat >/dev/null 2>&1 || nft add table ip hmnat
|
||||
nft flush table ip hmnat
|
||||
nft add chain ip hmnat hmpost '{ type nat hook postrouting priority 100 ; }'
|
||||
nft add rule ip hmnat hmpost ip saddr "$SUBNET.0/24" oifname "$UPLINK" masquerade
|
||||
|
||||
# El host tiene su PROPIO firewall (inet filter, policy drop en input/forward). Insertamos accepts
|
||||
# ACOTADOS al tap del lab DENTRO de inet filter (al tope, vía `insert`), reversibles por su comment
|
||||
# "hmlab". input: el DISCOVER llega a dnsmasq. forward: el NAT VM→internet pasa.
|
||||
if nft list chain inet filter input >/dev/null 2>&1; then
|
||||
echo "==> permitir $TAP en el firewall del host (inet filter input/forward)"
|
||||
fw_clean
|
||||
nft insert rule inet filter input iifname "$TAP" accept comment "hmlab"
|
||||
nft insert rule inet filter forward iifname "$TAP" accept comment "hmlab"
|
||||
nft insert rule inet filter forward oifname "$TAP" accept comment "hmlab"
|
||||
fi
|
||||
|
||||
echo "==> dnsmasq (DHCP $SUBNET.50-150 + DNS→$DNS_UP) en $TAP"
|
||||
[ -f "$PIDF" ] && kill "$(cat "$PIDF")" 2>/dev/null || true
|
||||
dnsmasq --interface="$TAP" --bind-interfaces --except-interface=lo \
|
||||
--dhcp-range="$SUBNET.50,$SUBNET.150,12h" --dhcp-authoritative \
|
||||
--dhcp-option=3,"$SUBNET.1" --dhcp-option=6,"$SUBNET.1" \
|
||||
--listen-address="$SUBNET.1" --no-resolv --server="$DNS_UP" \
|
||||
--dhcp-leasefile=/tmp/hmdnsmasq.leases \
|
||||
--pid-file="$PIDF" --log-dhcp --log-facility=/tmp/hmdnsmasq.log
|
||||
chmod 644 /tmp/hmdnsmasq.log /tmp/hmdnsmasq.leases 2>/dev/null || true
|
||||
echo "==> listo. QEMU: -netdev tap,id=n0,ifname=$TAP,script=no,downscript=no -device e1000,netdev=n0"
|
||||
}
|
||||
|
||||
down() {
|
||||
echo "==> teardown"
|
||||
[ -f "$PIDF" ] && kill "$(cat "$PIDF")" 2>/dev/null && rm -f "$PIDF" || true
|
||||
nft delete table ip hmnat 2>/dev/null || true
|
||||
fw_clean
|
||||
ip link del "$TAP" 2>/dev/null || true
|
||||
echo "==> red del lab desmontada"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
up) up ;;
|
||||
down) down ;;
|
||||
*) echo "uso: sudo sh scripts/labnet.sh {up|down}" >&2; exit 1 ;;
|
||||
esac
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/bin/bash
|
||||
# netdiag.sh — diagnostica (y prueba un fix) el camino DHCP del lab de red, todo como root en una corrida.
|
||||
# Corré: sudo bash scripts/netdiag.sh
|
||||
# Deja un reporte legible en work/netdiag.txt (chmod 644) y lo imprime.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.." || exit 1
|
||||
BR="${BR:-hmtap0}"; TAP="${TAP:-hmtap0}"; SUBNET="${SUBNET:-192.168.100}"; VMMAC="52:54:00:12:34:56"
|
||||
OUT=work/netdiag.txt
|
||||
DNSLOG=/tmp/hmdnsmasq.log
|
||||
mkdir -p work
|
||||
exec > >(tee "$OUT") 2>&1
|
||||
|
||||
[ "$(id -u)" = 0 ] || { echo "corré con: sudo bash scripts/netdiag.sh"; exit 1; }
|
||||
echo "===== netdiag ====="
|
||||
|
||||
run_test() { KVM=1 MEM=4096 DEADLINE=45 LOG=work/netdiag-run.log python3 scripts/drive-netup.py >/dev/null 2>&1; }
|
||||
|
||||
echo; echo "### 1. firewall del host (nft) ###"
|
||||
echo "-- base chains input + policies --"
|
||||
nft list ruleset 2>/dev/null | grep -iE 'table |chain |hook (input|forward|prerouting)|policy|type ' | head -40
|
||||
INPUT_DROP=$(nft list ruleset 2>/dev/null | grep -iE 'hook input' | grep -ci 'policy drop')
|
||||
echo "input-hook chains con policy drop: $INPUT_DROP"
|
||||
|
||||
echo; echo "### 2. captura DHCP en $TAP (egress del VM) + $BR mientras manda DISCOVER ###"
|
||||
command -v tcpdump >/dev/null || echo "(tcpdump ausente — salteo captura)"
|
||||
: > /tmp/netdiag.pcap.txt; : > /tmp/netdiag.br.txt
|
||||
echo "estado del tap/bridge ANTES (idle):"; ip -br link show "$TAP"; ip -br link show "$BR"
|
||||
if command -v tcpdump >/dev/null; then
|
||||
timeout 55 tcpdump -i "$TAP" -nnev 'udp port 67 or udp port 68' > /tmp/netdiag.pcap.txt 2>/dev/null &
|
||||
TPID=$!
|
||||
timeout 55 tcpdump -i "$BR" -nnev 'udp port 67 or udp port 68' > /tmp/netdiag.br.txt 2>/dev/null &
|
||||
TPID2=$!; sleep 1
|
||||
fi
|
||||
run_test
|
||||
sleep 1; for pid in ${TPID:-} ${TPID2:-}; do kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null; done
|
||||
echo "-- DISCOVER/Offer en el TAP $TAP (lo que el VM realmente manda/recibe) --"
|
||||
grep -icE 'Discover|Offer|Request|Reply|BOOTP' /tmp/netdiag.pcap.txt; grep -iE 'Discover|Offer|cksum' /tmp/netdiag.pcap.txt | head -6
|
||||
echo "-- paquetes DHCP capturados en $BR --"
|
||||
grep -icE 'Discover|Offer|Request|Reply|BOOTP' /tmp/netdiag.pcap.txt
|
||||
echo "-- muestra (Discover/Offer/checksum) --"
|
||||
grep -iE 'Discover|Offer|cksum' /tmp/netdiag.pcap.txt | head -10
|
||||
echo "-- dnsmasq: ¿recibió/ofreció? --"
|
||||
grep -iE 'DHCPDISCOVER|DHCPOFFER|DHCPACK|no address' "$DNSLOG" 2>/dev/null | tail -8 || echo "(sin entradas DHCP)"
|
||||
|
||||
echo; echo "### 3. veredicto del diagnóstico ###"
|
||||
SAW_DISC=$(grep -ic 'Discover' /tmp/netdiag.pcap.txt)
|
||||
BAD_CKSUM=$(grep -ic 'bad udp cksum' /tmp/netdiag.pcap.txt)
|
||||
DNS_SAW=$(grep -ic 'DHCPDISCOVER' "$DNSLOG" 2>/dev/null)
|
||||
echo "DISCOVER en bridge=$SAW_DISC bad-cksum=$BAD_CKSUM dnsmasq-vio-DISCOVER=$DNS_SAW input-drop=$INPUT_DROP"
|
||||
if [ "$SAW_DISC" = 0 ]; then echo ">> el DISCOVER NO llega al bridge (problema tap/QEMU)"; fi
|
||||
if [ "$SAW_DISC" != 0 ] && [ "$BAD_CKSUM" != 0 ]; then echo ">> DISCOVER llega con CHECKSUM MALO (offload) ⇒ dnsmasq lo descarta"; fi
|
||||
if [ "$SAW_DISC" != 0 ] && [ "$DNS_SAW" = 0 ] && [ "$BAD_CKSUM" = 0 ]; then echo ">> DISCOVER llega OK pero dnsmasq no lo procesa ⇒ firewall INPUT (drop=$INPUT_DROP)"; fi
|
||||
|
||||
echo; echo "### 4. firewall INPUT: inspecciono, meto accepts CONTADOS y retesteo ###"
|
||||
echo "-- inet filter input ANTES (con handles) --"
|
||||
nft -a list chain inet filter input 2>/dev/null
|
||||
echo "-- inserto accepts contados (por iif del bridge y por puerto DHCP/DNS) --"
|
||||
nft insert rule inet filter input udp dport '{ 67, 68, 53 }' counter accept comment '"hmlab"' 2>&1 | head -2
|
||||
nft insert rule inet filter input iifname "$BR" counter accept comment '"hmlab"' 2>&1 | head -2
|
||||
run_test
|
||||
echo "-- reglas hmlab DESPUÉS del retest (los counters dicen cuál matcheó) --"
|
||||
nft -a list chain inet filter input 2>/dev/null | grep -i hmlab
|
||||
echo "-- dnsmasq tras retest --"
|
||||
grep -iE 'DHCPDISCOVER|DHCPOFFER|DHCPACK' "$DNSLOG" 2>/dev/null | tail -5 || echo "(nada)"
|
||||
echo "-- netup tras retest --"
|
||||
grep -aE 'netup: lease|netup: red configurada|sin respuesta DHCP|PING_OK|PING_FAIL|DNS_OK|DNS_FAIL' work/netdiag-run.log | tail -5
|
||||
if grep -qa 'netup: red configurada' work/netdiag-run.log; then echo "RESULTADO: ✓ RED OK"; else echo "RESULTADO: ✗ sigue fallando"; fi
|
||||
|
||||
echo; echo "### 5. dnsmasq log COMPLETO (volcado para análisis) ###"
|
||||
tail -30 "$DNSLOG" 2>/dev/null || echo "(no legible)"
|
||||
echo; echo "### 6. netup salida COMPLETA del retest ###"
|
||||
tr -d '\r' < work/netdiag-run.log 2>/dev/null | sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g' | grep -aE 'netup:|e1000.*Link|PING_|DNS_' | tail -12
|
||||
|
||||
chmod 644 "$OUT" 2>/dev/null
|
||||
chmod 666 work/netdiag-run.log 2>/dev/null
|
||||
echo; echo "(reporte: $OUT)"
|
||||
Reference in New Issue
Block a user