aboutsummaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-10-11 11:34:59 +0100
committerdiogo464 <[email protected]>2025-10-11 11:34:59 +0100
commit521218ce06fbb7bd518eb6a069406936079e3ec2 (patch)
tree862e84ec23175119abb7652e197a4113e7fcc31b /src/main.rs
parent89db26c86bf48a4c527778fc254765a38b7e9085 (diff)
initial working version
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs408
1 files changed, 263 insertions, 145 deletions
diff --git a/src/main.rs b/src/main.rs
index 51bbd77..c179ac0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,4 @@
1#![feature(gethostname)]
1#![feature(cursor_split)] 2#![feature(cursor_split)]
2pub mod dhcp; 3pub mod dhcp;
3pub mod tftp; 4pub mod tftp;
@@ -8,13 +9,273 @@ use std::{
8 net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}, 9 net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket},
9}; 10};
10 11
12use clap::Parser;
11use ipnet::Ipv4Net; 13use ipnet::Ipv4Net;
12 14
13use crate::dhcp::{DhcpOption, DhcpPacket}; 15use crate::dhcp::{DhcpOption, DhcpPacket};
14 16
15const LOCAL_IPV4: Ipv4Addr = Ipv4Addr::new(192, 168, 1, 103); 17const BOOT_FILE_X64_BIOS: &'static str = "netboot.xyz.kpxe";
16const LOCAL_HOSTNAME: &'static str = "Diogos-Air"; 18const BOOT_FILE_X64_EFI: &'static str = "netboot.xyz.efi";
19const BOOT_FILE_A64_EFI: &'static str = "netboot.xyz-arm64.efi";
20const MENU_FILE: &'static str = "menu.ipxe";
17 21
22#[derive(Debug, Parser)]
23struct Cli {
24 #[clap(long)]
25 hostname: Option<String>,
26
27 #[clap(long, default_value = "0.0.0.0")]
28 listen_address: Ipv4Addr,
29
30 #[clap(long, default_value = "67")]
31 dhcp_port: u16,
32
33 #[clap(long, default_value = "4011")]
34 proxy_dhcp_port: u16,
35
36 #[clap(long, default_value = "69")]
37 tftp_port: u16,
38}
39
40struct Context {
41 local_hostname: String,
42 local_address: Ipv4Addr,
43}
44
45fn main() {
46 let cli = Cli::parse();
47
48 let dhcp_sockaddr = SocketAddrV4::new(cli.listen_address, cli.dhcp_port);
49 let pdhcp_sockaddr = SocketAddrV4::new(cli.listen_address, cli.proxy_dhcp_port);
50 let tftp_sockaddr = SocketAddrV4::new(cli.listen_address, cli.tftp_port);
51
52 let hostname = match cli.hostname {
53 Some(hostname) => hostname,
54 None => {
55 let hostname = std::net::hostname().expect("unable to obtain local machine's hostname");
56 hostname
57 .into_string()
58 .expect("unable to convert local machine's hostname to utf-8 string")
59 }
60 };
61 let local_ip_address = if cli.listen_address == Ipv4Addr::UNSPECIFIED {
62 let interfaces = list_network_interfaces().expect("unable to list network interfaces");
63 let mut chosen = None;
64 for interface in interfaces {
65 if interface.address.is_loopback() {
66 continue;
67 }
68 chosen = Some((interface.interface, interface.address));
69 break;
70 }
71
72 let (name, addr) =
73 chosen.expect("unable to find network interface with non-loopback IPv4 address");
74 println!("using local address {} from interface {}", addr, name);
75 addr
76 } else {
77 cli.listen_address
78 };
79
80 println!("local hostname = {hostname}");
81 println!("local address = {local_ip_address}");
82
83 let context = Context {
84 local_hostname: hostname,
85 local_address: local_ip_address,
86 };
87
88 let socket_dhcp = UdpSocket::bind(dhcp_sockaddr).unwrap();
89 socket_dhcp.set_broadcast(true).unwrap();
90 socket_dhcp.set_nonblocking(true).unwrap();
91
92 let socket_pdhcp = UdpSocket::bind(pdhcp_sockaddr).unwrap();
93 socket_pdhcp.set_broadcast(true).unwrap();
94 socket_pdhcp.set_nonblocking(true).unwrap();
95
96 let socket_tftp = UdpSocket::bind(tftp_sockaddr).unwrap();
97 socket_tftp.set_broadcast(false).unwrap();
98 socket_tftp.set_nonblocking(true).unwrap();
99
100 let tftp_filesystem = tftp::StaticFileSystem::new(&[
101 (BOOT_FILE_X64_BIOS, include_bytes!("../netboot.xyz.kpxe")),
102 (BOOT_FILE_X64_EFI, include_bytes!("../netboot.xyz.efi")),
103 (
104 BOOT_FILE_A64_EFI,
105 include_bytes!("../netboot.xyz-arm64.efi"),
106 ),
107 (MENU_FILE, include_bytes!("../menu.ipxe")),
108 ]);
109 let mut tftp_server = tftp::Server::default();
110
111 loop {
112 let mut buf = [0u8; 1500];
113
114 if let Ok((n, addr)) = socket_dhcp.recv_from(&mut buf) {
115 println!("Received {} bytes from {} on port 67", n, addr);
116 handle_packet(&context, &buf[..n], &socket_dhcp);
117 }
118
119 if let Ok((n, addr)) = socket_pdhcp.recv_from(&mut buf) {
120 println!("Received {} bytes from {} on port 4011", n, addr);
121 handle_packet_4011(&context, &buf[..n], &socket_pdhcp, addr);
122 }
123
124 if let Ok((n, addr)) = socket_tftp.recv_from(&mut buf) {
125 println!("Received {} bytes from {} on port 4011", n, addr);
126 match tftp_server.process(&tftp_filesystem, addr, &buf) {
127 tftp::ServerCommand::Send(tftp_packet) => {
128 let mut output = Vec::default();
129 tftp_packet.write(&mut output).unwrap();
130 socket_tftp.send_to(&output, addr).unwrap();
131 }
132 tftp::ServerCommand::Ignore => {}
133 }
134 }
135
136 std::thread::sleep(std::time::Duration::from_millis(1));
137 }
138}
139
140fn handle_packet(context: &Context, buf: &[u8], socket: &UdpSocket) {
141 let packet = match dhcp::parse_packet(buf) {
142 Ok(packet) => packet,
143 Err(err) => {
144 eprintln!("failed to parse DHCP packet: {err}");
145 return;
146 }
147 };
148
149 println!("Parsed DHCP packet: XID={:08x}", packet.xid);
150
151 // Check if it's a PXE client and extract client UUID
152 let mut pxe_class = None;
153 let mut client_uuid = None;
154 let mut is_ipxe = false;
155
156 for option in &packet.options {
157 match option {
158 DhcpOption::VendorClassIdentifier(vendor_class) => {
159 if let Ok(class) = dhcp::PxeClassIdentifier::try_from(vendor_class.as_slice()) {
160 println!("{class}");
161 pxe_class = Some(class);
162 }
163 }
164 DhcpOption::UserClassInformation(user_class) => {
165 if user_class == dhcp::USER_CLASS_IPXE {
166 is_ipxe = true;
167 }
168 }
169 DhcpOption::ClientMachineIdentifier(uuid) => {
170 client_uuid = Some(uuid.clone());
171 }
172 _ => {}
173 }
174 }
175
176 let pxe_client_class = match pxe_class {
177 Some(dhcp::PxeClassIdentifier::Client(class)) => class,
178 _ => {
179 println!("Not a PXE client, ignoring");
180 return;
181 }
182 };
183
184 println!("Responding to PXE client with DHCPOFFER");
185 let mut response_buf = Vec::default();
186 let response = DhcpPacket::new_boot(
187 packet.xid,
188 packet.chaddr,
189 client_uuid,
190 context.local_address,
191 context.local_hostname.clone(),
192 match is_ipxe {
193 true => MENU_FILE.to_string(),
194 false => match pxe_client_class.architecture {
195 dhcp::SystemArchitecture::IntelX86pc => BOOT_FILE_X64_BIOS.to_string(),
196 dhcp::SystemArchitecture::EfiARM64 => BOOT_FILE_A64_EFI.to_string(),
197 dhcp::SystemArchitecture::EfiX86_64 | dhcp::SystemArchitecture::EfiBC => {
198 BOOT_FILE_X64_EFI.to_string()
199 }
200 _ => {
201 eprintln!(
202 "unsupported architecture {:?}",
203 pxe_client_class.architecture
204 );
205 return;
206 }
207 },
208 },
209 );
210 response.write(&mut response_buf).unwrap();
211 socket
212 .send_to(&response_buf, SocketAddrV4::new(Ipv4Addr::BROADCAST, 68))
213 .unwrap();
214}
215
216fn handle_packet_4011(context: &Context, buf: &[u8], socket: &UdpSocket, sender_addr: SocketAddr) {
217 let packet = match dhcp::parse_packet(buf) {
218 Ok(packet) => packet,
219 Err(err) => {
220 println!("Failed to parse packet on 4011: {}", err);
221 return;
222 }
223 };
224
225 println!("Parsed DHCP packet on 4011: XID={:08x}", packet.xid);
226
227 // Extract client UUID
228 let mut client_uuid = None;
229 for option in &packet.options {
230 if let DhcpOption::ClientMachineIdentifier(uuid) = option {
231 client_uuid = Some(uuid.clone());
232 break;
233 }
234 }
235
236 let mut client_class = None;
237 for option in &packet.options {
238 if let DhcpOption::VendorClassIdentifier(vendor_class) = option {
239 if let Ok(dhcp::PxeClassIdentifier::Client(class)) =
240 dhcp::PxeClassIdentifier::try_from(vendor_class.as_slice())
241 {
242 println!("{class}");
243 client_class = Some(class);
244 }
245 }
246 }
247 let client_class = match client_class {
248 Some(class) => class,
249 None => return,
250 };
251
252 let file = match client_class.architecture {
253 dhcp::SystemArchitecture::IntelX86pc => BOOT_FILE_X64_BIOS.to_string(),
254 dhcp::SystemArchitecture::EfiARM64 => BOOT_FILE_A64_EFI.to_string(),
255 dhcp::SystemArchitecture::EfiX86_64 | dhcp::SystemArchitecture::EfiBC => {
256 BOOT_FILE_X64_EFI.to_string()
257 }
258 _ => {
259 eprintln!("unsupported architecture {:?}", client_class.architecture);
260 return;
261 }
262 };
263
264 println!("Responding with DHCPACK");
265 let mut response_buf = Vec::default();
266 let response = DhcpPacket::new_boot_ack(
267 packet.xid,
268 packet.chaddr,
269 client_uuid,
270 context.local_address,
271 context.local_hostname.clone(),
272 file,
273 );
274 response.write(&mut response_buf).unwrap();
275 socket.send_to(&response_buf, sender_addr).unwrap();
276}
277
278#[allow(unused)]
18#[derive(Debug, Clone)] 279#[derive(Debug, Clone)]
19struct InterfaceAddr { 280struct InterfaceAddr {
20 interface: String, 281 interface: String,
@@ -68,146 +329,3 @@ fn list_network_interfaces() -> Result<Vec<InterfaceAddr>> {
68 Ok(interfaces) 329 Ok(interfaces)
69 } 330 }
70} 331}
71
72fn main() {
73 let socket67 = UdpSocket::bind("0.0.0.0:67").unwrap();
74 socket67.set_broadcast(true).unwrap();
75 socket67.set_nonblocking(true).unwrap();
76
77 let socket4011 = UdpSocket::bind("0.0.0.0:4011").unwrap();
78 socket4011.set_broadcast(true).unwrap();
79 socket4011.set_nonblocking(true).unwrap();
80
81 std::thread::spawn(|| {
82 tftp::serve("tftp").unwrap();
83 });
84
85 loop {
86 let mut buf = [0u8; 1500];
87
88 // Try port 67 first
89 if let Ok((n, addr)) = socket67.recv_from(&mut buf) {
90 println!("Received {} bytes from {} on port 67", n, addr);
91 handle_packet(&buf[..n], &socket67);
92 } else if let Ok((n, addr)) = socket4011.recv_from(&mut buf) {
93 println!("Received {} bytes from {} on port 4011", n, addr);
94 handle_packet_4011(&buf[..n], &socket4011, addr);
95 } else {
96 std::thread::sleep(std::time::Duration::from_millis(10));
97 }
98 }
99}
100
101fn handle_packet(buf: &[u8], socket: &UdpSocket) {
102 match dhcp::parse_packet(buf) {
103 Ok(packet) => {
104 println!("Parsed DHCP packet: XID={:08x}", packet.xid);
105
106 // Check if it's a PXE client and extract client UUID
107 let mut is_pxe = false;
108 let mut client_uuid = None;
109 let mut is_ipxe = false;
110
111 for option in &packet.options {
112 match option {
113 DhcpOption::VendorClassIdentifier(vendor_class) => {
114 println!("Vendor class: {}", vendor_class);
115 if vendor_class.contains("PXEClient") {
116 is_pxe = true;
117 }
118 }
119 DhcpOption::UserClassInformation(user_class) => {
120 println!("User class: {}", user_class);
121 is_ipxe = true;
122 }
123 DhcpOption::Unknown { code: 97, data } => {
124 println!("Found client machine identifier");
125 client_uuid = Some(data.clone());
126 }
127 _ => {}
128 }
129 }
130
131 if is_pxe {
132 println!("Responding to PXE client with DHCPOFFER");
133 let mut response_buf = Vec::default();
134 let response = DhcpPacket::new_boot(
135 packet.xid,
136 packet.chaddr,
137 client_uuid.unwrap(),
138 LOCAL_IPV4,
139 LOCAL_HOSTNAME.to_string(),
140 match is_ipxe {
141 true => "test.ipxe".to_string(),
142 false => "ipxe.efi".to_string(),
143 },
144 );
145 response.write(&mut response_buf).unwrap();
146 socket
147 .send_to(&response_buf, SocketAddrV4::new(Ipv4Addr::BROADCAST, 68))
148 .unwrap();
149 } else {
150 println!("Not a PXE client, ignoring");
151 }
152 }
153 Err(e) => {
154 println!("Failed to parse packet: {}", e);
155 }
156 }
157}
158
159fn handle_packet_4011(buf: &[u8], socket: &UdpSocket, sender_addr: SocketAddr) {
160 match dhcp::parse_packet(buf) {
161 Ok(packet) => {
162 println!("Parsed DHCP packet on 4011: XID={:08x}", packet.xid);
163
164 // Extract client UUID
165 let mut client_uuid = None;
166 for option in &packet.options {
167 if let DhcpOption::Unknown { code: 97, data } = option {
168 client_uuid = Some(data.clone());
169 break;
170 }
171 }
172
173 println!("Responding with DHCPACK");
174 let mut response_buf = Vec::default();
175 let response = DhcpPacket::new_boot_ack(
176 packet.xid,
177 packet.chaddr,
178 client_uuid.unwrap(),
179 LOCAL_IPV4,
180 LOCAL_HOSTNAME.to_string(),
181 "ipxe.efi".to_string(),
182 );
183 response.write(&mut response_buf).unwrap();
184 socket.send_to(&response_buf, sender_addr).unwrap();
185 }
186 Err(e) => {
187 println!("Failed to parse packet on 4011: {}", e);
188 }
189 }
190}
191
192const DHCP_PACKET_PAYLOAD: &'static [u8] = &[
193 0x1, 0x1, 0x6, 0x0, 0xf1, 0x25, 0x7c, 0x21, 0x0, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
194 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2b, 0x67, 0x3f, 0xda, 0x70, 0x0, 0x0,
195 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
196 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
197 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
198 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
199 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
200 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
201 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
202 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
203 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
204 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
205 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x63, 0x82, 0x53, 0x63, 0x35, 0x1, 0x1, 0x39,
206 0x2, 0x5, 0xc0, 0x37, 0x23, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0xc, 0xd, 0xf, 0x11, 0x12, 0x16,
207 0x17, 0x1c, 0x28, 0x29, 0x2a, 0x2b, 0x32, 0x33, 0x36, 0x3a, 0x3b, 0x3c, 0x42, 0x43, 0x61, 0x80,
208 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x61, 0x11, 0x0, 0xcc, 0xfc, 0x32, 0x1b, 0xce, 0x2a,
209 0xb2, 0x11, 0xa8, 0x5c, 0xb1, 0xac, 0x38, 0x38, 0x10, 0xf, 0x5e, 0x3, 0x1, 0x3, 0x10, 0x5d,
210 0x2, 0x0, 0x7, 0x3c, 0x20, 0x50, 0x58, 0x45, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3a, 0x41,
211 0x72, 0x63, 0x68, 0x3a, 0x30, 0x30, 0x30, 0x30, 0x37, 0x3a, 0x55, 0x4e, 0x44, 0x49, 0x3a, 0x30,
212 0x30, 0x33, 0x30, 0x31, 0x36, 0xff,
213];