///|
/// DHCP ports.
pub let dhcp_server_port : Int = 67
///|
pub let dhcp_client_port : Int = 68
///|
/// DHCP message types.
pub let msg_discover : Int = 1
///|
pub let msg_offer : Int = 2
///|
pub let msg_request : Int = 3
///|
pub let msg_decline : Int = 4
///|
pub let msg_ack : Int = 5
///|
pub let msg_nak : Int = 6
///|
pub let msg_release : Int = 7
///|
pub let msg_inform : Int = 8
///|
/// DHCP option codes.
pub let opt_subnet_mask : Int = 1
///|
pub let opt_router : Int = 3
///|
pub let opt_dns : Int = 6
///|
pub let opt_hostname : Int = 12
///|
pub let opt_requested_ip : Int = 50
///|
pub let opt_lease_time : Int = 51
///|
pub let opt_message_type : Int = 53
///|
pub let opt_server_id : Int = 54
///|
pub let opt_param_request_list : Int = 55
///|
pub let opt_vendor_class_id : Int = 60
///|
pub let opt_client_id : Int = 61
///|
pub let opt_end : Int = 255
///|
/// DHCP magic cookie.
pub let magic_cookie : Int = 0x63825363
///|
/// BOOTREQUEST / BOOTREPLY opcodes.
pub let boot_request : Int = 1
///|
pub let boot_reply : Int = 2
///|
fn push_u16_be(output : Array[Byte], value : Int) -> Unit {
output.push(((value >> 8) & 0xFF).to_byte())
output.push((value & 0xFF).to_byte())
}
///|
fn push_u32_be(output : Array[Byte], value : Int) -> Unit {
output.push(((value >> 24) & 0xFF).to_byte())
output.push(((value >> 16) & 0xFF).to_byte())
output.push(((value >> 8) & 0xFF).to_byte())
output.push((value & 0xFF).to_byte())
}
///|
fn read_u16_be(data : Bytes, offset : Int) -> Int {
(data[offset].to_int() << 8) + data[offset + 1].to_int()
}
///|
fn read_u32_be(data : Bytes, offset : Int) -> Int {
(data[offset].to_int() << 24) +
(data[offset + 1].to_int() << 16) +
(data[offset + 2].to_int() << 8) +
data[offset + 3].to_int()
}
///|
fn append_bytes(output : Array[Byte], data : Bytes) -> Unit {
for byte in data {
output.push(byte)
}
}
///|
/// A single DHCP option.
pub(all) struct DhcpOption {
code : Int
data : Bytes
} derive(Eq, Debug)
///|
/// DHCP message.
pub(all) struct DhcpMessage {
op : Int // 1=request, 2=reply
htype : Int // 1=ethernet
hlen : Int // 6
hops : Int
xid : Int
secs : Int
flags : Int
ciaddr : Int // client IP
yiaddr : Int // your IP
siaddr : Int // server IP
giaddr : Int // gateway IP
chaddr : Bytes // 16 bytes (MAC + padding)
sname : Bytes // 64 bytes
file : Bytes // 128 bytes
options : Array[DhcpOption]
} derive(Eq, Debug)
///|
/// Create a DHCP Discover message.
pub fn build_discover(
mac : Bytes,
xid : Int,
hostname? : String = "",
) -> DhcpMessage {
let chaddr : Array[Byte] = []
append_bytes(chaddr, mac)
while chaddr.length() < 16 {
chaddr.push(b'\x00')
}
let options : Array[DhcpOption] = []
// Message type = Discover
options.push(DhcpOption::{
code: opt_message_type,
data: Bytes::from_array([b'\x01']),
})
// Client ID (type=1 + MAC)
let cid : Array[Byte] = [b'\x01']
append_bytes(cid, mac)
options.push(DhcpOption::{ code: opt_client_id, data: Bytes::from_array(cid) })
// Hostname
if hostname != "" {
let hbytes = hostname.to_array().map(fn(c) { c.to_int().to_byte() })
options.push(DhcpOption::{
code: opt_hostname,
data: Bytes::from_array(hbytes),
})
}
// Parameter request list
options.push(DhcpOption::{
code: opt_param_request_list,
data: Bytes::from_array([
opt_subnet_mask.to_byte(),
opt_router.to_byte(),
opt_dns.to_byte(),
]),
})
DhcpMessage::{
op: boot_request,
htype: 1,
hlen: 6,
hops: 0,
xid,
secs: 0,
flags: 0x8000, // broadcast flag
ciaddr: 0,
yiaddr: 0,
siaddr: 0,
giaddr: 0,
chaddr: Bytes::from_array(chaddr),
sname: Bytes::default(),
file: Bytes::default(),
options,
}
}
///|
/// Encode a DHCP message to bytes.
pub fn encode_dhcp_message(msg : DhcpMessage) -> Bytes {
let output : Array[Byte] = []
output.push(msg.op.to_byte())
output.push(msg.htype.to_byte())
output.push(msg.hlen.to_byte())
output.push(msg.hops.to_byte())
push_u32_be(output, msg.xid)
push_u16_be(output, msg.secs)
push_u16_be(output, msg.flags)
push_u32_be(output, msg.ciaddr)
push_u32_be(output, msg.yiaddr)
push_u32_be(output, msg.siaddr)
push_u32_be(output, msg.giaddr)
// chaddr - 16 bytes
for i = 0; i < 16; i = i + 1 {
if i < msg.chaddr.length() {
output.push(msg.chaddr[i])
} else {
output.push(b'\x00')
}
}
// sname - 64 bytes
for i = 0; i < 64; i = i + 1 {
if i < msg.sname.length() {
output.push(msg.sname[i])
} else {
output.push(b'\x00')
}
}
// file - 128 bytes
for i = 0; i < 128; i = i + 1 {
if i < msg.file.length() {
output.push(msg.file[i])
} else {
output.push(b'\x00')
}
}
// magic cookie
push_u32_be(output, magic_cookie)
// options
for opt in msg.options {
output.push(opt.code.to_byte())
output.push(opt.data.length().to_byte())
append_bytes(output, opt.data)
}
// end option
output.push(opt_end.to_byte())
Bytes::from_array(output)
}
///|
/// Parse a DHCP message from bytes.
pub fn parse_dhcp_message(data : Bytes) -> DhcpMessage raise @frame.FrameError {
guard data.length() >= 240 else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let op = data[0].to_int()
let htype = data[1].to_int()
let hlen = data[2].to_int()
let hops = data[3].to_int()
let xid = read_u32_be(data, 4)
let secs = read_u16_be(data, 8)
let flags = read_u16_be(data, 10)
let ciaddr = read_u32_be(data, 12)
let yiaddr = read_u32_be(data, 16)
let siaddr = read_u32_be(data, 20)
let giaddr = read_u32_be(data, 24)
let ch : Array[Byte] = []
for i = 28; i < 44; i = i + 1 {
ch.push(data[i])
}
let chaddr = Bytes::from_array(ch)
let sn : Array[Byte] = []
for i = 44; i < 108; i = i + 1 {
sn.push(data[i])
}
let sname = Bytes::from_array(sn)
let fl : Array[Byte] = []
for i = 108; i < 236; i = i + 1 {
fl.push(data[i])
}
let file = Bytes::from_array(fl)
// Parse options after magic cookie (offset 240)
let options : Array[DhcpOption] = []
let mut pos = 240
while pos < data.length() {
let code = data[pos].to_int()
pos = pos + 1
if code == opt_end {
break
}
if code == 0 {
// Pad option
continue
}
guard pos < data.length() else { break }
let len = data[pos].to_int()
pos = pos + 1
guard pos + len <= data.length() else { break }
let d : Array[Byte] = []
for i = 0; i < len; i = i + 1 {
d.push(data[pos + i])
}
options.push(DhcpOption::{ code, data: Bytes::from_array(d) })
pos = pos + len
}
DhcpMessage::{
op,
htype,
hlen,
hops,
xid,
secs,
flags,
ciaddr,
yiaddr,
siaddr,
giaddr,
chaddr,
sname,
file,
options,
}
}
///|
/// Get the DHCP message type from options.
pub fn get_message_type(msg : DhcpMessage) -> Int {
for opt in msg.options {
if opt.code == opt_message_type && opt.data.length() >= 1 {
return opt.data[0].to_int()
}
}
0
}
///|
/// Get a human-readable label for a DHCP message type.
pub fn message_type_label(msg_type : Int) -> String {
match msg_type {
1 => "DISCOVER"
2 => "OFFER"
3 => "REQUEST"
4 => "DECLINE"
5 => "ACK"
6 => "NAK"
7 => "RELEASE"
8 => "INFORM"
_ => "Unknown(" + msg_type.to_string() + ")"
}
}
///|
/// Format DHCP message summary.
pub fn format_dhcp_message(msg : DhcpMessage) -> String {
let msg_type = get_message_type(msg)
let op_str = if msg.op == boot_request { "REQUEST" } else { "REPLY" }
let lines : Array[String] = []
lines.push("DHCP " + message_type_label(msg_type) + " (" + op_str + ")")
lines.push("xid=0x" + @frame.uint32_to_hex(msg.xid))
lines.push("options=" + msg.options.length().to_string())
lines.join("\n")
}