aboutsummaryrefslogtreecommitdiff
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
parent89db26c86bf48a4c527778fc254765a38b7e9085 (diff)
initial working version
-rw-r--r--Cargo.lock242
-rw-r--r--Cargo.toml1
-rw-r--r--menu.ipxe121
-rw-r--r--netboot.xyz-arm64.efibin0 -> 1110528 bytes
-rw-r--r--netboot.xyz.efibin0 -> 1118720 bytes
-rw-r--r--netboot.xyz.kpxebin0 -> 384280 bytes
-rw-r--r--pxespec.pdfbin0 -> 502352 bytes
-rw-r--r--src/dhcp.rs398
-rw-r--r--src/main.rs408
-rw-r--r--src/tftp.rs377
-rw-r--r--tftp/ipxe.efibin1044480 -> 0 bytes
-rw-r--r--tftp/test.ipxe25
12 files changed, 1283 insertions, 289 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e27c7d7..77e808b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3,12 +3,120 @@
3version = 4 3version = 4
4 4
5[[package]] 5[[package]]
6name = "anstream"
7version = "0.6.21"
8source = "registry+https://github.com/rust-lang/crates.io-index"
9checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
10dependencies = [
11 "anstyle",
12 "anstyle-parse",
13 "anstyle-query",
14 "anstyle-wincon",
15 "colorchoice",
16 "is_terminal_polyfill",
17 "utf8parse",
18]
19
20[[package]]
21name = "anstyle"
22version = "1.0.13"
23source = "registry+https://github.com/rust-lang/crates.io-index"
24checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
25
26[[package]]
27name = "anstyle-parse"
28version = "0.2.7"
29source = "registry+https://github.com/rust-lang/crates.io-index"
30checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
31dependencies = [
32 "utf8parse",
33]
34
35[[package]]
36name = "anstyle-query"
37version = "1.1.4"
38source = "registry+https://github.com/rust-lang/crates.io-index"
39checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
40dependencies = [
41 "windows-sys",
42]
43
44[[package]]
45name = "anstyle-wincon"
46version = "3.0.10"
47source = "registry+https://github.com/rust-lang/crates.io-index"
48checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
49dependencies = [
50 "anstyle",
51 "once_cell_polyfill",
52 "windows-sys",
53]
54
55[[package]]
56name = "clap"
57version = "4.5.48"
58source = "registry+https://github.com/rust-lang/crates.io-index"
59checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae"
60dependencies = [
61 "clap_builder",
62 "clap_derive",
63]
64
65[[package]]
66name = "clap_builder"
67version = "4.5.48"
68source = "registry+https://github.com/rust-lang/crates.io-index"
69checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9"
70dependencies = [
71 "anstream",
72 "anstyle",
73 "clap_lex",
74 "strsim",
75]
76
77[[package]]
78name = "clap_derive"
79version = "4.5.47"
80source = "registry+https://github.com/rust-lang/crates.io-index"
81checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
82dependencies = [
83 "heck",
84 "proc-macro2",
85 "quote",
86 "syn",
87]
88
89[[package]]
90name = "clap_lex"
91version = "0.7.5"
92source = "registry+https://github.com/rust-lang/crates.io-index"
93checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
94
95[[package]]
96name = "colorchoice"
97version = "1.0.4"
98source = "registry+https://github.com/rust-lang/crates.io-index"
99checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
100
101[[package]]
102name = "heck"
103version = "0.5.0"
104source = "registry+https://github.com/rust-lang/crates.io-index"
105checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
106
107[[package]]
6name = "ipnet" 108name = "ipnet"
7version = "2.11.0" 109version = "2.11.0"
8source = "registry+https://github.com/rust-lang/crates.io-index" 110source = "registry+https://github.com/rust-lang/crates.io-index"
9checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" 111checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
10 112
11[[package]] 113[[package]]
114name = "is_terminal_polyfill"
115version = "1.70.1"
116source = "registry+https://github.com/rust-lang/crates.io-index"
117checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
118
119[[package]]
12name = "libc" 120name = "libc"
13version = "0.2.176" 121version = "0.2.176"
14source = "registry+https://github.com/rust-lang/crates.io-index" 122source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -18,6 +126,140 @@ checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
18name = "netiso" 126name = "netiso"
19version = "0.1.0" 127version = "0.1.0"
20dependencies = [ 128dependencies = [
129 "clap",
21 "ipnet", 130 "ipnet",
22 "libc", 131 "libc",
23] 132]
133
134[[package]]
135name = "once_cell_polyfill"
136version = "1.70.1"
137source = "registry+https://github.com/rust-lang/crates.io-index"
138checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
139
140[[package]]
141name = "proc-macro2"
142version = "1.0.101"
143source = "registry+https://github.com/rust-lang/crates.io-index"
144checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
145dependencies = [
146 "unicode-ident",
147]
148
149[[package]]
150name = "quote"
151version = "1.0.41"
152source = "registry+https://github.com/rust-lang/crates.io-index"
153checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
154dependencies = [
155 "proc-macro2",
156]
157
158[[package]]
159name = "strsim"
160version = "0.11.1"
161source = "registry+https://github.com/rust-lang/crates.io-index"
162checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
163
164[[package]]
165name = "syn"
166version = "2.0.106"
167source = "registry+https://github.com/rust-lang/crates.io-index"
168checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
169dependencies = [
170 "proc-macro2",
171 "quote",
172 "unicode-ident",
173]
174
175[[package]]
176name = "unicode-ident"
177version = "1.0.19"
178source = "registry+https://github.com/rust-lang/crates.io-index"
179checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
180
181[[package]]
182name = "utf8parse"
183version = "0.2.2"
184source = "registry+https://github.com/rust-lang/crates.io-index"
185checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
186
187[[package]]
188name = "windows-link"
189version = "0.2.1"
190source = "registry+https://github.com/rust-lang/crates.io-index"
191checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
192
193[[package]]
194name = "windows-sys"
195version = "0.60.2"
196source = "registry+https://github.com/rust-lang/crates.io-index"
197checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
198dependencies = [
199 "windows-targets",
200]
201
202[[package]]
203name = "windows-targets"
204version = "0.53.5"
205source = "registry+https://github.com/rust-lang/crates.io-index"
206checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
207dependencies = [
208 "windows-link",
209 "windows_aarch64_gnullvm",
210 "windows_aarch64_msvc",
211 "windows_i686_gnu",
212 "windows_i686_gnullvm",
213 "windows_i686_msvc",
214 "windows_x86_64_gnu",
215 "windows_x86_64_gnullvm",
216 "windows_x86_64_msvc",
217]
218
219[[package]]
220name = "windows_aarch64_gnullvm"
221version = "0.53.1"
222source = "registry+https://github.com/rust-lang/crates.io-index"
223checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
224
225[[package]]
226name = "windows_aarch64_msvc"
227version = "0.53.1"
228source = "registry+https://github.com/rust-lang/crates.io-index"
229checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
230
231[[package]]
232name = "windows_i686_gnu"
233version = "0.53.1"
234source = "registry+https://github.com/rust-lang/crates.io-index"
235checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
236
237[[package]]
238name = "windows_i686_gnullvm"
239version = "0.53.1"
240source = "registry+https://github.com/rust-lang/crates.io-index"
241checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
242
243[[package]]
244name = "windows_i686_msvc"
245version = "0.53.1"
246source = "registry+https://github.com/rust-lang/crates.io-index"
247checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
248
249[[package]]
250name = "windows_x86_64_gnu"
251version = "0.53.1"
252source = "registry+https://github.com/rust-lang/crates.io-index"
253checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
254
255[[package]]
256name = "windows_x86_64_gnullvm"
257version = "0.53.1"
258source = "registry+https://github.com/rust-lang/crates.io-index"
259checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
260
261[[package]]
262name = "windows_x86_64_msvc"
263version = "0.53.1"
264source = "registry+https://github.com/rust-lang/crates.io-index"
265checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
diff --git a/Cargo.toml b/Cargo.toml
index 9a079c1..c800605 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,5 +4,6 @@ version = "0.1.0"
4edition = "2024" 4edition = "2024"
5 5
6[dependencies] 6[dependencies]
7clap = { version = "4.5.48", features = ["env", "derive"] }
7ipnet = "2.11.0" 8ipnet = "2.11.0"
8libc = "0.2.176" 9libc = "0.2.176"
diff --git a/menu.ipxe b/menu.ipxe
new file mode 100644
index 0000000..cf4f20b
--- /dev/null
+++ b/menu.ipxe
@@ -0,0 +1,121 @@
1#!ipxe
2
3:start
4isset ${arch} && goto skip_arch_detect ||
5cpuid --ext 29 && set arch x86_64 || set arch i386
6iseq ${buildarch} arm64 && set arch arm64 ||
7:skip_arch_detect
8chain --autofree boot.cfg ||
9echo Attempting to retrieve latest upstream version number...
10chain --timeout 5000 https://boot.netboot.xyz/version.ipxe ||
11ntp 0.pool.ntp.org ||
12iseq ${cls} serial && goto ignore_cls ||
13set cls:hex 1b:5b:4a # ANSI clear screen sequence - "^[[J"
14set cls ${cls:string}
15:ignore_cls
16
17:version_check
18set latest_version 2.x
19echo ${cls}
20iseq ${version} ${latest_version} && goto version_up2date ||
21echo
22echo Updated version of netboot.xyz is available:
23echo
24echo Running version.....${version}
25echo Updated version.....${latest_version}
26echo
27echo Please download the latest version from netboot.xyz.
28echo
29echo Attempting to chain to latest version...
30chain --autofree http://${boot_domain}/ipxe/${ipxe_disk} ||
31:version_up2date
32
33isset ${menu} && goto ${menu} ||
34isset ${ip} || dhcp
35
36:main_menu
37clear menu
38set space:hex 20:20
39set space ${space:string}
40isset ${next-server} && menu ${site_name} v${version} - next-server: ${next-server} || menu ${site_name}
41item --gap Default:
42item local ${space} Boot from local hdd
43item --gap Distributions:
44iseq ${menu_linux} 1 && item linux ${space} Linux Network Installs (64-bit) ||
45iseq ${menu_linux_i386} 1 && item linux-i386 ${space} Linux Network Installs (32-bit) ||
46iseq ${menu_linux_arm} 1 && item linux-arm ${space} Linux Network Installs (arm64) ||
47iseq ${menu_live} 1 && item live ${space} Live CDs ||
48iseq ${menu_live_arm} 1 && item live-arm ${space} Live CDs ||
49iseq ${menu_bsd} 1 && item bsd ${space} BSD Installs ||
50iseq ${menu_unix} 1 && item unix ${space} Unix Network Installs ||
51iseq ${menu_freedos} 1 && item freedos ${space} FreeDOS ||
52iseq ${menu_windows} 1 && item windows ${space} Windows ||
53item --gap Tools:
54iseq ${menu_utils} 1 && iseq ${platform} efi && item utils-efi ${space} Utilities (UEFI) ||
55iseq ${menu_utils} 1 && iseq ${platform} pcbios && iseq ${arch} x86_64 && item utils-pcbios-64 ${space} Utilities (64-bit) ||
56iseq ${menu_utils} 1 && iseq ${platform} pcbios && iseq ${arch} i386 && item utils-pcbios-32 ${space} Utilities (32-bit) ||
57iseq ${menu_utils_arm} 1 && item utils-arm ${space} Utilities (arm64) ||
58item change_arch ${space} Architecture: ${arch}
59item shell ${space} iPXE shell
60item netinfo ${space} Network card info
61iseq ${menu_pci} 1 && item lspci ${space} PCI Device List ||
62item about ${space} About netboot.xyz
63item --gap Signature Checks:
64item sig_check ${space} netboot.xyz [ enabled: ${sigs_enabled} ]
65isset ${github_user} && item --gap Custom Github Menu: ||
66isset ${github_user} && item custom-github ${space} ${github_user}'s Custom Menu ||
67isset ${custom_url} && item --gap Custom URL Menu: ||
68isset ${custom_url} && item custom-url ${space} Custom URL Menu ||
69isset ${menu} && set timeout 0 || set timeout ${boot_timeout}
70choose --timeout ${timeout} --default ${menu} menu || goto local
71echo ${cls}
72goto ${menu} ||
73iseq ${sigs_enabled} true && goto verify_sigs || goto change_menu
74
75:verify_sigs
76imgverify ${menu}.ipxe ${sigs}${menu}.ipxe.sig || goto error
77goto change_menu
78
79:change_menu
80chain ${menu}.ipxe || goto error
81goto main_menu
82
83:error
84echo Error occurred, press any key to return to menu ...
85prompt
86goto main_menu
87
88:local
89echo Booting from local disks ...
90exit 1
91
92:shell
93echo Type "exit" to return to menu.
94set menu main_menu
95shell
96goto main_menu
97
98:change_arch
99iseq ${arch} x86_64 && set arch i386 && set menu_linux_i386 1 && set menu_linux 0 && goto main_menu ||
100iseq ${arch} i386 && set arch x86_64 && set menu_linux_i386 0 && set menu_linux 1 && goto main_menu ||
101goto main_menu
102
103:sig_check
104iseq ${sigs_enabled} true && set sigs_enabled false || set sigs_enabled true
105goto main_menu
106
107:about
108chain https://boot.netboot.xyz/about.ipxe || chain about.ipxe
109goto main_menu
110
111:custom-github
112chain https://raw.githubusercontent.com/${github_user}/netboot.xyz-custom/master/custom.ipxe || goto error
113goto main_menu
114
115:custom-url
116chain ${custom_url}/custom.ipxe || goto error
117goto main_menu
118
119:custom-user
120chain custom/custom.ipxe
121goto main_menu
diff --git a/netboot.xyz-arm64.efi b/netboot.xyz-arm64.efi
new file mode 100644
index 0000000..9ad9da2
--- /dev/null
+++ b/netboot.xyz-arm64.efi
Binary files differ
diff --git a/netboot.xyz.efi b/netboot.xyz.efi
new file mode 100644
index 0000000..f69ee57
--- /dev/null
+++ b/netboot.xyz.efi
Binary files differ
diff --git a/netboot.xyz.kpxe b/netboot.xyz.kpxe
new file mode 100644
index 0000000..5edb32c
--- /dev/null
+++ b/netboot.xyz.kpxe
Binary files differ
diff --git a/pxespec.pdf b/pxespec.pdf
new file mode 100644
index 0000000..029259d
--- /dev/null
+++ b/pxespec.pdf
Binary files differ
diff --git a/src/dhcp.rs b/src/dhcp.rs
index 38cc8e4..51680d1 100644
--- a/src/dhcp.rs
+++ b/src/dhcp.rs
@@ -1,6 +1,7 @@
1use std::{ 1use std::{
2 io::{Cursor, Read as _, Result, Write}, 2 io::{Cursor, Read as _, Result, Write},
3 net::Ipv4Addr, 3 net::Ipv4Addr,
4 str::FromStr,
4}; 5};
5 6
6use crate::wire; 7use crate::wire;
@@ -8,6 +9,11 @@ use crate::wire;
8const MAGIC_COOKIE: [u8; 4] = [0x63, 0x82, 0x53, 0x63]; 9const MAGIC_COOKIE: [u8; 4] = [0x63, 0x82, 0x53, 0x63];
9const FLAG_BROADCAST: u16 = 1 << 15; 10const FLAG_BROADCAST: u16 = 1 << 15;
10 11
12pub const VENDOR_CLASS_PXE_CLIENT: &'static [u8] = b"PXEClient";
13pub const VENDOR_CLASS_PXE_SERVER: &'static [u8] = b"PXEServer";
14
15pub const USER_CLASS_IPXE: &'static [u8] = b"iPXE";
16
11#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] 17#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum BootOp { 18pub enum BootOp {
13 #[default] 19 #[default]
@@ -29,6 +35,8 @@ impl From<BootOp> for u8 {
29 } 35 }
30} 36}
31 37
38pub type HardwareAddress = [u8; 16];
39
32#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] 40#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum HardwareType { 41pub enum HardwareType {
34 #[default] 42 #[default]
@@ -78,10 +86,11 @@ pub enum DhcpOption {
78 End, 86 End,
79 MessageType(DhcpMessageType), 87 MessageType(DhcpMessageType),
80 ServerIdentifier(Ipv4Addr), 88 ServerIdentifier(Ipv4Addr),
81 VendorClassIdentifier(String), 89 VendorClassIdentifier(Vec<u8>),
82 TftpServerName(String), 90 TftpServerName(String),
83 TftpFileName(String), 91 TftpFileName(String),
84 UserClassInformation(String), 92 UserClassInformation(Vec<u8>),
93 ClientSystemArchitecture(SystemArchitecture),
85 ClientMachineIdentifier(Vec<u8>), 94 ClientMachineIdentifier(Vec<u8>),
86 Unknown { code: u8, data: Vec<u8> }, 95 Unknown { code: u8, data: Vec<u8> },
87} 96}
@@ -95,6 +104,7 @@ impl DhcpOption {
95 pub const CODE_TFTP_SERVER_NAME: u8 = 66; 104 pub const CODE_TFTP_SERVER_NAME: u8 = 66;
96 pub const CODE_TFTP_FILE_NAME: u8 = 67; 105 pub const CODE_TFTP_FILE_NAME: u8 = 67;
97 pub const CODE_USER_CLASS_INFORMATION: u8 = 77; 106 pub const CODE_USER_CLASS_INFORMATION: u8 = 77;
107 pub const CODE_CLIENT_SYSTEM_ARCHITECTURE: u8 = 93;
98 pub const CODE_CLIENT_MACHINE_IDENTIFIER: u8 = 97; 108 pub const CODE_CLIENT_MACHINE_IDENTIFIER: u8 = 97;
99 109
100 pub fn code(&self) -> u8 { 110 pub fn code(&self) -> u8 {
@@ -107,12 +117,294 @@ impl DhcpOption {
107 DhcpOption::TftpServerName(_) => Self::CODE_TFTP_SERVER_NAME, 117 DhcpOption::TftpServerName(_) => Self::CODE_TFTP_SERVER_NAME,
108 DhcpOption::TftpFileName(_) => Self::CODE_TFTP_FILE_NAME, 118 DhcpOption::TftpFileName(_) => Self::CODE_TFTP_FILE_NAME,
109 DhcpOption::UserClassInformation(_) => Self::CODE_USER_CLASS_INFORMATION, 119 DhcpOption::UserClassInformation(_) => Self::CODE_USER_CLASS_INFORMATION,
120 DhcpOption::ClientSystemArchitecture(_) => Self::CODE_CLIENT_SYSTEM_ARCHITECTURE,
110 DhcpOption::ClientMachineIdentifier(_) => Self::CODE_CLIENT_MACHINE_IDENTIFIER, 121 DhcpOption::ClientMachineIdentifier(_) => Self::CODE_CLIENT_MACHINE_IDENTIFIER,
111 DhcpOption::Unknown { code, .. } => *code, 122 DhcpOption::Unknown { code, .. } => *code,
112 } 123 }
113 } 124 }
114} 125}
115 126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
128pub enum SystemArchitecture {
129 IntelX86pc,
130 NECPC98,
131 EfiItanium,
132 DecAlpha,
133 ArcX86,
134 IntelLeanClient,
135 EfiIA32,
136 EfiBC,
137 EfiXscale,
138 EfiX86_64,
139 EfiARM32,
140 EfiARM64,
141 EfiARM32Http,
142 EfiARM64Http,
143 ARM32Uboot,
144 ARM64Uboot,
145 Unknown(u16),
146}
147
148impl SystemArchitecture {
149 pub const CODE_INTEL_X86_PC: u16 = 0;
150 pub const CODE_NEC_PC98: u16 = 1;
151 pub const CODE_EFI_ITANIUM: u16 = 2;
152 pub const CODE_DEC_ALPHA: u16 = 3;
153 pub const CODE_ARC_X86: u16 = 4;
154 pub const CODE_INTEL_LEAN_CLIENT: u16 = 5;
155 pub const CODE_EFI_IA32: u16 = 6;
156 pub const CODE_EFI_BC: u16 = 7;
157 pub const CODE_EFI_XSCALE: u16 = 8;
158 pub const CODE_EFI_X86_64: u16 = 9;
159 pub const CODE_EFI_ARM32: u16 = 10;
160 pub const CODE_EFI_ARM64: u16 = 11;
161 pub const CODE_EFI_ARM32_HTTP: u16 = 18;
162 pub const CODE_EFI_ARM64_HTTP: u16 = 19;
163 pub const CODE_ARM32_UBOOT: u16 = 21;
164 pub const CODE_ARM64_UBOOT: u16 = 22;
165}
166
167impl From<u16> for SystemArchitecture {
168 fn from(value: u16) -> Self {
169 match value {
170 Self::CODE_INTEL_X86_PC => SystemArchitecture::IntelX86pc,
171 Self::CODE_NEC_PC98 => SystemArchitecture::NECPC98,
172 Self::CODE_EFI_ITANIUM => SystemArchitecture::EfiItanium,
173 Self::CODE_DEC_ALPHA => SystemArchitecture::DecAlpha,
174 Self::CODE_ARC_X86 => SystemArchitecture::ArcX86,
175 Self::CODE_INTEL_LEAN_CLIENT => SystemArchitecture::IntelLeanClient,
176 Self::CODE_EFI_IA32 => SystemArchitecture::EfiIA32,
177 Self::CODE_EFI_BC => SystemArchitecture::EfiBC,
178 Self::CODE_EFI_XSCALE => SystemArchitecture::EfiXscale,
179 Self::CODE_EFI_X86_64 => SystemArchitecture::EfiX86_64,
180 Self::CODE_EFI_ARM32 => SystemArchitecture::EfiARM32,
181 Self::CODE_EFI_ARM64 => SystemArchitecture::EfiARM64,
182 Self::CODE_EFI_ARM32_HTTP => SystemArchitecture::EfiARM32Http,
183 Self::CODE_EFI_ARM64_HTTP => SystemArchitecture::EfiARM64Http,
184 Self::CODE_ARM32_UBOOT => SystemArchitecture::ARM32Uboot,
185 Self::CODE_ARM64_UBOOT => SystemArchitecture::ARM64Uboot,
186 _ => SystemArchitecture::Unknown(value),
187 }
188 }
189}
190
191impl From<SystemArchitecture> for u16 {
192 fn from(value: SystemArchitecture) -> Self {
193 match value {
194 SystemArchitecture::IntelX86pc => SystemArchitecture::CODE_INTEL_X86_PC,
195 SystemArchitecture::NECPC98 => SystemArchitecture::CODE_NEC_PC98,
196 SystemArchitecture::EfiItanium => SystemArchitecture::CODE_EFI_ITANIUM,
197 SystemArchitecture::DecAlpha => SystemArchitecture::CODE_DEC_ALPHA,
198 SystemArchitecture::ArcX86 => SystemArchitecture::CODE_ARC_X86,
199 SystemArchitecture::IntelLeanClient => SystemArchitecture::CODE_INTEL_LEAN_CLIENT,
200 SystemArchitecture::EfiIA32 => SystemArchitecture::CODE_EFI_IA32,
201 SystemArchitecture::EfiBC => SystemArchitecture::CODE_EFI_BC,
202 SystemArchitecture::EfiXscale => SystemArchitecture::CODE_EFI_XSCALE,
203 SystemArchitecture::EfiX86_64 => SystemArchitecture::CODE_EFI_X86_64,
204 SystemArchitecture::EfiARM32 => SystemArchitecture::CODE_EFI_ARM32,
205 SystemArchitecture::EfiARM64 => SystemArchitecture::CODE_EFI_ARM64,
206 SystemArchitecture::EfiARM32Http => SystemArchitecture::CODE_EFI_ARM32_HTTP,
207 SystemArchitecture::EfiARM64Http => SystemArchitecture::CODE_EFI_ARM64_HTTP,
208 SystemArchitecture::ARM32Uboot => SystemArchitecture::CODE_ARM32_UBOOT,
209 SystemArchitecture::ARM64Uboot => SystemArchitecture::CODE_ARM64_UBOOT,
210 SystemArchitecture::Unknown(code) => code,
211 }
212 }
213}
214
215impl FromStr for SystemArchitecture {
216 type Err = <u16 as FromStr>::Err;
217
218 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
219 s.parse::<u16>().map(From::from)
220 }
221}
222
223#[derive(Debug)]
224pub struct InvalidPxeClassIdentifierKind(String);
225
226impl InvalidPxeClassIdentifierKind {
227 fn new(kind: impl Into<String>) -> Self {
228 Self(kind.into())
229 }
230}
231
232impl std::fmt::Display for InvalidPxeClassIdentifierKind {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 write!(
235 f,
236 "invalid pxe class identifier kind '{}', expected 'PXEClient' or 'PXEServer'",
237 self.0
238 )
239 }
240}
241
242impl std::error::Error for InvalidPxeClassIdentifierKind {}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
245pub enum PxeClassIdentifierKind {
246 Client,
247 Server,
248}
249
250impl PxeClassIdentifierKind {
251 pub const KIND_CLIENT: &'static str = "PXEClient";
252 pub const KIND_SERVER: &'static str = "PXEServer";
253}
254
255impl FromStr for PxeClassIdentifierKind {
256 type Err = InvalidPxeClassIdentifierKind;
257
258 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
259 match s {
260 Self::KIND_CLIENT => Ok(Self::Client),
261 Self::KIND_SERVER => Ok(Self::Server),
262 _ => Err(InvalidPxeClassIdentifierKind::new(s)),
263 }
264 }
265}
266
267#[derive(Debug)]
268pub struct InvalidPxeClassIdentifier(String, String);
269
270impl InvalidPxeClassIdentifier {
271 fn new(class: impl Into<String>, reason: impl Into<String>) -> Self {
272 Self(class.into(), reason.into())
273 }
274}
275
276impl std::fmt::Display for InvalidPxeClassIdentifier {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 write!(f, "invalid pxe class identifier '{}': {}", self.0, self.1)
279 }
280}
281
282impl std::error::Error for InvalidPxeClassIdentifier {}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
285pub enum PxeClassIdentifier {
286 Client(PxeClassIdentifierClient),
287 Server(PxeClassIdentifierServer),
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
291pub struct PxeClassIdentifierServer;
292
293impl std::fmt::Display for PxeClassIdentifierServer {
294 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295 f.write_str("PXEServer")
296 }
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
300pub struct PxeClassIdentifierClient {
301 pub architecture: SystemArchitecture,
302 pub undi_major: u16,
303 pub undi_minor: u16,
304}
305
306impl std::fmt::Display for PxeClassIdentifierClient {
307 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308 write!(
309 f,
310 "PXEClient:Arch:{:05}:UNDI:{:03}{:03}",
311 u16::from(self.architecture),
312 self.undi_major,
313 self.undi_minor
314 )
315 }
316}
317
318impl std::fmt::Display for PxeClassIdentifier {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 match self {
321 PxeClassIdentifier::Client(client) => client.fmt(f),
322 PxeClassIdentifier::Server(server) => server.fmt(f),
323 }
324 }
325}
326
327impl TryFrom<&[u8]> for PxeClassIdentifier {
328 type Error = InvalidPxeClassIdentifier;
329
330 fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
331 let str = std::str::from_utf8(value).map_err(|err| {
332 InvalidPxeClassIdentifier::new(
333 format!("{value:?}"),
334 format!("invalid utf-8 string: {err}"),
335 )
336 })?;
337 str.parse()
338 }
339}
340
341impl FromStr for PxeClassIdentifier {
342 type Err = InvalidPxeClassIdentifier;
343
344 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
345 let mut parts = s.split(":");
346 let make_err = |reason: String| InvalidPxeClassIdentifier::new(s, reason);
347
348 let kind = match parts.next() {
349 Some(kind) => kind
350 .parse::<PxeClassIdentifierKind>()
351 .map_err(|err| make_err(err.to_string()))?,
352 None => return Err(make_err("missing class kind".to_string())),
353 };
354
355 if kind == PxeClassIdentifierKind::Server {
356 if parts.next().is_some() {
357 return Err(make_err("invalid class".to_string()));
358 }
359 return Ok(Self::Server(PxeClassIdentifierServer));
360 }
361
362 if !parts.next().map(|s| s == "Arch").unwrap_or(false) {
363 return Err(make_err("invalid class".to_string()));
364 }
365
366 let architecture = match parts.next() {
367 Some(arch) => arch
368 .parse::<SystemArchitecture>()
369 .map_err(|err| make_err(err.to_string()))?,
370 None => return Err(make_err("missing architecture".to_string())),
371 };
372
373 if !parts.next().map(|s| s == "UNDI").unwrap_or(false) {
374 return Err(make_err("invalid class".to_string()));
375 }
376
377 let undi_str = match parts.next() {
378 Some(undi_str) => undi_str,
379 None => return Err(make_err("missing undi version".to_string())),
380 };
381
382 if undi_str.len() != 6 {
383 return Err(make_err("invalid undi version length".to_string()));
384 }
385
386 let (undi_major_str, undi_minor_str) = undi_str.split_at_checked(3).unwrap();
387
388 let undi_major = undi_major_str
389 .parse::<u16>()
390 .map_err(|err| make_err(err.to_string()))?;
391
392 let undi_minor = undi_minor_str
393 .parse::<u16>()
394 .map_err(|err| make_err(err.to_string()))?;
395
396 if parts.next().is_some() {
397 return Err(make_err("invalid class".to_string()));
398 }
399
400 Ok(Self::Client(PxeClassIdentifierClient {
401 architecture,
402 undi_major,
403 undi_minor,
404 }))
405 }
406}
407
116#[derive(Debug)] 408#[derive(Debug)]
117pub struct DhcpPacket { 409pub struct DhcpPacket {
118 pub op: BootOp, 410 pub op: BootOp,
@@ -125,7 +417,7 @@ pub struct DhcpPacket {
125 pub yiaddr: Ipv4Addr, 417 pub yiaddr: Ipv4Addr,
126 pub siaddr: Ipv4Addr, 418 pub siaddr: Ipv4Addr,
127 pub giaddr: Ipv4Addr, 419 pub giaddr: Ipv4Addr,
128 pub chaddr: [u8; 16], 420 pub chaddr: HardwareAddress,
129 // server host name 421 // server host name
130 pub sname: String, 422 pub sname: String,
131 // boot file name 423 // boot file name
@@ -158,11 +450,21 @@ impl DhcpPacket {
158 pub fn new_boot( 450 pub fn new_boot(
159 xid: u32, 451 xid: u32,
160 chaddr: [u8; 16], 452 chaddr: [u8; 16],
161 client_uuid: Vec<u8>, 453 client_uuid: Option<Vec<u8>>,
162 local_ip: Ipv4Addr, 454 local_ip: Ipv4Addr,
163 local_hostname: String, 455 local_hostname: String,
164 filename: String, 456 filename: String,
165 ) -> Self { 457 ) -> Self {
458 let mut options = vec![
459 DhcpOption::MessageType(DhcpMessageType::Offer),
460 DhcpOption::ServerIdentifier(local_ip),
461 DhcpOption::VendorClassIdentifier(b"PXEClient".to_vec()),
462 DhcpOption::TftpServerName(local_hostname),
463 DhcpOption::TftpFileName(filename),
464 ];
465 if let Some(uuid) = client_uuid {
466 options.push(DhcpOption::ClientMachineIdentifier(uuid));
467 }
166 Self { 468 Self {
167 op: BootOp::Reply, 469 op: BootOp::Reply,
168 htype: HardwareType::Ethernet, 470 htype: HardwareType::Ethernet,
@@ -177,25 +479,28 @@ impl DhcpPacket {
177 chaddr, 479 chaddr,
178 sname: Default::default(), 480 sname: Default::default(),
179 file: Default::default(), 481 file: Default::default(),
180 options: vec![ 482 options,
181 DhcpOption::MessageType(DhcpMessageType::Offer),
182 DhcpOption::ServerIdentifier(local_ip),
183 DhcpOption::VendorClassIdentifier("PXEClient".to_string()),
184 DhcpOption::ClientMachineIdentifier(client_uuid),
185 DhcpOption::TftpServerName(local_hostname),
186 DhcpOption::TftpFileName(filename),
187 ],
188 } 483 }
189 } 484 }
190 485
191 pub fn new_boot_ack( 486 pub fn new_boot_ack(
192 xid: u32, 487 xid: u32,
193 chaddr: [u8; 16], 488 chaddr: [u8; 16],
194 client_uuid: Vec<u8>, 489 client_uuid: Option<Vec<u8>>,
195 local_ip: Ipv4Addr, 490 local_ip: Ipv4Addr,
196 hostname: String, 491 hostname: String,
197 filename: String, 492 filename: String,
198 ) -> Self { 493 ) -> Self {
494 let mut options = vec![
495 DhcpOption::MessageType(DhcpMessageType::Ack),
496 DhcpOption::ServerIdentifier(local_ip),
497 DhcpOption::VendorClassIdentifier(b"PXEClient".to_vec()),
498 DhcpOption::TftpServerName(hostname),
499 DhcpOption::TftpFileName(filename),
500 ];
501 if let Some(uuid) = client_uuid {
502 options.push(DhcpOption::ClientMachineIdentifier(uuid));
503 }
199 Self { 504 Self {
200 op: BootOp::Reply, 505 op: BootOp::Reply,
201 htype: HardwareType::Ethernet, 506 htype: HardwareType::Ethernet,
@@ -210,14 +515,7 @@ impl DhcpPacket {
210 chaddr, 515 chaddr,
211 sname: Default::default(), 516 sname: Default::default(),
212 file: Default::default(), 517 file: Default::default(),
213 options: vec![ 518 options,
214 DhcpOption::MessageType(DhcpMessageType::Ack),
215 DhcpOption::ServerIdentifier(local_ip),
216 DhcpOption::VendorClassIdentifier("PXEClient".to_string()),
217 DhcpOption::ClientMachineIdentifier(client_uuid),
218 DhcpOption::TftpServerName(hostname),
219 DhcpOption::TftpFileName(filename),
220 ],
221 } 519 }
222 } 520 }
223 521
@@ -296,10 +594,23 @@ fn read_option(cursor: &mut Cursor<&[u8]>) -> Result<DhcpOption> {
296 DhcpOption::CODE_PAD => DhcpOption::Pad, 594 DhcpOption::CODE_PAD => DhcpOption::Pad,
297 DhcpOption::CODE_END => DhcpOption::End, 595 DhcpOption::CODE_END => DhcpOption::End,
298 DhcpOption::CODE_VENDOR_CLASS_IDENTIFIER => { 596 DhcpOption::CODE_VENDOR_CLASS_IDENTIFIER => {
299 DhcpOption::VendorClassIdentifier(read_len8_prefixed_string(cursor)?) 597 DhcpOption::VendorClassIdentifier(read_len8_prefixed_vec(cursor)?)
300 } 598 }
301 DhcpOption::CODE_USER_CLASS_INFORMATION => { 599 DhcpOption::CODE_USER_CLASS_INFORMATION => {
302 DhcpOption::UserClassInformation(read_len8_prefixed_string(cursor)?) 600 DhcpOption::UserClassInformation(read_len8_prefixed_vec(cursor)?)
601 }
602 DhcpOption::CODE_CLIENT_SYSTEM_ARCHITECTURE => {
603 let len = read_u8(cursor)?;
604 assert_eq!(len, 2);
605
606 let mut buf = [0u8; 2];
607 cursor.read_exact(&mut buf)?;
608
609 let arch = SystemArchitecture::from(u16::from_be_bytes(buf));
610 DhcpOption::ClientSystemArchitecture(arch)
611 }
612 DhcpOption::CODE_CLIENT_MACHINE_IDENTIFIER => {
613 DhcpOption::ClientMachineIdentifier(read_len8_prefixed_vec(cursor)?)
303 } 614 }
304 _ => { 615 _ => {
305 let len = read_u8(cursor)?; 616 let len = read_u8(cursor)?;
@@ -353,6 +664,20 @@ pub fn parse_packet(buf: &[u8]) -> Result<DhcpPacket> {
353} 664}
354 665
355pub fn write_packet<W: Write>(mut writer: W, packet: &DhcpPacket) -> Result<()> { 666pub fn write_packet<W: Write>(mut writer: W, packet: &DhcpPacket) -> Result<()> {
667 if packet.sname.len() >= 64 {
668 return Err(std::io::Error::new(
669 std::io::ErrorKind::InvalidInput,
670 "sname cannot be longer than 64 bytes",
671 ));
672 }
673
674 if packet.file.len() >= 128 {
675 return Err(std::io::Error::new(
676 std::io::ErrorKind::InvalidInput,
677 "filename cannot be longer than 128 bytes",
678 ));
679 }
680
356 wire::write_u8(&mut writer, u8::from(packet.op))?; 681 wire::write_u8(&mut writer, u8::from(packet.op))?;
357 wire::write_u8(&mut writer, u8::from(packet.htype))?; 682 wire::write_u8(&mut writer, u8::from(packet.htype))?;
358 wire::write_u8(&mut writer, packet.htype.hardware_len())?; 683 wire::write_u8(&mut writer, packet.htype.hardware_len())?;
@@ -365,10 +690,21 @@ pub fn write_packet<W: Write>(mut writer: W, packet: &DhcpPacket) -> Result<()>
365 wire::write_ipv4(&mut writer, packet.siaddr)?; 690 wire::write_ipv4(&mut writer, packet.siaddr)?;
366 wire::write_ipv4(&mut writer, packet.giaddr)?; 691 wire::write_ipv4(&mut writer, packet.giaddr)?;
367 wire::write(&mut writer, &packet.chaddr)?; 692 wire::write(&mut writer, &packet.chaddr)?;
368 //wire::write_null_terminated_string(&mut writer, &packet.sname)?; 693
369 //wire::write_null_terminated_string(&mut writer, &packet.file)?; 694 let sname_bytes = packet.sname.as_bytes();
370 wire::write(&mut writer, &vec![0u8; 64])?; 695 wire::write(&mut writer, sname_bytes)?;
371 wire::write(&mut writer, &vec![0u8; 128])?; 696 for _ in 0..(64 - sname_bytes.len()) {
697 wire::write_u8(&mut writer, 0)?;
698 }
699
700 let file_bytes = packet.file.as_bytes();
701 wire::write(&mut writer, file_bytes)?;
702 for _ in 0..(128 - file_bytes.len()) {
703 wire::write_u8(&mut writer, 0)?;
704 }
705
706 // wire::write(&mut writer, &vec![0u8; 64])?;
707 // wire::write(&mut writer, &vec![0u8; 128])?;
372 wire::write(&mut writer, &MAGIC_COOKIE)?; 708 wire::write(&mut writer, &MAGIC_COOKIE)?;
373 for option in &packet.options { 709 for option in &packet.options {
374 write_option(&mut writer, option)?; 710 write_option(&mut writer, option)?;
@@ -390,12 +726,16 @@ pub fn write_option<W: Write>(mut writer: W, option: &DhcpOption) -> Result<()>
390 wire::write_ipv4(&mut writer, *ip)?; 726 wire::write_ipv4(&mut writer, *ip)?;
391 } 727 }
392 DhcpOption::VendorClassIdentifier(vendor_class) => { 728 DhcpOption::VendorClassIdentifier(vendor_class) => {
393 write_option_len_prefixed_string(&mut writer, &vendor_class)? 729 write_option_len_prefixed_buf(&mut writer, &vendor_class)?
394 } 730 }
395 DhcpOption::TftpServerName(name) => write_option_len_prefixed_string(&mut writer, &name)?, 731 DhcpOption::TftpServerName(name) => write_option_len_prefixed_string(&mut writer, &name)?,
396 DhcpOption::TftpFileName(name) => write_option_len_prefixed_string(&mut writer, &name)?, 732 DhcpOption::TftpFileName(name) => write_option_len_prefixed_string(&mut writer, &name)?,
397 DhcpOption::UserClassInformation(user_class) => { 733 DhcpOption::UserClassInformation(user_class) => {
398 write_option_len_prefixed_string(&mut writer, &user_class)? 734 write_option_len_prefixed_buf(&mut writer, &user_class)?
735 }
736 DhcpOption::ClientSystemArchitecture(arch) => {
737 wire::write_u8(&mut writer, 2)?;
738 wire::write_u16(&mut writer, u16::from(*arch))?;
399 } 739 }
400 DhcpOption::ClientMachineIdentifier(identifier) => { 740 DhcpOption::ClientMachineIdentifier(identifier) => {
401 write_option_len_prefixed_buf(&mut writer, &identifier)? 741 write_option_len_prefixed_buf(&mut writer, &identifier)?
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];
diff --git a/src/tftp.rs b/src/tftp.rs
index d986a44..72bac22 100644
--- a/src/tftp.rs
+++ b/src/tftp.rs
@@ -1,7 +1,7 @@
1use std::{ 1use std::{
2 collections::HashMap,
2 io::{Cursor, Read as _, Result, Write}, 3 io::{Cursor, Read as _, Result, Write},
3 net::UdpSocket, 4 net::SocketAddr,
4 path::{Path, PathBuf},
5 str::FromStr, 5 str::FromStr,
6}; 6};
7 7
@@ -9,6 +9,9 @@ use crate::wire;
9 9
10pub const PORT: u16 = 69; 10pub const PORT: u16 = 69;
11 11
12const DEFAULT_BLOCK_SIZE: u64 = 512;
13const MAX_BLOCK_SIZE: usize = 2048;
14
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct InvalidTftpOp(u16); 16pub struct InvalidTftpOp(u16);
14 17
@@ -91,7 +94,7 @@ impl FromStr for TftpMode {
91 94
92#[derive(Debug)] 95#[derive(Debug)]
93pub enum TftpPacket { 96pub enum TftpPacket {
94 Request(TftpRequestPacket), 97 ReadRequest(TftpReadRequestPacket),
95 Data(TftpDataPacket), 98 Data(TftpDataPacket),
96 Ack(TftpAckPacket), 99 Ack(TftpAckPacket),
97 OAck(TftpOAckPacket), 100 OAck(TftpOAckPacket),
@@ -99,9 +102,13 @@ pub enum TftpPacket {
99} 102}
100 103
101impl TftpPacket { 104impl TftpPacket {
105 pub fn parse(buf: &[u8]) -> Result<Self> {
106 parse_packet(buf)
107 }
108
102 pub fn write<W: Write>(&self, writer: W) -> Result<()> { 109 pub fn write<W: Write>(&self, writer: W) -> Result<()> {
103 match self { 110 match self {
104 TftpPacket::Request(p) => p.write(writer), 111 TftpPacket::ReadRequest(p) => p.write(writer),
105 TftpPacket::Data(p) => p.write(writer), 112 TftpPacket::Data(p) => p.write(writer),
106 TftpPacket::Ack(p) => p.write(writer), 113 TftpPacket::Ack(p) => p.write(writer),
107 TftpPacket::OAck(p) => p.write(writer), 114 TftpPacket::OAck(p) => p.write(writer),
@@ -111,14 +118,14 @@ impl TftpPacket {
111} 118}
112 119
113#[derive(Debug)] 120#[derive(Debug)]
114pub struct TftpRequestPacket { 121pub struct TftpReadRequestPacket {
115 pub filename: String, 122 pub filename: String,
116 pub mode: TftpMode, 123 pub mode: TftpMode,
117 pub tsize: Option<u64>, 124 pub tsize: Option<u64>,
118 pub blksize: Option<u64>, 125 pub blksize: Option<u64>,
119} 126}
120 127
121impl TftpRequestPacket { 128impl TftpReadRequestPacket {
122 pub fn write<W: Write>(&self, mut writer: W) -> Result<()> { 129 pub fn write<W: Write>(&self, mut writer: W) -> Result<()> {
123 todo!() 130 todo!()
124 } 131 }
@@ -183,16 +190,85 @@ impl TftpOAckPacket {
183 } 190 }
184} 191}
185 192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
194pub enum TftpErrorCode {
195 Undefined,
196 FileNotFound,
197 AccessViolation,
198 DiskFull,
199 IllegalOperation,
200 UnknownTransferId,
201 FileAreadyExists,
202 NoSuchUser,
203 Unknown(u16),
204}
205
206impl std::fmt::Display for TftpErrorCode {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 write!(f, "{:?} ({})", *self, u16::from(*self))
209 }
210}
211
212impl TftpErrorCode {
213 pub const CODE_UNDEFINED: u16 = 0;
214 pub const CODE_FILE_NOT_FOUND: u16 = 1;
215 pub const CODE_ACCESS_VIOLATION: u16 = 2;
216 pub const CODE_DISK_FULL: u16 = 3;
217 pub const CODE_ILLEGAL_OPERATION: u16 = 4;
218 pub const CODE_UNKNOWN_TRANSFER_ID: u16 = 5;
219 pub const CODE_FILE_ALREADY_EXISTS: u16 = 6;
220 pub const CODE_NO_SUCH_USER: u16 = 7;
221}
222
223impl From<u16> for TftpErrorCode {
224 fn from(value: u16) -> Self {
225 match value {
226 Self::CODE_UNDEFINED => Self::Undefined,
227 Self::CODE_FILE_NOT_FOUND => Self::FileNotFound,
228 Self::CODE_ACCESS_VIOLATION => Self::AccessViolation,
229 Self::CODE_DISK_FULL => Self::DiskFull,
230 Self::CODE_ILLEGAL_OPERATION => Self::IllegalOperation,
231 Self::CODE_UNKNOWN_TRANSFER_ID => Self::UnknownTransferId,
232 Self::CODE_FILE_ALREADY_EXISTS => Self::FileAreadyExists,
233 Self::CODE_NO_SUCH_USER => Self::NoSuchUser,
234 unknown => Self::Unknown(unknown),
235 }
236 }
237}
238
239impl From<TftpErrorCode> for u16 {
240 fn from(value: TftpErrorCode) -> Self {
241 match value {
242 TftpErrorCode::Undefined => TftpErrorCode::CODE_UNDEFINED,
243 TftpErrorCode::FileNotFound => TftpErrorCode::CODE_FILE_NOT_FOUND,
244 TftpErrorCode::AccessViolation => TftpErrorCode::CODE_ACCESS_VIOLATION,
245 TftpErrorCode::DiskFull => TftpErrorCode::CODE_DISK_FULL,
246 TftpErrorCode::IllegalOperation => TftpErrorCode::CODE_ILLEGAL_OPERATION,
247 TftpErrorCode::UnknownTransferId => TftpErrorCode::CODE_UNKNOWN_TRANSFER_ID,
248 TftpErrorCode::FileAreadyExists => TftpErrorCode::CODE_FILE_ALREADY_EXISTS,
249 TftpErrorCode::NoSuchUser => TftpErrorCode::CODE_NO_SUCH_USER,
250 TftpErrorCode::Unknown(code) => code,
251 }
252 }
253}
254
186#[derive(Debug)] 255#[derive(Debug)]
187pub struct TftpErrorPacket { 256pub struct TftpErrorPacket {
188 pub code: u16, 257 pub code: TftpErrorCode,
189 pub message: String, 258 pub message: String,
190} 259}
191 260
192impl TftpErrorPacket { 261impl TftpErrorPacket {
262 pub fn new(code: TftpErrorCode, message: impl Into<String>) -> Self {
263 Self {
264 code,
265 message: message.into(),
266 }
267 }
268
193 pub fn write<W: Write>(&self, mut writer: W) -> Result<()> { 269 pub fn write<W: Write>(&self, mut writer: W) -> Result<()> {
194 wire::write_u16(&mut writer, TftpOp::Error.into())?; 270 wire::write_u16(&mut writer, TftpOp::Error.into())?;
195 wire::write_u16(&mut writer, self.code)?; 271 wire::write_u16(&mut writer, u16::from(self.code))?;
196 wire::write_null_terminated_string(&mut writer, &self.message)?; 272 wire::write_null_terminated_string(&mut writer, &self.message)?;
197 Ok(()) 273 Ok(())
198 } 274 }
@@ -224,7 +300,7 @@ pub fn parse_packet(buf: &[u8]) -> Result<TftpPacket> {
224 } 300 }
225 } 301 }
226 302
227 Ok(TftpPacket::Request(TftpRequestPacket { 303 Ok(TftpPacket::ReadRequest(TftpReadRequestPacket {
228 filename, 304 filename,
229 mode, 305 mode,
230 tsize, 306 tsize,
@@ -243,7 +319,7 @@ pub fn parse_packet(buf: &[u8]) -> Result<TftpPacket> {
243 Ok(TftpPacket::Ack(TftpAckPacket { block })) 319 Ok(TftpPacket::Ack(TftpAckPacket { block }))
244 } 320 }
245 TftpOp::Error => { 321 TftpOp::Error => {
246 let code = wire::read_u16(&mut cursor)?; 322 let code = TftpErrorCode::from(wire::read_u16(&mut cursor)?);
247 let message = wire::read_null_terminated_string(&mut cursor)?; 323 let message = wire::read_null_terminated_string(&mut cursor)?;
248 Ok(TftpPacket::Error(TftpErrorPacket { code, message })) 324 Ok(TftpPacket::Error(TftpErrorPacket { code, message }))
249 } 325 }
@@ -267,101 +343,222 @@ pub fn parse_packet(buf: &[u8]) -> Result<TftpPacket> {
267 } 343 }
268} 344}
269 345
270pub fn serve(dir: impl AsRef<Path>) -> Result<()> { 346pub trait FileSystem {
271 let dir = dir.as_ref(); 347 fn stat(&self, filename: &str) -> Result<u64>;
272 let socket = UdpSocket::bind(format!("0.0.0.0:{PORT}"))?; 348 fn read(&self, filename: &str, offset: u64, buf: &mut [u8]) -> Result<u64>;
349}
273 350
274 // TODO: this needs to be done per addr 351#[derive(Debug)]
275 let mut last_blksize = 512u64; 352pub struct StaticFileSystem {
276 let mut current_file = PathBuf::default(); 353 files: &'static [(&'static str, &'static [u8])],
354}
355
356impl StaticFileSystem {
357 pub fn new(files: &'static [(&'static str, &'static [u8])]) -> Self {
358 Self { files }
359 }
277 360
278 loop { 361 fn find_file(&self, filename: &str) -> Result<&'static [u8]> {
279 let mut buf = [0u8; 1500]; 362 self.files
280 let (n, addr) = socket.recv_from(&mut buf)?; 363 .iter()
281 let packet = parse_packet(&buf[..n]).unwrap(); 364 .find(|(name, _)| *name == filename)
365 .map(|(_, contents)| *contents)
366 .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"))
367 }
368}
369
370impl FileSystem for StaticFileSystem {
371 fn stat(&self, filename: &str) -> Result<u64> {
372 let file = self.find_file(filename)?;
373 Ok(u64::try_from(file.len()).unwrap())
374 }
375
376 fn read(&self, filename: &str, offset: u64, buf: &mut [u8]) -> Result<u64> {
377 let file = self.find_file(filename)?;
378 let offset = usize::try_from(offset).unwrap();
379 if offset >= file.len() {
380 return Ok(0);
381 }
382
383 let rem = &file[offset..];
384 let copy_n = rem.len().min(buf.len());
385 buf[..copy_n].copy_from_slice(&rem[..copy_n]);
386 Ok(u64::try_from(copy_n).unwrap())
387 }
388}
282 389
283 let response = match packet { 390#[derive(Debug)]
284 TftpPacket::Request(req) => { 391struct Client {
392 blksize: u64,
393 filename: Option<String>,
394}
395
396impl Default for Client {
397 fn default() -> Self {
398 Self {
399 blksize: DEFAULT_BLOCK_SIZE,
400 filename: None,
401 }
402 }
403}
404
405pub enum ServerCommand {
406 Send(TftpPacket),
407 Ignore,
408}
409
410impl ServerCommand {
411 fn error(code: TftpErrorCode, message: impl Into<String>) -> Self {
412 Self::Send(TftpPacket::Error(TftpErrorPacket::new(code, message)))
413 }
414}
415
416#[derive(Debug, Default)]
417pub struct Server {
418 clients: HashMap<SocketAddr, Client>,
419}
420
421impl Server {
422 pub fn process(
423 &mut self,
424 fs: &dyn FileSystem,
425 source: SocketAddr,
426 buf: &[u8],
427 ) -> ServerCommand {
428 let packet = match TftpPacket::parse(buf) {
429 Ok(packet) => packet,
430 Err(err) => {
431 return ServerCommand::Send(TftpPacket::Error(TftpErrorPacket::new(
432 TftpErrorCode::Undefined,
433 format!("invalid packet: {err}"),
434 )));
435 }
436 };
437
438 match packet {
439 TftpPacket::ReadRequest(req) => self.process_read_req(fs, source, &req),
440 TftpPacket::Ack(ack) => self.process_ack(fs, source, &ack),
441 TftpPacket::Error(err) => {
285 println!( 442 println!(
286 "Request options: tsize={:?}, blksize={:?}", 443 "received error from client {}: ({}) {}",
287 req.tsize, req.blksize 444 source, err.code, err.message
288 ); 445 );
446 self.clients.remove(&source);
447 ServerCommand::Ignore
448 }
449 TftpPacket::Data(_) | TftpPacket::OAck(_) => ServerCommand::Ignore,
450 }
451 }
289 452
290 let filepath = dir.join(req.filename); 453 fn process_read_req(
291 current_file = filepath.clone(); 454 &mut self,
292 let meta = std::fs::metadata(&filepath).unwrap(); 455 fs: &dyn FileSystem,
293 let actual_file_size = meta.len(); 456 source: SocketAddr,
457 req: &TftpReadRequestPacket,
458 ) -> ServerCommand {
459 println!(
460 "Request options: tsize={:?}, blksize={:?}",
461 req.tsize, req.blksize
462 );
463
464 let client = self.clients.entry(source).or_default();
465 client.filename = Some(req.filename.clone());
466
467 // Only send OACK if client sent options
468 if req.tsize.is_some() || req.blksize.is_some() {
469 if let Some(blksize) = req.blksize {
470 client.blksize = blksize;
471 }
294 472
295 // Only send OACK if client sent options 473 let tsize_response = if req.tsize.is_some() {
296 if req.tsize.is_some() || req.blksize.is_some() { 474 let filesize = match fs.stat(&req.filename) {
297 if let Some(blksize) = req.blksize { 475 Ok(filesize) => filesize,
298 last_blksize = blksize; 476 Err(err) => {
477 return ServerCommand::error(
478 TftpErrorCode::Undefined,
479 format!("failed to obtain file size: {}", err),
480 );
299 } 481 }
482 };
300 483
301 let tsize_response = if req.tsize.is_some() { 484 Some(filesize)
302 Some(actual_file_size) 485 } else {
303 } else { 486 None
304 None 487 };
305 }; 488
306 489 ServerCommand::Send(TftpPacket::OAck(TftpOAckPacket {
307 Some(TftpPacket::OAck(TftpOAckPacket { 490 tsize: tsize_response,
308 tsize: tsize_response, 491 blksize: req.blksize,
309 blksize: req.blksize, 492 }))
310 })) 493 } else {
311 } else { 494 // No options, send first data block directly
312 // No options, send first data block directly 495 let options = self.clients.entry(source).or_default();
313 let contents = std::fs::read(&filepath).unwrap(); 496 let block_size = usize::try_from(options.blksize).unwrap();
314 let block_size = 512; 497
315 let first_block = if contents.len() > block_size { 498 assert!(block_size <= MAX_BLOCK_SIZE);
316 contents[..block_size].to_vec() 499 let mut contents = [0u8; MAX_BLOCK_SIZE];
317 } else { 500 let contents = &mut contents[..block_size];
318 contents 501
319 }; 502 let n = match fs.read(&req.filename, 0, contents) {
320 503 Ok(n) => usize::try_from(n).unwrap(),
321 Some(TftpPacket::Data(TftpDataPacket { 504 Err(err) => {
322 block: 1, 505 return ServerCommand::error(
323 data: first_block, 506 TftpErrorCode::Undefined,
324 })) 507 format!("failed to read file contents: {}", err),
325 } 508 );
326 }
327 TftpPacket::Data(dat) => unimplemented!(),
328 TftpPacket::Ack(ack) => {
329 println!("Received ACK packet: block {}", ack.block);
330
331 let contents = std::fs::read(&current_file).unwrap();
332 let next_block = ack.block + 1;
333 let start_offset = (next_block - 1) as u64 * last_blksize;
334 let end_offset = next_block as u64 * last_blksize;
335 let prev_start_offset = (next_block.saturating_sub(2)) as u64 * last_blksize;
336 let prev_remain = contents.len() - prev_start_offset as usize;
337 if prev_remain as u64 >= last_blksize || ack.block == 0 {
338 let end = std::cmp::min(end_offset as usize, contents.len());
339 let block_data = contents[start_offset as usize..end].to_vec();
340 println!("sending tftp data packet with {} bytes", block_data.len());
341 Some(TftpPacket::Data(TftpDataPacket {
342 block: next_block,
343 data: block_data,
344 }))
345 } else {
346 None
347 } 509 }
510 };
511
512 ServerCommand::Send(TftpPacket::Data(TftpDataPacket {
513 block: 1,
514 data: contents[..n].to_vec(),
515 }))
516 }
517 }
518
519 fn process_ack(
520 &mut self,
521 fs: &dyn FileSystem,
522 source: SocketAddr,
523 ack: &TftpAckPacket,
524 ) -> ServerCommand {
525 println!("Received ACK packet: block {}", ack.block);
526
527 let client = self.clients.entry(source).or_default();
528 let filename = match &client.filename {
529 Some(filename) => filename,
530 None => {
531 return ServerCommand::error(
532 TftpErrorCode::Undefined,
533 "unknown filename for client",
534 );
348 } 535 }
349 TftpPacket::OAck(ack) => todo!(), 536 };
350 TftpPacket::Error(err) => { 537 let block_size = usize::try_from(client.blksize).unwrap();
351 println!( 538
352 "Received ERROR packet: code {}, message: {}", 539 let next_block = ack.block + 1;
353 err.code, err.message 540 let start_offset = (usize::from(next_block) - 1) * block_size;
541
542 let mut contents = [0u8; MAX_BLOCK_SIZE];
543 let contents = &mut contents[..block_size];
544 let n = match fs.read(filename, u64::try_from(start_offset).unwrap(), contents) {
545 Ok(n) => usize::try_from(n).unwrap(),
546 Err(err) => {
547 return ServerCommand::error(
548 TftpErrorCode::Undefined,
549 format!("failed to read file contents: {}", err),
354 ); 550 );
355 None
356 } 551 }
357 }; 552 };
553 let contents = &contents[..n];
358 554
359 if let Some(response) = response { 555 if contents.is_empty() {
360 let mut writer = Cursor::new(&mut buf[..]); 556 return ServerCommand::Ignore;
361 println!("Sending to {addr}: {response:#?}");
362 response.write(&mut writer).unwrap();
363 let (response, _) = writer.split();
364 socket.send_to(&response, addr).unwrap();
365 } 557 }
558
559 ServerCommand::Send(TftpPacket::Data(TftpDataPacket {
560 block: next_block,
561 data: contents.to_vec(),
562 }))
366 } 563 }
367} 564}
diff --git a/tftp/ipxe.efi b/tftp/ipxe.efi
deleted file mode 100644
index d1c12b3..0000000
--- a/tftp/ipxe.efi
+++ /dev/null
Binary files differ
diff --git a/tftp/test.ipxe b/tftp/test.ipxe
deleted file mode 100644
index f4ee267..0000000
--- a/tftp/test.ipxe
+++ /dev/null
@@ -1,25 +0,0 @@
1#!ipxe
2
3:start
4#console --picture http://boot.ipxe.org/ipxe.png
5menu debian
6item --gap -- ---------------------- Net installer -----------------------------
7item --key 3 Debian9_x86 Debian 9 (3)2-bit net install
8item --key 6 Debian9_x86_64 Debian 9 (6)4-bit net install
9item --gap -- ------------------------- Options --------------------------------
10item --key g goback (G)o back to previous menu
11choose version && goto ${version} || goto start
12
13:Debian9_x86
14echo Booting Debian 9 32-bit
15kernel http://deb.debian.org/debian/dists/stretch/main/installer-i386/current/images/netboot/debian-installer/i386/linux initrd=initrd.gz
16initrd http://deb.debian.org/debian/dists/stretch/main/installer-i386/current/images/netboot/debian-installer/i386/initrd.gz
17boot || imgfree
18goto start
19
20:Debian9_x86_64
21echo Booting Debian 9 64-bit
22kernel http://deb.debian.org/debian/dists/trixie/main/installer-amd64/current/images/netboot/debian-installer/amd64/linux initrd=initrd.gz
23initrd http://deb.debian.org/debian/dists/trixie/main/installer-amd64/current/images/netboot/debian-installer/amd64/initrd.gz
24boot || imgfree
25goto start