diff options
| author | diogo464 <[email protected]> | 2025-09-25 17:32:25 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-09-25 17:32:25 +0100 |
| commit | 5bf4135847954bec6b8c90ef2996439783ae8056 (patch) | |
| tree | 4f0bef15b7ee3d1c03333962a73b8f8a75e9a335 | |
booting debian
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.lock | 7 | ||||
| -rw-r--r-- | Cargo.toml | 6 | ||||
| -rw-r--r-- | src/main.rs | 756 | ||||
| -rw-r--r-- | tftp/ipxe.efi | bin | 0 -> 1044480 bytes | |||
| -rw-r--r-- | tftp/test.ipxe | 25 |
6 files changed, 795 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore | |||
| @@ -0,0 +1 @@ | |||
| /target | |||
diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..5a09a45 --- /dev/null +++ b/Cargo.lock | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | # This file is automatically @generated by Cargo. | ||
| 2 | # It is not intended for manual editing. | ||
| 3 | version = 4 | ||
| 4 | |||
| 5 | [[package]] | ||
| 6 | name = "netiso" | ||
| 7 | version = "0.1.0" | ||
diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b0abcfb --- /dev/null +++ b/Cargo.toml | |||
| @@ -0,0 +1,6 @@ | |||
| 1 | [package] | ||
| 2 | name = "netiso" | ||
| 3 | version = "0.1.0" | ||
| 4 | edition = "2024" | ||
| 5 | |||
| 6 | [dependencies] | ||
diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..87ea283 --- /dev/null +++ b/src/main.rs | |||
| @@ -0,0 +1,756 @@ | |||
| 1 | use std::io::{BufRead, Cursor, Read, Result, Write}; | ||
| 2 | use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}; | ||
| 3 | |||
| 4 | const FLAG_BROADCAST: u16 = 1 << 15; | ||
| 5 | |||
| 6 | const OPTION_CODE_PAD: u8 = 0; | ||
| 7 | const OPTION_CODE_END: u8 = 255; | ||
| 8 | const OPTION_CODE_VENDOR_CLASS_IDENTIFIER: u8 = 60; | ||
| 9 | const OPTION_CODE_USER_CLASS_INFORMATION: u8 = 77; | ||
| 10 | |||
| 11 | const MAGIC_COOKIE: [u8; 4] = [0x63, 0x82, 0x53, 0x63]; | ||
| 12 | |||
| 13 | //const BOOT_FILE_NAME: &[u8] = b"pxelinux.0"; | ||
| 14 | //const BOOT_FILE_NAME: &[u8] = b"debian-installer/amd64/bootnetx64.efi"; | ||
| 15 | const BOOT_FILE_NAME: &[u8] = b"ipxe.efi"; | ||
| 16 | const BOOT_FILE_NAME_IPXE: &[u8] = b"test.ipxe"; | ||
| 17 | |||
| 18 | const LOCAL_IPV4: Ipv4Addr = Ipv4Addr::new(192, 168, 2, 184); | ||
| 19 | |||
| 20 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 21 | enum BootOp { | ||
| 22 | Request, | ||
| 23 | Reply, | ||
| 24 | } | ||
| 25 | |||
| 26 | impl BootOp { | ||
| 27 | pub const OP_REQUEST: u8 = 1; | ||
| 28 | pub const OP_REPLY: u8 = 2; | ||
| 29 | } | ||
| 30 | |||
| 31 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 32 | enum HardwareType { | ||
| 33 | Ethernet, | ||
| 34 | } | ||
| 35 | |||
| 36 | impl HardwareType { | ||
| 37 | pub const TYPE_ETHER: u8 = 1; | ||
| 38 | pub const LEN_ETHER: u8 = 6; | ||
| 39 | } | ||
| 40 | |||
| 41 | #[derive(Debug, Clone)] | ||
| 42 | enum DhcpOption { | ||
| 43 | Pad, | ||
| 44 | End, | ||
| 45 | VendorClassIdentifier(String), | ||
| 46 | UserClassInformation(String), | ||
| 47 | Unknown { code: u8, data: Vec<u8> }, | ||
| 48 | } | ||
| 49 | |||
| 50 | #[derive(Debug)] | ||
| 51 | struct DhcpPacket { | ||
| 52 | op: BootOp, | ||
| 53 | htype: HardwareType, | ||
| 54 | hlen: u8, | ||
| 55 | hops: u8, // should be zero | ||
| 56 | xid: u32, | ||
| 57 | secs: u16, | ||
| 58 | flags: u16, | ||
| 59 | ciaddr: Ipv4Addr, | ||
| 60 | yiaddr: Ipv4Addr, | ||
| 61 | siaddr: Ipv4Addr, | ||
| 62 | giaddr: Ipv4Addr, | ||
| 63 | chaddr: [u8; 16], | ||
| 64 | sname: [u8; 64], | ||
| 65 | file: [u8; 128], | ||
| 66 | options: Vec<DhcpOption>, | ||
| 67 | } | ||
| 68 | |||
| 69 | fn read_u8(cursor: &mut Cursor<&[u8]>) -> Result<u8> { | ||
| 70 | let mut buf = [0u8; 1]; | ||
| 71 | cursor.read_exact(&mut buf)?; | ||
| 72 | Ok(buf[0]) | ||
| 73 | } | ||
| 74 | |||
| 75 | fn read_u16(cursor: &mut Cursor<&[u8]>) -> Result<u16> { | ||
| 76 | let mut buf = [0u8; 2]; | ||
| 77 | cursor.read_exact(&mut buf)?; | ||
| 78 | Ok(u16::from_be_bytes(buf)) | ||
| 79 | } | ||
| 80 | |||
| 81 | fn read_u32(cursor: &mut Cursor<&[u8]>) -> Result<u32> { | ||
| 82 | let mut buf = [0u8; 4]; | ||
| 83 | cursor.read_exact(&mut buf)?; | ||
| 84 | Ok(u32::from_be_bytes(buf)) | ||
| 85 | } | ||
| 86 | |||
| 87 | fn read_arr<const N: usize>(cursor: &mut Cursor<&[u8]>) -> Result<[u8; N]> { | ||
| 88 | let mut buf = [0u8; N]; | ||
| 89 | cursor.read_exact(&mut buf)?; | ||
| 90 | Ok(buf) | ||
| 91 | } | ||
| 92 | |||
| 93 | fn read_null_terminated_vec(cursor: &mut Cursor<&[u8]>) -> Result<Vec<u8>> { | ||
| 94 | let mut buf = Vec::default(); | ||
| 95 | cursor.read_until(0, &mut buf)?; | ||
| 96 | buf.pop(); | ||
| 97 | Ok(buf) | ||
| 98 | } | ||
| 99 | |||
| 100 | fn read_null_terminated_string(cursor: &mut Cursor<&[u8]>) -> Result<String> { | ||
| 101 | let buf = read_null_terminated_vec(cursor)?; | ||
| 102 | Ok(String::from_utf8(buf).unwrap()) | ||
| 103 | } | ||
| 104 | |||
| 105 | fn read_len8_prefixed_vec(cursor: &mut Cursor<&[u8]>) -> Result<Vec<u8>> { | ||
| 106 | let len = read_u8(cursor)?; | ||
| 107 | let mut buf = vec![0u8; len as usize]; | ||
| 108 | cursor.read_exact(&mut buf)?; | ||
| 109 | Ok(buf) | ||
| 110 | } | ||
| 111 | |||
| 112 | fn read_len8_prefixed_string(cursor: &mut Cursor<&[u8]>) -> Result<String> { | ||
| 113 | let buf = read_len8_prefixed_vec(cursor)?; | ||
| 114 | Ok(String::from_utf8(buf).unwrap()) | ||
| 115 | } | ||
| 116 | |||
| 117 | fn read_ipv4(cursor: &mut Cursor<&[u8]>) -> Result<Ipv4Addr> { | ||
| 118 | Ok(Ipv4Addr::from_octets(read_arr(cursor)?)) | ||
| 119 | } | ||
| 120 | |||
| 121 | fn read_op(cursor: &mut Cursor<&[u8]>) -> Result<BootOp> { | ||
| 122 | let v = read_u8(cursor)?; | ||
| 123 | match v { | ||
| 124 | BootOp::OP_REQUEST => Ok(BootOp::Request), | ||
| 125 | BootOp::OP_REPLY => Ok(BootOp::Reply), | ||
| 126 | _ => Err(std::io::Error::new( | ||
| 127 | std::io::ErrorKind::InvalidData, | ||
| 128 | "invalid boot op", | ||
| 129 | )), | ||
| 130 | } | ||
| 131 | } | ||
| 132 | |||
| 133 | fn read_htype(cursor: &mut Cursor<&[u8]>) -> Result<HardwareType> { | ||
| 134 | match read_u8(cursor)? { | ||
| 135 | HardwareType::TYPE_ETHER => Ok(HardwareType::Ethernet), | ||
| 136 | _ => Err(std::io::Error::new( | ||
| 137 | std::io::ErrorKind::InvalidData, | ||
| 138 | "invalid hardware type", | ||
| 139 | )), | ||
| 140 | } | ||
| 141 | } | ||
| 142 | |||
| 143 | fn read_option(cursor: &mut Cursor<&[u8]>) -> Result<DhcpOption> { | ||
| 144 | let code = read_u8(cursor)?; | ||
| 145 | Ok(match code { | ||
| 146 | OPTION_CODE_PAD => DhcpOption::Pad, | ||
| 147 | OPTION_CODE_END => DhcpOption::End, | ||
| 148 | OPTION_CODE_VENDOR_CLASS_IDENTIFIER => { | ||
| 149 | DhcpOption::VendorClassIdentifier(read_len8_prefixed_string(cursor)?) | ||
| 150 | } | ||
| 151 | OPTION_CODE_USER_CLASS_INFORMATION => { | ||
| 152 | DhcpOption::UserClassInformation(read_len8_prefixed_string(cursor)?) | ||
| 153 | } | ||
| 154 | _ => { | ||
| 155 | let len = read_u8(cursor)?; | ||
| 156 | let mut data = vec![0u8; usize::from(len)]; | ||
| 157 | cursor.read_exact(&mut data)?; | ||
| 158 | DhcpOption::Unknown { code, data } | ||
| 159 | } | ||
| 160 | }) | ||
| 161 | } | ||
| 162 | |||
| 163 | fn parse_packet(buf: &[u8]) -> Result<DhcpPacket> { | ||
| 164 | let mut cursor = Cursor::new(buf); | ||
| 165 | let mut packet = DhcpPacket { | ||
| 166 | op: read_op(&mut cursor)?, | ||
| 167 | htype: read_htype(&mut cursor)?, | ||
| 168 | hlen: read_u8(&mut cursor)?, | ||
| 169 | hops: read_u8(&mut cursor)?, | ||
| 170 | xid: read_u32(&mut cursor)?, | ||
| 171 | secs: read_u16(&mut cursor)?, | ||
| 172 | flags: read_u16(&mut cursor)?, | ||
| 173 | ciaddr: read_ipv4(&mut cursor)?, | ||
| 174 | yiaddr: read_ipv4(&mut cursor)?, | ||
| 175 | siaddr: read_ipv4(&mut cursor)?, | ||
| 176 | giaddr: read_ipv4(&mut cursor)?, | ||
| 177 | chaddr: read_arr(&mut cursor)?, | ||
| 178 | sname: read_arr(&mut cursor)?, | ||
| 179 | file: read_arr(&mut cursor)?, | ||
| 180 | options: Default::default(), | ||
| 181 | }; | ||
| 182 | |||
| 183 | let magic = read_arr::<4>(&mut cursor)?; | ||
| 184 | assert_eq!(magic, MAGIC_COOKIE); | ||
| 185 | |||
| 186 | while cursor.position() < buf.len() as u64 { | ||
| 187 | let option = read_option(&mut cursor)?; | ||
| 188 | packet.options.push(option); | ||
| 189 | } | ||
| 190 | |||
| 191 | Ok(packet) | ||
| 192 | } | ||
| 193 | |||
| 194 | fn write_buf(writer: &mut Vec<u8>, buf: &[u8]) -> Result<()> { | ||
| 195 | writer.write_all(buf) | ||
| 196 | } | ||
| 197 | |||
| 198 | fn write_u8(writer: &mut Vec<u8>, v: u8) -> Result<()> { | ||
| 199 | write_buf(writer, &[v]) | ||
| 200 | } | ||
| 201 | |||
| 202 | fn write_u16(writer: &mut Vec<u8>, v: u16) -> Result<()> { | ||
| 203 | let buf = u16::to_be_bytes(v); | ||
| 204 | write_buf(writer, &buf) | ||
| 205 | } | ||
| 206 | |||
| 207 | fn write_u32(writer: &mut Vec<u8>, v: u32) -> Result<()> { | ||
| 208 | let buf = u32::to_be_bytes(v); | ||
| 209 | write_buf(writer, &buf) | ||
| 210 | } | ||
| 211 | |||
| 212 | fn write_ipv4(writer: &mut Vec<u8>, v: Ipv4Addr) -> Result<()> { | ||
| 213 | write_buf(writer, &v.octets()) | ||
| 214 | } | ||
| 215 | |||
| 216 | fn write_boot_packet( | ||
| 217 | xid: u32, | ||
| 218 | chaddr: [u8; 16], | ||
| 219 | client_uuid: Option<Vec<u8>>, | ||
| 220 | ipxe: bool, | ||
| 221 | ) -> Result<Vec<u8>> { | ||
| 222 | let mut writer = Vec::default(); | ||
| 223 | write_u8(&mut writer, BootOp::OP_REPLY)?; | ||
| 224 | write_u8(&mut writer, HardwareType::TYPE_ETHER)?; | ||
| 225 | write_u8(&mut writer, 6)?; | ||
| 226 | write_u8(&mut writer, 0)?; | ||
| 227 | write_u32(&mut writer, xid)?; | ||
| 228 | write_u16(&mut writer, 0)?; | ||
| 229 | write_u16(&mut writer, FLAG_BROADCAST)?; | ||
| 230 | write_ipv4(&mut writer, Ipv4Addr::UNSPECIFIED)?; // ciaddr | ||
| 231 | write_ipv4(&mut writer, Ipv4Addr::UNSPECIFIED)?; // yiaddr | ||
| 232 | write_ipv4(&mut writer, LOCAL_IPV4)?; // siaddr (TFTP server) | ||
| 233 | write_ipv4(&mut writer, Ipv4Addr::UNSPECIFIED)?; // giaddr | ||
| 234 | write_buf(&mut writer, &chaddr)?; | ||
| 235 | write_buf(&mut writer, &[0u8; 64])?; | ||
| 236 | write_buf(&mut writer, &[0u8; 128])?; | ||
| 237 | write_buf(&mut writer, &MAGIC_COOKIE)?; | ||
| 238 | |||
| 239 | // Option 53: DHCP Message Type (DHCPOFFER) | ||
| 240 | write_u8(&mut writer, 53)?; | ||
| 241 | write_u8(&mut writer, 1)?; | ||
| 242 | write_u8(&mut writer, 2)?; // DHCPOFFER | ||
| 243 | |||
| 244 | // Option 54: DHCP Server Identifier | ||
| 245 | write_u8(&mut writer, 54)?; | ||
| 246 | write_u8(&mut writer, 4)?; | ||
| 247 | write_ipv4(&mut writer, LOCAL_IPV4)?; // Your server IP | ||
| 248 | |||
| 249 | // Option 60: Vendor Class Identifier | ||
| 250 | const PXE_CLIENT: &[u8] = b"PXEClient"; | ||
| 251 | write_u8(&mut writer, 60)?; | ||
| 252 | write_u8(&mut writer, PXE_CLIENT.len() as u8)?; | ||
| 253 | write_buf(&mut writer, PXE_CLIENT)?; | ||
| 254 | |||
| 255 | // Option 97: Client Machine Identifier (UUID from client) | ||
| 256 | if let Some(uuid) = client_uuid { | ||
| 257 | write_u8(&mut writer, 97)?; | ||
| 258 | write_u8(&mut writer, uuid.len() as u8)?; | ||
| 259 | write_buf(&mut writer, &uuid)?; | ||
| 260 | } | ||
| 261 | |||
| 262 | // TFTP server name | ||
| 263 | const SERVER_NAME: &[u8] = b"diogos-air"; | ||
| 264 | write_u8(&mut writer, 66)?; | ||
| 265 | write_u8(&mut writer, SERVER_NAME.len() as u8)?; | ||
| 266 | write_buf(&mut writer, SERVER_NAME)?; | ||
| 267 | |||
| 268 | write_u8(&mut writer, 67)?; | ||
| 269 | if !ipxe { | ||
| 270 | write_u8(&mut writer, BOOT_FILE_NAME.len() as u8)?; | ||
| 271 | write_buf(&mut writer, BOOT_FILE_NAME)?; | ||
| 272 | } else { | ||
| 273 | write_u8(&mut writer, BOOT_FILE_NAME_IPXE.len() as u8)?; | ||
| 274 | write_buf(&mut writer, BOOT_FILE_NAME_IPXE)?; | ||
| 275 | } | ||
| 276 | |||
| 277 | // Option 255: End | ||
| 278 | write_u8(&mut writer, 255)?; | ||
| 279 | |||
| 280 | Ok(writer) | ||
| 281 | } | ||
| 282 | |||
| 283 | fn write_boot_ack(xid: u32, chaddr: [u8; 16], client_uuid: Option<Vec<u8>>) -> Result<Vec<u8>> { | ||
| 284 | let mut writer = Vec::default(); | ||
| 285 | write_u8(&mut writer, BootOp::OP_REPLY)?; | ||
| 286 | write_u8(&mut writer, HardwareType::TYPE_ETHER)?; | ||
| 287 | write_u8(&mut writer, 6)?; | ||
| 288 | write_u8(&mut writer, 0)?; | ||
| 289 | write_u32(&mut writer, xid)?; | ||
| 290 | write_u16(&mut writer, 0)?; | ||
| 291 | write_u16(&mut writer, 0)?; | ||
| 292 | write_ipv4(&mut writer, Ipv4Addr::UNSPECIFIED)?; // ciaddr | ||
| 293 | write_ipv4(&mut writer, Ipv4Addr::UNSPECIFIED)?; // yiaddr | ||
| 294 | write_ipv4(&mut writer, Ipv4Addr::UNSPECIFIED)?; // siaddr (TFTP server) | ||
| 295 | write_ipv4(&mut writer, Ipv4Addr::UNSPECIFIED)?; // giaddr | ||
| 296 | write_buf(&mut writer, &chaddr)?; | ||
| 297 | write_buf(&mut writer, &[0u8; 64])?; | ||
| 298 | write_buf(&mut writer, &[0u8; 128])?; | ||
| 299 | write_buf(&mut writer, &MAGIC_COOKIE)?; | ||
| 300 | |||
| 301 | // Option 53: DHCP Message Type (DHCPOFFER) | ||
| 302 | write_u8(&mut writer, 53)?; | ||
| 303 | write_u8(&mut writer, 1)?; | ||
| 304 | write_u8(&mut writer, 5)?; // DHCPACK | ||
| 305 | |||
| 306 | // Option 54: DHCP Server Identifier | ||
| 307 | write_u8(&mut writer, 54)?; | ||
| 308 | write_u8(&mut writer, 4)?; | ||
| 309 | write_ipv4(&mut writer, LOCAL_IPV4)?; // Your server IP | ||
| 310 | |||
| 311 | // Option 60: Vendor Class Identifier | ||
| 312 | const PXE_CLIENT: &[u8] = b"PXEClient"; | ||
| 313 | write_u8(&mut writer, 60)?; | ||
| 314 | write_u8(&mut writer, PXE_CLIENT.len() as u8)?; | ||
| 315 | write_buf(&mut writer, PXE_CLIENT)?; | ||
| 316 | |||
| 317 | // Option 97: Client Machine Identifier (UUID from client) | ||
| 318 | if let Some(uuid) = client_uuid { | ||
| 319 | write_u8(&mut writer, 97)?; | ||
| 320 | write_u8(&mut writer, uuid.len() as u8)?; | ||
| 321 | write_buf(&mut writer, &uuid)?; | ||
| 322 | } | ||
| 323 | |||
| 324 | // TFTP server name | ||
| 325 | const SERVER_NAME: &[u8] = b"diogos-air"; | ||
| 326 | write_u8(&mut writer, 66)?; | ||
| 327 | write_u8(&mut writer, SERVER_NAME.len() as u8)?; | ||
| 328 | write_buf(&mut writer, SERVER_NAME)?; | ||
| 329 | |||
| 330 | // TFTP file name | ||
| 331 | write_u8(&mut writer, 67)?; | ||
| 332 | write_u8(&mut writer, BOOT_FILE_NAME.len() as u8)?; | ||
| 333 | write_buf(&mut writer, BOOT_FILE_NAME)?; | ||
| 334 | |||
| 335 | write_u8(&mut writer, 71)?; | ||
| 336 | write_u8(&mut writer, 4)?; | ||
| 337 | write_buf(&mut writer, &[0, 0, 0, 0])?; | ||
| 338 | |||
| 339 | // Option 255: End | ||
| 340 | write_u8(&mut writer, 255)?; | ||
| 341 | |||
| 342 | Ok(writer) | ||
| 343 | } | ||
| 344 | |||
| 345 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 346 | struct InvalidTftpOp(u16); | ||
| 347 | |||
| 348 | impl std::fmt::Display for InvalidTftpOp { | ||
| 349 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
| 350 | write!(f, "invalid tftp opcode '{}'", self.0) | ||
| 351 | } | ||
| 352 | } | ||
| 353 | |||
| 354 | impl std::error::Error for InvalidTftpOp {} | ||
| 355 | |||
| 356 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 357 | enum TftpOp { | ||
| 358 | ReadRequest, | ||
| 359 | WriteRequest, | ||
| 360 | Data, | ||
| 361 | Ack, | ||
| 362 | Error, | ||
| 363 | Oack, | ||
| 364 | } | ||
| 365 | |||
| 366 | impl Into<u16> for TftpOp { | ||
| 367 | fn into(self) -> u16 { | ||
| 368 | match self { | ||
| 369 | TftpOp::ReadRequest => 1, | ||
| 370 | TftpOp::WriteRequest => 2, | ||
| 371 | TftpOp::Data => 3, | ||
| 372 | TftpOp::Ack => 4, | ||
| 373 | TftpOp::Error => 5, | ||
| 374 | TftpOp::Oack => 6, | ||
| 375 | } | ||
| 376 | } | ||
| 377 | } | ||
| 378 | |||
| 379 | impl TryFrom<u16> for TftpOp { | ||
| 380 | type Error = InvalidTftpOp; | ||
| 381 | |||
| 382 | fn try_from(value: u16) -> std::result::Result<Self, InvalidTftpOp> { | ||
| 383 | match value { | ||
| 384 | 1 => Ok(Self::ReadRequest), | ||
| 385 | 2 => Ok(Self::WriteRequest), | ||
| 386 | 3 => Ok(Self::Data), | ||
| 387 | 4 => Ok(Self::Ack), | ||
| 388 | 5 => Ok(Self::Error), | ||
| 389 | 6 => Ok(Self::Oack), | ||
| 390 | unknown => Err(InvalidTftpOp(unknown)), | ||
| 391 | } | ||
| 392 | } | ||
| 393 | } | ||
| 394 | |||
| 395 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 396 | enum TftpMode { | ||
| 397 | NetAscii, | ||
| 398 | Octet, | ||
| 399 | Mail, | ||
| 400 | } | ||
| 401 | |||
| 402 | #[derive(Debug)] | ||
| 403 | struct TftpRequestPacket { | ||
| 404 | filename: String, | ||
| 405 | mode: String, | ||
| 406 | tsize: Option<u64>, | ||
| 407 | blksize: Option<u64>, | ||
| 408 | } | ||
| 409 | |||
| 410 | #[derive(Debug)] | ||
| 411 | struct TftpDataPacket { | ||
| 412 | block: u16, | ||
| 413 | data: Vec<u8>, | ||
| 414 | } | ||
| 415 | |||
| 416 | #[derive(Debug)] | ||
| 417 | struct TftpAckPacket { | ||
| 418 | block: u16, | ||
| 419 | } | ||
| 420 | |||
| 421 | #[derive(Debug)] | ||
| 422 | struct TftpErrorPacket { | ||
| 423 | code: u16, | ||
| 424 | message: String, | ||
| 425 | } | ||
| 426 | |||
| 427 | fn tftp_request_packet_parse(cursor: &mut Cursor<&[u8]>) -> Result<TftpRequestPacket> { | ||
| 428 | let filename = read_null_terminated_string(cursor)?; | ||
| 429 | let mode = read_null_terminated_string(cursor)?; | ||
| 430 | let mut tsize = None; | ||
| 431 | let mut blksize = None; | ||
| 432 | while let Ok(opt_name) = read_null_terminated_string(cursor) { | ||
| 433 | if opt_name.is_empty() { | ||
| 434 | break; | ||
| 435 | } | ||
| 436 | let opt_data = read_null_terminated_string(cursor)?; | ||
| 437 | match opt_name.as_str() { | ||
| 438 | "tsize" => tsize = Some(opt_data.parse::<u64>().unwrap()), | ||
| 439 | "blksize" => blksize = Some(opt_data.parse::<u64>().unwrap()), | ||
| 440 | _ => eprintln!("unknown tftp request option '{opt_name}'"), | ||
| 441 | } | ||
| 442 | } | ||
| 443 | |||
| 444 | Ok(TftpRequestPacket { | ||
| 445 | filename, | ||
| 446 | mode, | ||
| 447 | tsize, | ||
| 448 | blksize, | ||
| 449 | }) | ||
| 450 | } | ||
| 451 | |||
| 452 | fn tftp_data_packet_write(writer: &mut Vec<u8>, block: u16, data: Vec<u8>) -> Result<()> { | ||
| 453 | write_u16(writer, TftpOp::Data.into())?; | ||
| 454 | write_u16(writer, block)?; | ||
| 455 | write_buf(writer, &data)?; | ||
| 456 | Ok(()) | ||
| 457 | } | ||
| 458 | |||
| 459 | fn tftp_oack_packet_write( | ||
| 460 | writer: &mut Vec<u8>, | ||
| 461 | tsize: Option<u64>, | ||
| 462 | blksize: Option<u64>, | ||
| 463 | ) -> Result<()> { | ||
| 464 | write_u16(writer, TftpOp::Oack.into())?; | ||
| 465 | |||
| 466 | // Only include options that were requested by the client | ||
| 467 | if let Some(blksize_val) = blksize { | ||
| 468 | write_buf(writer, b"blksize")?; | ||
| 469 | write_u8(writer, 0)?; // null terminator | ||
| 470 | let blksize_str = blksize_val.to_string(); | ||
| 471 | write_buf(writer, blksize_str.as_bytes())?; | ||
| 472 | write_u8(writer, 0)?; // null terminator | ||
| 473 | } | ||
| 474 | |||
| 475 | if let Some(tsize_val) = tsize { | ||
| 476 | write_buf(writer, b"tsize")?; | ||
| 477 | write_u8(writer, 0)?; // null terminator | ||
| 478 | let tsize_str = tsize_val.to_string(); | ||
| 479 | write_buf(writer, tsize_str.as_bytes())?; | ||
| 480 | write_u8(writer, 0)?; // null terminator | ||
| 481 | } | ||
| 482 | |||
| 483 | Ok(()) | ||
| 484 | } | ||
| 485 | |||
| 486 | #[derive(Debug)] | ||
| 487 | enum TftpPacket { | ||
| 488 | Request(TftpRequestPacket), | ||
| 489 | Data(TftpDataPacket), | ||
| 490 | Ack(TftpAckPacket), | ||
| 491 | Error(TftpErrorPacket), | ||
| 492 | } | ||
| 493 | |||
| 494 | fn tftp_packet_parse(cursor: &mut Cursor<&[u8]>) -> Result<TftpPacket> { | ||
| 495 | let op = TftpOp::try_from(read_u16(cursor)?).unwrap(); | ||
| 496 | match op { | ||
| 497 | TftpOp::ReadRequest | TftpOp::WriteRequest => { | ||
| 498 | let filename = read_null_terminated_string(cursor)?; | ||
| 499 | let mode = read_null_terminated_string(cursor)?; | ||
| 500 | let mut tsize = None; | ||
| 501 | let mut blksize = None; | ||
| 502 | |||
| 503 | while let Ok(opt_name) = read_null_terminated_string(cursor) { | ||
| 504 | if opt_name.is_empty() { | ||
| 505 | break; | ||
| 506 | } | ||
| 507 | let opt_data = read_null_terminated_string(cursor)?; | ||
| 508 | match opt_name.as_str() { | ||
| 509 | "tsize" => tsize = Some(opt_data.parse::<u64>().unwrap()), | ||
| 510 | "blksize" => blksize = Some(opt_data.parse::<u64>().unwrap()), | ||
| 511 | _ => eprintln!("unknown tftp request option '{opt_name}'"), | ||
| 512 | } | ||
| 513 | } | ||
| 514 | |||
| 515 | Ok(TftpPacket::Request(TftpRequestPacket { | ||
| 516 | filename, | ||
| 517 | mode, | ||
| 518 | tsize, | ||
| 519 | blksize, | ||
| 520 | })) | ||
| 521 | } | ||
| 522 | TftpOp::Data => { | ||
| 523 | let block = read_u16(cursor)?; | ||
| 524 | let mut data = Vec::new(); | ||
| 525 | cursor.read_to_end(&mut data)?; | ||
| 526 | Ok(TftpPacket::Data(TftpDataPacket { block, data })) | ||
| 527 | } | ||
| 528 | TftpOp::Ack => { | ||
| 529 | let block = read_u16(cursor)?; | ||
| 530 | Ok(TftpPacket::Ack(TftpAckPacket { block })) | ||
| 531 | } | ||
| 532 | TftpOp::Error => { | ||
| 533 | let code = read_u16(cursor)?; | ||
| 534 | let message = read_null_terminated_string(cursor)?; | ||
| 535 | Ok(TftpPacket::Error(TftpErrorPacket { code, message })) | ||
| 536 | } | ||
| 537 | TftpOp::Oack => { | ||
| 538 | // OACK parsing not implemented for now | ||
| 539 | Err(std::io::Error::new( | ||
| 540 | std::io::ErrorKind::Unsupported, | ||
| 541 | "OACK parsing not implemented", | ||
| 542 | )) | ||
| 543 | } | ||
| 544 | } | ||
| 545 | } | ||
| 546 | |||
| 547 | fn main() { | ||
| 548 | let socket67 = UdpSocket::bind("0.0.0.0:67").unwrap(); | ||
| 549 | socket67.set_broadcast(true).unwrap(); | ||
| 550 | socket67.set_nonblocking(true).unwrap(); | ||
| 551 | |||
| 552 | let socket69 = UdpSocket::bind("0.0.0.0:69").unwrap(); | ||
| 553 | socket69.set_broadcast(false).unwrap(); | ||
| 554 | socket69.set_nonblocking(true).unwrap(); | ||
| 555 | |||
| 556 | let socket4011 = UdpSocket::bind("0.0.0.0:4011").unwrap(); | ||
| 557 | socket4011.set_broadcast(true).unwrap(); | ||
| 558 | socket4011.set_nonblocking(true).unwrap(); | ||
| 559 | |||
| 560 | let mut last_blksize = 512u64; | ||
| 561 | let mut current_file = String::new(); | ||
| 562 | |||
| 563 | loop { | ||
| 564 | let mut buf = [0u8; 1500]; | ||
| 565 | |||
| 566 | // Try port 67 first | ||
| 567 | if let Ok((n, addr)) = socket67.recv_from(&mut buf) { | ||
| 568 | println!("Received {} bytes from {} on port 67", n, addr); | ||
| 569 | handle_packet(&buf[..n], &socket67); | ||
| 570 | } else if let Ok((n, addr)) = socket4011.recv_from(&mut buf) { | ||
| 571 | println!("Received {} bytes from {} on port 4011", n, addr); | ||
| 572 | handle_packet_4011(&buf[..n], &socket4011, addr); | ||
| 573 | } else if let Ok((n, addr)) = socket69.recv_from(&mut buf) { | ||
| 574 | let mut cursor = Cursor::new(&buf[..n]); | ||
| 575 | |||
| 576 | let packet = tftp_packet_parse(&mut cursor).unwrap(); | ||
| 577 | println!("Received TFTP request from {addr}: {packet:#?}"); | ||
| 578 | |||
| 579 | let mut response = Vec::default(); | ||
| 580 | match packet { | ||
| 581 | TftpPacket::Request(tftp_request_packet) => { | ||
| 582 | println!( | ||
| 583 | "Request options: tsize={:?}, blksize={:?}", | ||
| 584 | tftp_request_packet.tsize, tftp_request_packet.blksize | ||
| 585 | ); | ||
| 586 | |||
| 587 | let filepath = format!("tftp/{}", tftp_request_packet.filename); | ||
| 588 | current_file = filepath.clone(); | ||
| 589 | let meta = std::fs::metadata(&filepath).unwrap(); | ||
| 590 | let actual_file_size = meta.len(); | ||
| 591 | |||
| 592 | // Only send OACK if client sent options | ||
| 593 | if tftp_request_packet.tsize.is_some() || tftp_request_packet.blksize.is_some() | ||
| 594 | { | ||
| 595 | if let Some(blksize) = tftp_request_packet.blksize { | ||
| 596 | last_blksize = blksize; | ||
| 597 | } | ||
| 598 | |||
| 599 | let tsize_response = if tftp_request_packet.tsize.is_some() { | ||
| 600 | Some(actual_file_size) | ||
| 601 | } else { | ||
| 602 | None | ||
| 603 | }; | ||
| 604 | |||
| 605 | tftp_oack_packet_write( | ||
| 606 | &mut response, | ||
| 607 | tsize_response, | ||
| 608 | tftp_request_packet.blksize, | ||
| 609 | ) | ||
| 610 | .unwrap(); | ||
| 611 | } else { | ||
| 612 | // No options, send first data block directly | ||
| 613 | let contents = std::fs::read(&filepath).unwrap(); | ||
| 614 | let block_size = 512; | ||
| 615 | let first_block = if contents.len() > block_size { | ||
| 616 | contents[..block_size].to_vec() | ||
| 617 | } else { | ||
| 618 | contents | ||
| 619 | }; | ||
| 620 | |||
| 621 | tftp_data_packet_write(&mut response, 1, first_block).unwrap(); | ||
| 622 | } | ||
| 623 | } | ||
| 624 | TftpPacket::Data(tftp_data_packet) => { | ||
| 625 | println!("Received DATA packet: block {}", tftp_data_packet.block); | ||
| 626 | } | ||
| 627 | TftpPacket::Ack(tftp_ack_packet) => { | ||
| 628 | println!("Received ACK packet: block {}", tftp_ack_packet.block); | ||
| 629 | |||
| 630 | let contents = std::fs::read(¤t_file).unwrap(); | ||
| 631 | let next_block = tftp_ack_packet.block + 1; | ||
| 632 | let start_offset = (next_block - 1) as u64 * last_blksize; | ||
| 633 | let end_offset = next_block as u64 * last_blksize; | ||
| 634 | let prev_start_offset = (next_block.saturating_sub(2)) as u64 * last_blksize; | ||
| 635 | let prev_remain = contents.len() - prev_start_offset as usize; | ||
| 636 | if prev_remain as u64 >= last_blksize || tftp_ack_packet.block == 0 { | ||
| 637 | let end = std::cmp::min(end_offset as usize, contents.len()); | ||
| 638 | let block_data = contents[start_offset as usize..end].to_vec(); | ||
| 639 | println!("sending tftp data packet with {} bytes", block_data.len()); | ||
| 640 | tftp_data_packet_write(&mut response, next_block, block_data).unwrap(); | ||
| 641 | } | ||
| 642 | } | ||
| 643 | TftpPacket::Error(tftp_error_packet) => { | ||
| 644 | println!( | ||
| 645 | "Received ERROR packet: code {}, message: {}", | ||
| 646 | tftp_error_packet.code, tftp_error_packet.message | ||
| 647 | ); | ||
| 648 | } | ||
| 649 | } | ||
| 650 | // | ||
| 651 | // let filepath = format!("tftp/{}", request.filename); | ||
| 652 | // let meta = std::fs::metadata(&filepath).unwrap(); | ||
| 653 | // let contents = std::fs::read(&filepath).unwrap(); | ||
| 654 | // let mut response = Vec::default(); | ||
| 655 | if !response.is_empty() { | ||
| 656 | socket69.send_to(&response, addr).unwrap(); | ||
| 657 | } | ||
| 658 | } else { | ||
| 659 | std::thread::sleep(std::time::Duration::from_millis(10)); | ||
| 660 | } | ||
| 661 | } | ||
| 662 | } | ||
| 663 | |||
| 664 | fn handle_packet(buf: &[u8], socket: &UdpSocket) { | ||
| 665 | match parse_packet(buf) { | ||
| 666 | Ok(packet) => { | ||
| 667 | println!("Parsed DHCP packet: XID={:08x}", packet.xid); | ||
| 668 | |||
| 669 | // Check if it's a PXE client and extract client UUID | ||
| 670 | let mut is_pxe = false; | ||
| 671 | let mut client_uuid = None; | ||
| 672 | let mut is_ipxe = false; | ||
| 673 | |||
| 674 | for option in &packet.options { | ||
| 675 | match option { | ||
| 676 | DhcpOption::VendorClassIdentifier(vendor_class) => { | ||
| 677 | println!("Vendor class: {}", vendor_class); | ||
| 678 | if vendor_class.contains("PXEClient") { | ||
| 679 | is_pxe = true; | ||
| 680 | } | ||
| 681 | } | ||
| 682 | DhcpOption::UserClassInformation(user_class) => { | ||
| 683 | println!("User class: {}", user_class); | ||
| 684 | is_ipxe = true; | ||
| 685 | } | ||
| 686 | DhcpOption::Unknown { code: 97, data } => { | ||
| 687 | println!("Found client machine identifier"); | ||
| 688 | client_uuid = Some(data.clone()); | ||
| 689 | } | ||
| 690 | _ => {} | ||
| 691 | } | ||
| 692 | } | ||
| 693 | |||
| 694 | if is_pxe { | ||
| 695 | println!("Responding to PXE client with DHCPOFFER"); | ||
| 696 | let response = | ||
| 697 | write_boot_packet(packet.xid, packet.chaddr, client_uuid, is_ipxe).unwrap(); | ||
| 698 | socket | ||
| 699 | .send_to(&response, SocketAddrV4::new(Ipv4Addr::BROADCAST, 68)) | ||
| 700 | .unwrap(); | ||
| 701 | } else { | ||
| 702 | println!("Not a PXE client, ignoring"); | ||
| 703 | } | ||
| 704 | } | ||
| 705 | Err(e) => { | ||
| 706 | println!("Failed to parse packet: {}", e); | ||
| 707 | } | ||
| 708 | } | ||
| 709 | } | ||
| 710 | |||
| 711 | fn handle_packet_4011(buf: &[u8], socket: &UdpSocket, sender_addr: SocketAddr) { | ||
| 712 | match parse_packet(buf) { | ||
| 713 | Ok(packet) => { | ||
| 714 | println!("Parsed DHCP packet on 4011: XID={:08x}", packet.xid); | ||
| 715 | |||
| 716 | // Extract client UUID | ||
| 717 | let mut client_uuid = None; | ||
| 718 | for option in &packet.options { | ||
| 719 | if let DhcpOption::Unknown { code: 97, data } = option { | ||
| 720 | client_uuid = Some(data.clone()); | ||
| 721 | break; | ||
| 722 | } | ||
| 723 | } | ||
| 724 | |||
| 725 | println!("Responding with DHCPACK"); | ||
| 726 | let response = write_boot_ack(packet.xid, packet.chaddr, client_uuid).unwrap(); | ||
| 727 | socket.send_to(&response, sender_addr).unwrap(); | ||
| 728 | } | ||
| 729 | Err(e) => { | ||
| 730 | println!("Failed to parse packet on 4011: {}", e); | ||
| 731 | } | ||
| 732 | } | ||
| 733 | } | ||
| 734 | |||
| 735 | const DHCP_PACKET_PAYLOAD: &'static [u8] = &[ | ||
| 736 | 0x1, 0x1, 0x6, 0x0, 0xf1, 0x25, 0x7c, 0x21, 0x0, 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | ||
| 737 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2b, 0x67, 0x3f, 0xda, 0x70, 0x0, 0x0, | ||
| 738 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | ||
| 739 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | ||
| 740 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | ||
| 741 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | ||
| 742 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | ||
| 743 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | ||
| 744 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | ||
| 745 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | ||
| 746 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | ||
| 747 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, | ||
| 748 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x63, 0x82, 0x53, 0x63, 0x35, 0x1, 0x1, 0x39, | ||
| 749 | 0x2, 0x5, 0xc0, 0x37, 0x23, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0xc, 0xd, 0xf, 0x11, 0x12, 0x16, | ||
| 750 | 0x17, 0x1c, 0x28, 0x29, 0x2a, 0x2b, 0x32, 0x33, 0x36, 0x3a, 0x3b, 0x3c, 0x42, 0x43, 0x61, 0x80, | ||
| 751 | 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x61, 0x11, 0x0, 0xcc, 0xfc, 0x32, 0x1b, 0xce, 0x2a, | ||
| 752 | 0xb2, 0x11, 0xa8, 0x5c, 0xb1, 0xac, 0x38, 0x38, 0x10, 0xf, 0x5e, 0x3, 0x1, 0x3, 0x10, 0x5d, | ||
| 753 | 0x2, 0x0, 0x7, 0x3c, 0x20, 0x50, 0x58, 0x45, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3a, 0x41, | ||
| 754 | 0x72, 0x63, 0x68, 0x3a, 0x30, 0x30, 0x30, 0x30, 0x37, 0x3a, 0x55, 0x4e, 0x44, 0x49, 0x3a, 0x30, | ||
| 755 | 0x30, 0x33, 0x30, 0x31, 0x36, 0xff, | ||
| 756 | ]; | ||
diff --git a/tftp/ipxe.efi b/tftp/ipxe.efi new file mode 100644 index 0000000..d1c12b3 --- /dev/null +++ b/tftp/ipxe.efi | |||
| Binary files differ | |||
diff --git a/tftp/test.ipxe b/tftp/test.ipxe new file mode 100644 index 0000000..f4ee267 --- /dev/null +++ b/tftp/test.ipxe | |||
| @@ -0,0 +1,25 @@ | |||
| 1 | #!ipxe | ||
| 2 | |||
| 3 | :start | ||
| 4 | #console --picture http://boot.ipxe.org/ipxe.png | ||
| 5 | menu debian | ||
| 6 | item --gap -- ---------------------- Net installer ----------------------------- | ||
| 7 | item --key 3 Debian9_x86 Debian 9 (3)2-bit net install | ||
| 8 | item --key 6 Debian9_x86_64 Debian 9 (6)4-bit net install | ||
| 9 | item --gap -- ------------------------- Options -------------------------------- | ||
| 10 | item --key g goback (G)o back to previous menu | ||
| 11 | choose version && goto ${version} || goto start | ||
| 12 | |||
| 13 | :Debian9_x86 | ||
| 14 | echo Booting Debian 9 32-bit | ||
| 15 | kernel http://deb.debian.org/debian/dists/stretch/main/installer-i386/current/images/netboot/debian-installer/i386/linux initrd=initrd.gz | ||
| 16 | initrd http://deb.debian.org/debian/dists/stretch/main/installer-i386/current/images/netboot/debian-installer/i386/initrd.gz | ||
| 17 | boot || imgfree | ||
| 18 | goto start | ||
| 19 | |||
| 20 | :Debian9_x86_64 | ||
| 21 | echo Booting Debian 9 64-bit | ||
| 22 | kernel http://deb.debian.org/debian/dists/trixie/main/installer-amd64/current/images/netboot/debian-installer/amd64/linux initrd=initrd.gz | ||
| 23 | initrd http://deb.debian.org/debian/dists/trixie/main/installer-amd64/current/images/netboot/debian-installer/amd64/initrd.gz | ||
| 24 | boot || imgfree | ||
| 25 | goto start | ||
