Wake-on-LAN
Machines woken from a web page: magic packets, with SecureOn passwords, sent to a subnet's broadcast address or a host; MAC addresses read in any spelling; broadcast addresses worked out; and magic packets read back for a relay.
Version 1.0.0 · 5 functions · 0 crates
Functions
What use wake_on_lan.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use wol_mac, wol_packet from wake_on_lan (Quillo 0.6).
wol_mac(text: str) → anyor an errorA MAC address in any common spelling read: { mac, dash, cisco, bare, oui, unicast, universal } (mac is 00:1A:2B:3C:4D:5E); one that cannot be a MAC address is an error saying the spellings.
wol_packet(mac: str, password: str) → stror an errorThe magic packet for a MAC address as hex: 6 bytes of FF, the address 16 times, and the SecureOn password if one is given ("" for none; 6 bytes like a MAC, or 4 like an IPv4 address).
wol_send(mac: str, options: any) → anyor an errorWakes a machine: its magic packet sent count times (3), interval_ms apart (100), to to ("255.255.255.255" unless given: a subnet's broadcast address such as "192.168.1.255", or a host) on UDP port (9; 7 is the other usual one), from from (a local address, to pick the network card); password for SecureOn.
wol_broadcast(network: str) → stror an errorA subnet's broadcast address, for wol_send's to: "192.168.1.20/24" or "10.0.0.7/255.255.252.0" (or with a space) gives "192.168.1.255" and "10.0.3.255".
wol_read(bytes: list) → anyor an errorReads a magic packet (as a udp.listen handler gets p.bytes, or a frame holding one): { mac, password }, the password as hex or null. Anything else is an error.
Get it
$ quillo install wake_on_lan # wake_on_lan.qwer into this folder$ quillo install wake_on_lan --example # and its example, wake_on_lan.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/wake_on_lan.qwer$ curl -O https://quillolang.org/plugins/files/wake_on_lan.qweThen, in your program, all of it or only what you call:
use wake_on_lan.qweruse wol_mac, wol_packet from wake_on_lan // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// Waking the office's machines from a web page: each by name, its magic packet sent to the// subnet's broadcast address (or WOL_HOST), one with a SecureOn password; MAC addresses as// people paste them; and a relay that wakes machines on another subnet when asked over UDP.//// quillo run wake_on_lan.qwe then open http://localhost:8080/wake/cad-workstation// WOL_HOST=192.168.1.255 quillo run wake_on_lan.qwe to send to that subnet onlyuse "wake_on_lan.qwer"let machines = { "cad-workstation": { mac: "00-1A-2B-3C-4D-5E" }, "build-server": { mac: "a4bb.6d12.0f9e", password: "c0:ff:ee:00:00:01" }, "nas": { mac: "3c:7c:3f:1:2:3" },}let TO = env("WOL_HOST", "255.255.255.255")let PORT = int(env("WOL_PORT", "9"))route GET /wake/:name { let m = machines[name] ?? reply 404 "no machine by that name" wol_send(m.mac, { to: TO, port: PORT, password: m.password ?? "", count: 3 })?}route GET /macs = { same: ["00:1a:2b:3c:4d:5e", "00-1A-2B-3C-4D-5E", "001a.2b3c.4d5e", "001A2B3C4D5E", "001a2b-3c4d5e"] | map((m) => wol_mac(m)?.mac), nas: wol_mac(machines.nas.mac)?, wrong: wol_mac("00:1a:2b:3c:4d").error,}route GET /broadcast = { office: wol_broadcast("192.168.1.20/24")?, warehouse: wol_broadcast("10.0.0.7 255.255.252.0")?, bad: wol_broadcast("10.0.0.7/255.0.255.0").error,}// The packet itself, and read back as a relay would.route GET /packet { let hex = wol_packet("00:1a:2b:3c:4d:5e", "c0:ff:ee:00:00:01")? { bytes: len(hex) / 2, starts: slice(hex, 0, 24), password_part: slice(hex, -12) }}// A relay: a machine on this subnet takes "wake <mac>" requests or magic packets on a UDP// port and wakes the machine here, where broadcasts reach it.let relayed = []udp.listen(int(env("WOL_RELAY_PORT", "9009")), (p) => { let asked = p.text != null && starts_with(p.text, "wake ") ? { mac: slice(p.text, 5), password: null } : wol_read(p.bytes) if !asked { return null } relayed = relayed + [wol_send(asked.mac, { to: TO, port: PORT, password: asked.password ?? "" })]})route GET /relay { udp.send("127.0.0.1", int(env("WOL_RELAY_PORT", "9009")), "wake 00:1a:2b:3c:4d:5e") sleep(300) { relayed: relayed | map((r) => r.mac ?? r.error) }}$ quillo run wake_on_lan.qwe$ curl 'localhost:8080/wake/cad-workstation'$ curl 'localhost:8080/wake/build-server'$ curl 'localhost:8080/macs'$ curl 'localhost:8080/broadcast'$ curl 'localhost:8080/relay'It talks to the Redis that REDIS_URL names, else Redis on this machine. The site's test runs it against a stand-in.
Source
//! Wake-on-LAN: magic packets (with SecureOn passwords) sent to a subnet's broadcast address or a host, several times for lossy networks; MAC addresses read in their many spellings (colons, dashes, Cisco dots, bare hex, one-digit groups) and written in each; a subnet's broadcast address worked out; and magic packets read back, for a relay on `udp.listen`.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Value};use std::net::{Ipv4Addr, SocketAddr, ToSocketAddrs, UdpSocket};use std::time::Duration;fn parse_mac(text: &str) -> Result<[u8; 6], String> { let t = text.trim(); let bad = || format!("`{}` is not a MAC address: write it like 00:1a:2b:3c:4d:5e, 00-1A-2B-3C-4D-5E, 001a.2b3c.4d5e or 001A2B3C4D5E", text); let t2 = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")).unwrap_or(t); let groups: Vec<&str> = t2.split(|c| c == ':' || c == '-' || c == '.' || c == ' ').filter(|g| !g.is_empty()).collect(); if groups.iter().any(|g| !g.chars().all(|c| c.is_ascii_hexdigit())) { return Err(bad()); } let hex: String = match groups.len() { 1 if groups[0].len() == 12 => groups[0].to_string(), // Cisco: three groups of four 3 if groups.iter().all(|g| g.len() == 4) => groups.concat(), // two groups of six (001a2b-3c4d5e) 2 if groups.iter().all(|g| g.len() == 6) => groups.concat(), // six groups of one or two digits (0:1a:2b:3c:4d:5e, as some tools print them) 6 if groups.iter().all(|g| g.len() <= 2) => groups.iter().map(|g| format!("{:0>2}", g)).collect(), _ => return Err(bad()), }; let mut m = [0u8; 6]; for i in 0..6 { m[i] = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16).map_err(|_| bad())?; } Ok(m)}fn mac_text(m: &[u8], sep: &str, upper: bool) -> String { let s = m.iter().map(|b| if upper { format!("{:02X}", b) } else { format!("{:02x}", b) }).collect::<Vec<_>>().join(sep); s}fn parse_password(p: &str) -> Result<Vec<u8>, String> { let p = p.trim(); if p.is_empty() { return Ok(vec![]); } if let Ok(ip) = p.parse::<Ipv4Addr>() { return Ok(ip.octets().to_vec()); } if let Ok(m) = parse_mac(p) { return Ok(m.to_vec()); } let h: String = p.chars().filter(|c| !matches!(c, ':' | '-' | ' ')).collect(); if h.len() == 8 && h.chars().all(|c| c.is_ascii_hexdigit()) { return Ok((0..4).map(|i| u8::from_str_radix(&h[2 * i..2 * i + 2], 16).unwrap()).collect()); } Err(format!("`{}` is not a SecureOn password: it is 6 bytes written like a MAC address (01:02:03:04:05:06) or 4 like an IPv4 address", p))}fn packet(mac: &[u8; 6], pw: &[u8]) -> Vec<u8> { let mut p = vec![0xFFu8; 6]; for _ in 0..16 { p.extend_from_slice(mac); } p.extend_from_slice(pw); p}/// A MAC address in any common spelling read: `{ mac, dash, cisco, bare, oui, unicast, universal }`/// (`mac` is `00:1A:2B:3C:4D:5E`); one that cannot be a MAC address is an error saying the spellings.pub fn wol_mac(text: &str) -> Result<serde_json::Value, String> { let m = parse_mac(text)?; let bare = mac_text(&m, "", false); Ok(json!({ "mac": mac_text(&m, ":", true), "dash": mac_text(&m, "-", true), "cisco": format!("{}.{}.{}", &bare[0..4], &bare[4..8], &bare[8..12]), "bare": bare, "oui": mac_text(&m[..3], ":", true), "unicast": m[0] & 1 == 0, "universal": m[0] & 2 == 0, }))}/// The magic packet for a MAC address as hex: 6 bytes of FF, the address 16 times, and the/// SecureOn password if one is given (`""` for none; 6 bytes like a MAC, or 4 like an IPv4 address).pub fn wol_packet(mac: &str, password: &str) -> Result<String, String> { let m = parse_mac(mac)?; let pw = parse_password(password)?; Ok(packet(&m, &pw).iter().map(|b| format!("{:02x}", b)).collect())}fn sock_addr(host: &str, port: u16) -> Result<SocketAddr, String> { let h = host.trim(); let with_port = if h.matches(':').count() == 1 && !h.starts_with('[') { h.to_string() } else { format!("{}:{}", h, port) }; with_port.to_socket_addrs().map_err(|e| format!("{}: {}", host, e))?.find(|a| a.is_ipv4()).ok_or(format!("{} has no IPv4 address (Wake-on-LAN is IPv4 broadcast)", host))}/// Wakes a machine: its magic packet sent `count` times (3), `interval_ms` apart (100), to/// `to` (`"255.255.255.255"` unless given: a subnet's broadcast address such as/// `"192.168.1.255"`, or a host) on UDP `port` (9; 7 is the other usual one), from `from`/// (a local address, to pick the network card); `password` for SecureOn.pub fn wol_send(mac: &str, options: serde_json::Value) -> Result<serde_json::Value, String> { let m = parse_mac(mac)?; let o = if options.is_object() { options } else { json!({}) }; let s = |k: &str| o.get(k).and_then(|v| v.as_str()).map(|x| x.to_string()); let pw = parse_password(&s("password").unwrap_or_default())?; let port = o.get("port").and_then(|v| v.as_u64()).unwrap_or(9).clamp(1, 65535) as u16; let to = s("to").unwrap_or_else(|| "255.255.255.255".into()); let count = o.get("count").and_then(|v| v.as_u64()).unwrap_or(3).clamp(1, 20); let interval = o.get("interval_ms").and_then(|v| v.as_u64()).unwrap_or(100).min(5000); let a = sock_addr(&to, port)?; let from = match s("from") { Some(f) => format!("{}:0", f.parse::<Ipv4Addr>().map_err(|_| format!("from {:?} is not an IPv4 address of this machine", f))?), None => "0.0.0.0:0".to_string(), }; let sock = UdpSocket::bind(&from).map_err(|e| format!("cannot use {}: {}", from, e))?; sock.set_broadcast(true).map_err(|e| e.to_string())?; let p = packet(&m, &pw); for i in 0..count { if i > 0 { std::thread::sleep(Duration::from_millis(interval)); } sock.send_to(&p, a).map_err(|e| { let hint = if e.raw_os_error() == Some(13) || e.kind() == std::io::ErrorKind::PermissionDenied { " (broadcasts may be refused on this network: send to the subnet's broadcast address, from: a local address, or a host)" } else { "" }; format!("sending to {}: {}{}", a, e, hint) })?; } Ok(json!({ "mac": mac_text(&m, ":", true), "to": a.to_string(), "packets": count, "bytes": p.len(), "secure_on": !pw.is_empty() }))}/// A subnet's broadcast address, for `wol_send`'s `to`: `"192.168.1.20/24"` or/// `"10.0.0.7/255.255.252.0"` (or with a space) gives `"192.168.1.255"` and `"10.0.3.255"`.pub fn wol_broadcast(network: &str) -> Result<String, String> { let n = network.trim(); let (ip, mask) = n.split_once(|c| c == '/' || c == ' ').ok_or_else(|| format!("`{}`: write an address and its prefix, as 192.168.1.20/24", network))?; let ip: Ipv4Addr = ip.trim().parse().map_err(|_| format!("`{}` is not an IPv4 address", ip))?; let m = mask.trim(); let bits: u32 = if let Ok(p) = m.parse::<u32>() { if p > 32 { return Err(format!("/{} is not a prefix (0 to 32)", p)); } if p == 0 { 0 } else { u32::MAX << (32 - p) } } else { let mm: Ipv4Addr = m.parse().map_err(|_| format!("`{}` is neither a prefix nor a netmask", m))?; let b = u32::from(mm); if b.leading_ones() + b.trailing_zeros() != 32 { return Err(format!("{} is not a netmask (its ones must come first)", mm)); } b }; Ok(Ipv4Addr::from(u32::from(ip) | !bits).to_string())}/// Reads a magic packet (as a `udp.listen` handler gets `p.bytes`, or a frame holding one):/// `{ mac, password }`, the password as hex or null. Anything else is an error.pub fn wol_read(bytes: Vec<i64>) -> Result<serde_json::Value, String> { let b: Vec<u8> = bytes.iter().map(|x| u8::try_from(*x).map_err(|_| format!("{} is not a byte", x))).collect::<Result<_, _>>()?; let mut i = 0; while i + 102 <= b.len() { if b[i..i + 6].iter().all(|x| *x == 0xFF) { let mac = &b[i + 6..i + 12]; if mac != [0xFF; 6] && (0..16).all(|k| &b[i + 6 + 6 * k..i + 12 + 6 * k] == mac) { let rest = &b[i + 102..]; let pw = match rest.len() { 4 | 6 => Some(rest.iter().map(|x| format!("{:02x}", x)).collect::<Vec<_>>().join(":")), _ => None, }; return Ok(json!({ "mac": mac_text(mac, ":", true), "password": pw })); } } i += 1; } Err("not a magic packet: no 6 bytes of FF followed by one MAC address 16 times".into())}Related
mdns.qwer · 1.0.0Bonjour on the LAN: printers, web panels and screens found by service type with their host, port, addresses and TXT records, the network's service types listed, .local names resolved, your own services advertised, and a watch that sees them come and go.7 views · 10 downloadsCoAP coap.qwer · 1.0.0CoAP for IoT devices: GET, POST, PUT and DELETE with retransmission and separate responses, block-wise transfers both ways, observed resources whose notifications the program drains, /.well-known/core discovery, and a CoAP server of your own on udp.listen.7 views · 9 downloadsSiemens S7 PLCs s7.qwer · 1.0.0Siemens S7-300, S7-400, S7-1200 and S7-1500 PLCs over ISO-on-TCP, in pure Rust: data blocks, inputs, outputs, merkers, timers and counters read and written as typed values in STEP 7 addresses, many per call split to the PDU, and the CPU's mode and identity.8 views · 11 downloadsTFTP tftp.qwer · 1.0.0TFTP for provisioning phones, switches and PLCs: files fetched and sent with block size, transfer size and timeout options, lost packets asked for again, netascii text; and a small server of your own on a folder, its transfers logged.7 views · 8 downloadsModbus frames modbus.qwer · 1.0.0Modbus RTU and TCP frames built and read without a device: register and coil requests, answers and exceptions explained, CRC-16 and MBAP headers, registers turned into integers, floats and text in any word order and back, and a TCP client for a real PLC or meter.8 views · 11 downloadsNTP time ntp.qwer · 1.0.0How far this server's clock is off: SNTP queries with offset, round trip, stratum and reference, several servers asked at once and the best kept (Kiss-o'-Death and unsynchronized ones set aside), NTP timestamps read, and a small time server on udp.listen.7 views · 10 downloadsThread 0 posts
On Wake-on-LAN: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.