diff options
| author | diogo464 <[email protected]> | 2025-10-11 11:34:59 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-10-11 11:34:59 +0100 |
| commit | 521218ce06fbb7bd518eb6a069406936079e3ec2 (patch) | |
| tree | 862e84ec23175119abb7652e197a4113e7fcc31b | |
| parent | 89db26c86bf48a4c527778fc254765a38b7e9085 (diff) | |
initial working version
| -rw-r--r-- | Cargo.lock | 242 | ||||
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | menu.ipxe | 121 | ||||
| -rw-r--r-- | netboot.xyz-arm64.efi | bin | 0 -> 1110528 bytes | |||
| -rw-r--r-- | netboot.xyz.efi | bin | 0 -> 1118720 bytes | |||
| -rw-r--r-- | netboot.xyz.kpxe | bin | 0 -> 384280 bytes | |||
| -rw-r--r-- | pxespec.pdf | bin | 0 -> 502352 bytes | |||
| -rw-r--r-- | src/dhcp.rs | 398 | ||||
| -rw-r--r-- | src/main.rs | 408 | ||||
| -rw-r--r-- | src/tftp.rs | 377 | ||||
| -rw-r--r-- | tftp/ipxe.efi | bin | 1044480 -> 0 bytes | |||
| -rw-r--r-- | tftp/test.ipxe | 25 |
12 files changed, 1283 insertions, 289 deletions
| @@ -3,12 +3,120 @@ | |||
| 3 | version = 4 | 3 | version = 4 |
| 4 | 4 | ||
| 5 | [[package]] | 5 | [[package]] |
| 6 | name = "anstream" | ||
| 7 | version = "0.6.21" | ||
| 8 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 9 | checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" | ||
| 10 | dependencies = [ | ||
| 11 | "anstyle", | ||
| 12 | "anstyle-parse", | ||
| 13 | "anstyle-query", | ||
| 14 | "anstyle-wincon", | ||
| 15 | "colorchoice", | ||
| 16 | "is_terminal_polyfill", | ||
| 17 | "utf8parse", | ||
| 18 | ] | ||
| 19 | |||
| 20 | [[package]] | ||
| 21 | name = "anstyle" | ||
| 22 | version = "1.0.13" | ||
| 23 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 24 | checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" | ||
| 25 | |||
| 26 | [[package]] | ||
| 27 | name = "anstyle-parse" | ||
| 28 | version = "0.2.7" | ||
| 29 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 30 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" | ||
| 31 | dependencies = [ | ||
| 32 | "utf8parse", | ||
| 33 | ] | ||
| 34 | |||
| 35 | [[package]] | ||
| 36 | name = "anstyle-query" | ||
| 37 | version = "1.1.4" | ||
| 38 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 39 | checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" | ||
| 40 | dependencies = [ | ||
| 41 | "windows-sys", | ||
| 42 | ] | ||
| 43 | |||
| 44 | [[package]] | ||
| 45 | name = "anstyle-wincon" | ||
| 46 | version = "3.0.10" | ||
| 47 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 48 | checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" | ||
| 49 | dependencies = [ | ||
| 50 | "anstyle", | ||
| 51 | "once_cell_polyfill", | ||
| 52 | "windows-sys", | ||
| 53 | ] | ||
| 54 | |||
| 55 | [[package]] | ||
| 56 | name = "clap" | ||
| 57 | version = "4.5.48" | ||
| 58 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 59 | checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" | ||
| 60 | dependencies = [ | ||
| 61 | "clap_builder", | ||
| 62 | "clap_derive", | ||
| 63 | ] | ||
| 64 | |||
| 65 | [[package]] | ||
| 66 | name = "clap_builder" | ||
| 67 | version = "4.5.48" | ||
| 68 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 69 | checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" | ||
| 70 | dependencies = [ | ||
| 71 | "anstream", | ||
| 72 | "anstyle", | ||
| 73 | "clap_lex", | ||
| 74 | "strsim", | ||
| 75 | ] | ||
| 76 | |||
| 77 | [[package]] | ||
| 78 | name = "clap_derive" | ||
| 79 | version = "4.5.47" | ||
| 80 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 81 | checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" | ||
| 82 | dependencies = [ | ||
| 83 | "heck", | ||
| 84 | "proc-macro2", | ||
| 85 | "quote", | ||
| 86 | "syn", | ||
| 87 | ] | ||
| 88 | |||
| 89 | [[package]] | ||
| 90 | name = "clap_lex" | ||
| 91 | version = "0.7.5" | ||
| 92 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 93 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" | ||
| 94 | |||
| 95 | [[package]] | ||
| 96 | name = "colorchoice" | ||
| 97 | version = "1.0.4" | ||
| 98 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 99 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" | ||
| 100 | |||
| 101 | [[package]] | ||
| 102 | name = "heck" | ||
| 103 | version = "0.5.0" | ||
| 104 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 105 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" | ||
| 106 | |||
| 107 | [[package]] | ||
| 6 | name = "ipnet" | 108 | name = "ipnet" |
| 7 | version = "2.11.0" | 109 | version = "2.11.0" |
| 8 | source = "registry+https://github.com/rust-lang/crates.io-index" | 110 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| 9 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" | 111 | checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" |
| 10 | 112 | ||
| 11 | [[package]] | 113 | [[package]] |
| 114 | name = "is_terminal_polyfill" | ||
| 115 | version = "1.70.1" | ||
| 116 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 117 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" | ||
| 118 | |||
| 119 | [[package]] | ||
| 12 | name = "libc" | 120 | name = "libc" |
| 13 | version = "0.2.176" | 121 | version = "0.2.176" |
| 14 | source = "registry+https://github.com/rust-lang/crates.io-index" | 122 | source = "registry+https://github.com/rust-lang/crates.io-index" |
| @@ -18,6 +126,140 @@ checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" | |||
| 18 | name = "netiso" | 126 | name = "netiso" |
| 19 | version = "0.1.0" | 127 | version = "0.1.0" |
| 20 | dependencies = [ | 128 | dependencies = [ |
| 129 | "clap", | ||
| 21 | "ipnet", | 130 | "ipnet", |
| 22 | "libc", | 131 | "libc", |
| 23 | ] | 132 | ] |
| 133 | |||
| 134 | [[package]] | ||
| 135 | name = "once_cell_polyfill" | ||
| 136 | version = "1.70.1" | ||
| 137 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 138 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" | ||
| 139 | |||
| 140 | [[package]] | ||
| 141 | name = "proc-macro2" | ||
| 142 | version = "1.0.101" | ||
| 143 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 144 | checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" | ||
| 145 | dependencies = [ | ||
| 146 | "unicode-ident", | ||
| 147 | ] | ||
| 148 | |||
| 149 | [[package]] | ||
| 150 | name = "quote" | ||
| 151 | version = "1.0.41" | ||
| 152 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 153 | checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" | ||
| 154 | dependencies = [ | ||
| 155 | "proc-macro2", | ||
| 156 | ] | ||
| 157 | |||
| 158 | [[package]] | ||
| 159 | name = "strsim" | ||
| 160 | version = "0.11.1" | ||
| 161 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 162 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" | ||
| 163 | |||
| 164 | [[package]] | ||
| 165 | name = "syn" | ||
| 166 | version = "2.0.106" | ||
| 167 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 168 | checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" | ||
| 169 | dependencies = [ | ||
| 170 | "proc-macro2", | ||
| 171 | "quote", | ||
| 172 | "unicode-ident", | ||
| 173 | ] | ||
| 174 | |||
| 175 | [[package]] | ||
| 176 | name = "unicode-ident" | ||
| 177 | version = "1.0.19" | ||
| 178 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 179 | checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" | ||
| 180 | |||
| 181 | [[package]] | ||
| 182 | name = "utf8parse" | ||
| 183 | version = "0.2.2" | ||
| 184 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 185 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" | ||
| 186 | |||
| 187 | [[package]] | ||
| 188 | name = "windows-link" | ||
| 189 | version = "0.2.1" | ||
| 190 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 191 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" | ||
| 192 | |||
| 193 | [[package]] | ||
| 194 | name = "windows-sys" | ||
| 195 | version = "0.60.2" | ||
| 196 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 197 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" | ||
| 198 | dependencies = [ | ||
| 199 | "windows-targets", | ||
| 200 | ] | ||
| 201 | |||
| 202 | [[package]] | ||
| 203 | name = "windows-targets" | ||
| 204 | version = "0.53.5" | ||
| 205 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 206 | checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" | ||
| 207 | dependencies = [ | ||
| 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]] | ||
| 220 | name = "windows_aarch64_gnullvm" | ||
| 221 | version = "0.53.1" | ||
| 222 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 223 | checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" | ||
| 224 | |||
| 225 | [[package]] | ||
| 226 | name = "windows_aarch64_msvc" | ||
| 227 | version = "0.53.1" | ||
| 228 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 229 | checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" | ||
| 230 | |||
| 231 | [[package]] | ||
| 232 | name = "windows_i686_gnu" | ||
| 233 | version = "0.53.1" | ||
| 234 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 235 | checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" | ||
| 236 | |||
| 237 | [[package]] | ||
| 238 | name = "windows_i686_gnullvm" | ||
| 239 | version = "0.53.1" | ||
| 240 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 241 | checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" | ||
| 242 | |||
| 243 | [[package]] | ||
| 244 | name = "windows_i686_msvc" | ||
| 245 | version = "0.53.1" | ||
| 246 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 247 | checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" | ||
| 248 | |||
| 249 | [[package]] | ||
| 250 | name = "windows_x86_64_gnu" | ||
| 251 | version = "0.53.1" | ||
| 252 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 253 | checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" | ||
| 254 | |||
| 255 | [[package]] | ||
| 256 | name = "windows_x86_64_gnullvm" | ||
| 257 | version = "0.53.1" | ||
| 258 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 259 | checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" | ||
| 260 | |||
| 261 | [[package]] | ||
| 262 | name = "windows_x86_64_msvc" | ||
| 263 | version = "0.53.1" | ||
| 264 | source = "registry+https://github.com/rust-lang/crates.io-index" | ||
| 265 | checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" | ||
| @@ -4,5 +4,6 @@ version = "0.1.0" | |||
| 4 | edition = "2024" | 4 | edition = "2024" |
| 5 | 5 | ||
| 6 | [dependencies] | 6 | [dependencies] |
| 7 | clap = { version = "4.5.48", features = ["env", "derive"] } | ||
| 7 | ipnet = "2.11.0" | 8 | ipnet = "2.11.0" |
| 8 | libc = "0.2.176" | 9 | libc = "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 | ||
| 4 | isset ${arch} && goto skip_arch_detect || | ||
| 5 | cpuid --ext 29 && set arch x86_64 || set arch i386 | ||
| 6 | iseq ${buildarch} arm64 && set arch arm64 || | ||
| 7 | :skip_arch_detect | ||
| 8 | chain --autofree boot.cfg || | ||
| 9 | echo Attempting to retrieve latest upstream version number... | ||
| 10 | chain --timeout 5000 https://boot.netboot.xyz/version.ipxe || | ||
| 11 | ntp 0.pool.ntp.org || | ||
| 12 | iseq ${cls} serial && goto ignore_cls || | ||
| 13 | set cls:hex 1b:5b:4a # ANSI clear screen sequence - "^[[J" | ||
| 14 | set cls ${cls:string} | ||
| 15 | :ignore_cls | ||
| 16 | |||
| 17 | :version_check | ||
| 18 | set latest_version 2.x | ||
| 19 | echo ${cls} | ||
| 20 | iseq ${version} ${latest_version} && goto version_up2date || | ||
| 21 | echo | ||
| 22 | echo Updated version of netboot.xyz is available: | ||
| 23 | echo | ||
| 24 | echo Running version.....${version} | ||
| 25 | echo Updated version.....${latest_version} | ||
| 26 | echo | ||
| 27 | echo Please download the latest version from netboot.xyz. | ||
| 28 | echo | ||
| 29 | echo Attempting to chain to latest version... | ||
| 30 | chain --autofree http://${boot_domain}/ipxe/${ipxe_disk} || | ||
| 31 | :version_up2date | ||
| 32 | |||
| 33 | isset ${menu} && goto ${menu} || | ||
| 34 | isset ${ip} || dhcp | ||
| 35 | |||
| 36 | :main_menu | ||
| 37 | clear menu | ||
| 38 | set space:hex 20:20 | ||
| 39 | set space ${space:string} | ||
| 40 | isset ${next-server} && menu ${site_name} v${version} - next-server: ${next-server} || menu ${site_name} | ||
| 41 | item --gap Default: | ||
| 42 | item local ${space} Boot from local hdd | ||
| 43 | item --gap Distributions: | ||
| 44 | iseq ${menu_linux} 1 && item linux ${space} Linux Network Installs (64-bit) || | ||
| 45 | iseq ${menu_linux_i386} 1 && item linux-i386 ${space} Linux Network Installs (32-bit) || | ||
| 46 | iseq ${menu_linux_arm} 1 && item linux-arm ${space} Linux Network Installs (arm64) || | ||
| 47 | iseq ${menu_live} 1 && item live ${space} Live CDs || | ||
| 48 | iseq ${menu_live_arm} 1 && item live-arm ${space} Live CDs || | ||
| 49 | iseq ${menu_bsd} 1 && item bsd ${space} BSD Installs || | ||
| 50 | iseq ${menu_unix} 1 && item unix ${space} Unix Network Installs || | ||
| 51 | iseq ${menu_freedos} 1 && item freedos ${space} FreeDOS || | ||
| 52 | iseq ${menu_windows} 1 && item windows ${space} Windows || | ||
| 53 | item --gap Tools: | ||
| 54 | iseq ${menu_utils} 1 && iseq ${platform} efi && item utils-efi ${space} Utilities (UEFI) || | ||
| 55 | iseq ${menu_utils} 1 && iseq ${platform} pcbios && iseq ${arch} x86_64 && item utils-pcbios-64 ${space} Utilities (64-bit) || | ||
| 56 | iseq ${menu_utils} 1 && iseq ${platform} pcbios && iseq ${arch} i386 && item utils-pcbios-32 ${space} Utilities (32-bit) || | ||
| 57 | iseq ${menu_utils_arm} 1 && item utils-arm ${space} Utilities (arm64) || | ||
| 58 | item change_arch ${space} Architecture: ${arch} | ||
| 59 | item shell ${space} iPXE shell | ||
| 60 | item netinfo ${space} Network card info | ||
| 61 | iseq ${menu_pci} 1 && item lspci ${space} PCI Device List || | ||
| 62 | item about ${space} About netboot.xyz | ||
| 63 | item --gap Signature Checks: | ||
| 64 | item sig_check ${space} netboot.xyz [ enabled: ${sigs_enabled} ] | ||
| 65 | isset ${github_user} && item --gap Custom Github Menu: || | ||
| 66 | isset ${github_user} && item custom-github ${space} ${github_user}'s Custom Menu || | ||
| 67 | isset ${custom_url} && item --gap Custom URL Menu: || | ||
| 68 | isset ${custom_url} && item custom-url ${space} Custom URL Menu || | ||
| 69 | isset ${menu} && set timeout 0 || set timeout ${boot_timeout} | ||
| 70 | choose --timeout ${timeout} --default ${menu} menu || goto local | ||
| 71 | echo ${cls} | ||
| 72 | goto ${menu} || | ||
| 73 | iseq ${sigs_enabled} true && goto verify_sigs || goto change_menu | ||
| 74 | |||
| 75 | :verify_sigs | ||
| 76 | imgverify ${menu}.ipxe ${sigs}${menu}.ipxe.sig || goto error | ||
| 77 | goto change_menu | ||
| 78 | |||
| 79 | :change_menu | ||
| 80 | chain ${menu}.ipxe || goto error | ||
| 81 | goto main_menu | ||
| 82 | |||
| 83 | :error | ||
| 84 | echo Error occurred, press any key to return to menu ... | ||
| 85 | prompt | ||
| 86 | goto main_menu | ||
| 87 | |||
| 88 | :local | ||
| 89 | echo Booting from local disks ... | ||
| 90 | exit 1 | ||
| 91 | |||
| 92 | :shell | ||
| 93 | echo Type "exit" to return to menu. | ||
| 94 | set menu main_menu | ||
| 95 | shell | ||
| 96 | goto main_menu | ||
| 97 | |||
| 98 | :change_arch | ||
| 99 | iseq ${arch} x86_64 && set arch i386 && set menu_linux_i386 1 && set menu_linux 0 && goto main_menu || | ||
| 100 | iseq ${arch} i386 && set arch x86_64 && set menu_linux_i386 0 && set menu_linux 1 && goto main_menu || | ||
| 101 | goto main_menu | ||
| 102 | |||
| 103 | :sig_check | ||
| 104 | iseq ${sigs_enabled} true && set sigs_enabled false || set sigs_enabled true | ||
| 105 | goto main_menu | ||
| 106 | |||
| 107 | :about | ||
| 108 | chain https://boot.netboot.xyz/about.ipxe || chain about.ipxe | ||
| 109 | goto main_menu | ||
| 110 | |||
| 111 | :custom-github | ||
| 112 | chain https://raw.githubusercontent.com/${github_user}/netboot.xyz-custom/master/custom.ipxe || goto error | ||
| 113 | goto main_menu | ||
| 114 | |||
| 115 | :custom-url | ||
| 116 | chain ${custom_url}/custom.ipxe || goto error | ||
| 117 | goto main_menu | ||
| 118 | |||
| 119 | :custom-user | ||
| 120 | chain custom/custom.ipxe | ||
| 121 | goto 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 @@ | |||
| 1 | use std::{ | 1 | use 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 | ||
| 6 | use crate::wire; | 7 | use crate::wire; |
| @@ -8,6 +9,11 @@ use crate::wire; | |||
| 8 | const MAGIC_COOKIE: [u8; 4] = [0x63, 0x82, 0x53, 0x63]; | 9 | const MAGIC_COOKIE: [u8; 4] = [0x63, 0x82, 0x53, 0x63]; |
| 9 | const FLAG_BROADCAST: u16 = 1 << 15; | 10 | const FLAG_BROADCAST: u16 = 1 << 15; |
| 10 | 11 | ||
| 12 | pub const VENDOR_CLASS_PXE_CLIENT: &'static [u8] = b"PXEClient"; | ||
| 13 | pub const VENDOR_CLASS_PXE_SERVER: &'static [u8] = b"PXEServer"; | ||
| 14 | |||
| 15 | pub 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)] |
| 12 | pub enum BootOp { | 18 | pub enum BootOp { |
| 13 | #[default] | 19 | #[default] |
| @@ -29,6 +35,8 @@ impl From<BootOp> for u8 { | |||
| 29 | } | 35 | } |
| 30 | } | 36 | } |
| 31 | 37 | ||
| 38 | pub type HardwareAddress = [u8; 16]; | ||
| 39 | |||
| 32 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] | 40 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] |
| 33 | pub enum HardwareType { | 41 | pub 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)] | ||
| 128 | pub 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 | |||
| 148 | impl 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 | |||
| 167 | impl 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 | |||
| 191 | impl 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 | |||
| 215 | impl 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)] | ||
| 224 | pub struct InvalidPxeClassIdentifierKind(String); | ||
| 225 | |||
| 226 | impl InvalidPxeClassIdentifierKind { | ||
| 227 | fn new(kind: impl Into<String>) -> Self { | ||
| 228 | Self(kind.into()) | ||
| 229 | } | ||
| 230 | } | ||
| 231 | |||
| 232 | impl 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 | |||
| 242 | impl std::error::Error for InvalidPxeClassIdentifierKind {} | ||
| 243 | |||
| 244 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 245 | pub enum PxeClassIdentifierKind { | ||
| 246 | Client, | ||
| 247 | Server, | ||
| 248 | } | ||
| 249 | |||
| 250 | impl PxeClassIdentifierKind { | ||
| 251 | pub const KIND_CLIENT: &'static str = "PXEClient"; | ||
| 252 | pub const KIND_SERVER: &'static str = "PXEServer"; | ||
| 253 | } | ||
| 254 | |||
| 255 | impl 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)] | ||
| 268 | pub struct InvalidPxeClassIdentifier(String, String); | ||
| 269 | |||
| 270 | impl InvalidPxeClassIdentifier { | ||
| 271 | fn new(class: impl Into<String>, reason: impl Into<String>) -> Self { | ||
| 272 | Self(class.into(), reason.into()) | ||
| 273 | } | ||
| 274 | } | ||
| 275 | |||
| 276 | impl 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 | |||
| 282 | impl std::error::Error for InvalidPxeClassIdentifier {} | ||
| 283 | |||
| 284 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 285 | pub enum PxeClassIdentifier { | ||
| 286 | Client(PxeClassIdentifierClient), | ||
| 287 | Server(PxeClassIdentifierServer), | ||
| 288 | } | ||
| 289 | |||
| 290 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| 291 | pub struct PxeClassIdentifierServer; | ||
| 292 | |||
| 293 | impl 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)] | ||
| 300 | pub struct PxeClassIdentifierClient { | ||
| 301 | pub architecture: SystemArchitecture, | ||
| 302 | pub undi_major: u16, | ||
| 303 | pub undi_minor: u16, | ||
| 304 | } | ||
| 305 | |||
| 306 | impl 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 | |||
| 318 | impl 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 | |||
| 327 | impl 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 | |||
| 341 | impl 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)] |
| 117 | pub struct DhcpPacket { | 409 | pub 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 | ||
| 355 | pub fn write_packet<W: Write>(mut writer: W, packet: &DhcpPacket) -> Result<()> { | 666 | pub 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)] |
| 2 | pub mod dhcp; | 3 | pub mod dhcp; |
| 3 | pub mod tftp; | 4 | pub mod tftp; |
| @@ -8,13 +9,273 @@ use std::{ | |||
| 8 | net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}, | 9 | net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}, |
| 9 | }; | 10 | }; |
| 10 | 11 | ||
| 12 | use clap::Parser; | ||
| 11 | use ipnet::Ipv4Net; | 13 | use ipnet::Ipv4Net; |
| 12 | 14 | ||
| 13 | use crate::dhcp::{DhcpOption, DhcpPacket}; | 15 | use crate::dhcp::{DhcpOption, DhcpPacket}; |
| 14 | 16 | ||
| 15 | const LOCAL_IPV4: Ipv4Addr = Ipv4Addr::new(192, 168, 1, 103); | 17 | const BOOT_FILE_X64_BIOS: &'static str = "netboot.xyz.kpxe"; |
| 16 | const LOCAL_HOSTNAME: &'static str = "Diogos-Air"; | 18 | const BOOT_FILE_X64_EFI: &'static str = "netboot.xyz.efi"; |
| 19 | const BOOT_FILE_A64_EFI: &'static str = "netboot.xyz-arm64.efi"; | ||
| 20 | const MENU_FILE: &'static str = "menu.ipxe"; | ||
| 17 | 21 | ||
| 22 | #[derive(Debug, Parser)] | ||
| 23 | struct 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 | |||
| 40 | struct Context { | ||
| 41 | local_hostname: String, | ||
| 42 | local_address: Ipv4Addr, | ||
| 43 | } | ||
| 44 | |||
| 45 | fn 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 | |||
| 140 | fn 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 | |||
| 216 | fn 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)] |
| 19 | struct InterfaceAddr { | 280 | struct 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 | |||
| 72 | fn 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 | |||
| 101 | fn 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 | |||
| 159 | fn 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 | |||
| 192 | const 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 @@ | |||
| 1 | use std::{ | 1 | use 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 | ||
| 10 | pub const PORT: u16 = 69; | 10 | pub const PORT: u16 = 69; |
| 11 | 11 | ||
| 12 | const DEFAULT_BLOCK_SIZE: u64 = 512; | ||
| 13 | const MAX_BLOCK_SIZE: usize = 2048; | ||
| 14 | |||
| 12 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] |
| 13 | pub struct InvalidTftpOp(u16); | 16 | pub struct InvalidTftpOp(u16); |
| 14 | 17 | ||
| @@ -91,7 +94,7 @@ impl FromStr for TftpMode { | |||
| 91 | 94 | ||
| 92 | #[derive(Debug)] | 95 | #[derive(Debug)] |
| 93 | pub enum TftpPacket { | 96 | pub 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 | ||
| 101 | impl TftpPacket { | 104 | impl 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)] |
| 114 | pub struct TftpRequestPacket { | 121 | pub 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 | ||
| 121 | impl TftpRequestPacket { | 128 | impl 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)] | ||
| 194 | pub enum TftpErrorCode { | ||
| 195 | Undefined, | ||
| 196 | FileNotFound, | ||
| 197 | AccessViolation, | ||
| 198 | DiskFull, | ||
| 199 | IllegalOperation, | ||
| 200 | UnknownTransferId, | ||
| 201 | FileAreadyExists, | ||
| 202 | NoSuchUser, | ||
| 203 | Unknown(u16), | ||
| 204 | } | ||
| 205 | |||
| 206 | impl 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 | |||
| 212 | impl 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 | |||
| 223 | impl 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 | |||
| 239 | impl 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)] |
| 187 | pub struct TftpErrorPacket { | 256 | pub struct TftpErrorPacket { |
| 188 | pub code: u16, | 257 | pub code: TftpErrorCode, |
| 189 | pub message: String, | 258 | pub message: String, |
| 190 | } | 259 | } |
| 191 | 260 | ||
| 192 | impl TftpErrorPacket { | 261 | impl 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 | ||
| 270 | pub fn serve(dir: impl AsRef<Path>) -> Result<()> { | 346 | pub 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; | 352 | pub struct StaticFileSystem { |
| 276 | let mut current_file = PathBuf::default(); | 353 | files: &'static [(&'static str, &'static [u8])], |
| 354 | } | ||
| 355 | |||
| 356 | impl 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 | |||
| 370 | impl 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) => { | 391 | struct Client { |
| 392 | blksize: u64, | ||
| 393 | filename: Option<String>, | ||
| 394 | } | ||
| 395 | |||
| 396 | impl Default for Client { | ||
| 397 | fn default() -> Self { | ||
| 398 | Self { | ||
| 399 | blksize: DEFAULT_BLOCK_SIZE, | ||
| 400 | filename: None, | ||
| 401 | } | ||
| 402 | } | ||
| 403 | } | ||
| 404 | |||
| 405 | pub enum ServerCommand { | ||
| 406 | Send(TftpPacket), | ||
| 407 | Ignore, | ||
| 408 | } | ||
| 409 | |||
| 410 | impl 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)] | ||
| 417 | pub struct Server { | ||
| 418 | clients: HashMap<SocketAddr, Client>, | ||
| 419 | } | ||
| 420 | |||
| 421 | impl 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(¤t_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 | ||
| 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 | ||
