Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b50fb644a9 | |||
| d4612ef4c9 | |||
| d51320ebff | |||
| 102d00b516 | |||
| b0e3f8e9ad | |||
| b27515d583 | |||
| bd729751d7 | |||
| b869651768 | |||
| 0f5a4f59f4 | |||
| 3101345fe6 | |||
| 52daa468b8 | |||
| baea0e523b | |||
| 46e70a5472 | |||
| 70e0d79454 | |||
| e416b48bd8 | |||
| 08d0fb2ede | |||
| 4f5495acc6 | |||
| efa24228e3 | |||
| 2d266b54a8 | |||
| 72ba66efb9 | |||
| 9c71dd82cc | |||
| 7f7f29b4d4 | |||
| 263950aefa | |||
| 75f5480f84 | |||
| 73e44d8104 | |||
| 596e73a92e | |||
| d2e6ea1ba9 | |||
| aa6bf0f2ea | |||
| 1a3e12b60a | |||
| 579a512545 | |||
| ecc0ef33de | |||
| 48cb44cb3e | |||
| 93f026f292 | |||
| a158fe5844 | |||
| 1036881f03 | |||
| 19199096c5 | |||
| c899b42d36 | |||
| 1fad3ab869 | |||
| 649ba27669 | |||
| 22d05e5f89 | |||
| 82d8a049cc | |||
| f3c18f410c | |||
| 9bab466704 | |||
| 89083a25f9 | |||
| 0725f81144 | |||
| c015a375b1 | |||
| c1cfee3bea | |||
| 7abd45bf2e | |||
| 98c43529e7 | |||
| e185e03d88 | |||
| 817524a3f0 | |||
| c1eb0b3db0 | |||
| ff66b96266 | |||
| f6313ae5c4 | |||
| 998593f243 | |||
| 5cfbe23b5c | |||
| 27f8e351e6 | |||
| faf777b990 | |||
| ba5a7cd3fc | |||
| 25d6decc85 | |||
| 107e3b6851 | |||
| 0baceb0ef6 | |||
| 748f066a6e | |||
| 7ad8717f37 | |||
| e05315fc38 | |||
| 2b278390ee | |||
| 30db94c970 | |||
| 1606a6ffb2 | |||
| 1f9a25c949 | |||
| bb3e36e4e0 | |||
| 4506bfe02a | |||
| cb1c326645 | |||
| 81c366359d | |||
| 82bf2444e3 | |||
| 75950f10a8 | |||
| 0f53316100 | |||
| 2c6c430225 | |||
| 11ef817366 | |||
| 9f61f7bf68 | |||
| f646e42e55 | |||
| d0ecc06734 | |||
| 1c7f8390b3 | |||
| 4037c383b9 | |||
| 37cbed4c17 | |||
| 1b1902e5e7 | |||
| 947475a2ed | |||
| f46190851f | |||
| 908628215d | |||
| 16c113a382 | |||
| 0eaf6ceec6 | |||
| 7286457ae2 | |||
| fb9b158e66 | |||
| 230a219c5f | |||
| 171d8c5258 | |||
| 21cf3d900c | |||
| 7efa83d6bd | |||
| ea2219148e | |||
| c89af69d08 | |||
| 0d8f3aadc0 | |||
| 69a8e406c6 | |||
| 71971d12e0 | |||
| 431fa59e49 | |||
| 9a4f9ddd05 | |||
| 01cab772aa | |||
| 2206ca8f94 | |||
| f98dd4339f | |||
| 5276fcb739 | |||
| df509fe737 | |||
| 8b9a4fa7b6 | |||
| b244dbd0d9 | |||
| 61b1510a46 | |||
| ceb1a5799a | |||
| 27021d15d3 | |||
| 7fbf50fabc | |||
| b7e7b55638 | |||
| ddb40deac5 | |||
| d3b8d08420 | |||
| b7d6dd1545 | |||
| 774a0ac118 | |||
| 7cfed158b8 | |||
| cbd40e0d63 | |||
| f0ff6a7976 | |||
| d1fe24066f | |||
| 384ab65f2f | |||
| 1bfba43a5b | |||
| 37803065d6 | |||
| e653ef10d6 | |||
| 4bfb878b55 | |||
| 4a1d1f4576 | |||
| b1a6bd871f |
Generated
+202
-143
@@ -13,7 +13,7 @@ dependencies = [
|
||||
"pcid",
|
||||
"redox-scheme",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
"spin",
|
||||
]
|
||||
@@ -52,11 +52,11 @@ dependencies = [
|
||||
"log",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"parking_lot 0.12.5",
|
||||
"parking_lot 0.12.3",
|
||||
"plain",
|
||||
"redox-scheme",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"ron",
|
||||
"rustc-hash",
|
||||
"scheme-utils",
|
||||
@@ -76,7 +76,7 @@ dependencies = [
|
||||
"log",
|
||||
"pcid",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -85,7 +85,7 @@ version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr 2.8.2",
|
||||
"memchr 2.8.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -100,7 +100,7 @@ dependencies = [
|
||||
"libredox",
|
||||
"log",
|
||||
"pcid",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"ron",
|
||||
"serde",
|
||||
]
|
||||
@@ -181,9 +181,9 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe"
|
||||
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
@@ -206,7 +206,7 @@ dependencies = [
|
||||
"libc",
|
||||
"libredox",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
]
|
||||
|
||||
@@ -238,7 +238,7 @@ dependencies = [
|
||||
"fdt 0.2.0-alpha1",
|
||||
"libredox",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -342,9 +342,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.65"
|
||||
version = "1.2.66"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
|
||||
checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -436,7 +436,7 @@ dependencies = [
|
||||
"libredox",
|
||||
"log",
|
||||
"redox-log",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -495,11 +495,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-queue"
|
||||
version = "0.3.12"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
|
||||
checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26"
|
||||
dependencies = [
|
||||
"crossbeam-utils 0.8.21",
|
||||
"crossbeam-utils 0.8.22",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -515,9 +515,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
version = "0.8.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
||||
|
||||
[[package]]
|
||||
name = "daemon"
|
||||
@@ -526,7 +526,7 @@ dependencies = [
|
||||
"libc",
|
||||
"libredox",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -535,14 +535,14 @@ version = "0.3.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad"
|
||||
dependencies = [
|
||||
"defmt 1.1.0",
|
||||
"defmt 1.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f"
|
||||
checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"defmt-macros",
|
||||
@@ -550,12 +550,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "defmt-macros"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b"
|
||||
checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8"
|
||||
dependencies = [
|
||||
"defmt-parser",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
@@ -574,6 +573,10 @@ dependencies = [
|
||||
name = "dhcpd"
|
||||
version = "0.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "dhcpv6d"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.8.1"
|
||||
@@ -594,7 +597,7 @@ dependencies = [
|
||||
"log",
|
||||
"partitionlib",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
]
|
||||
|
||||
@@ -611,7 +614,7 @@ dependencies = [
|
||||
"log",
|
||||
"redox-ioctl",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
]
|
||||
|
||||
@@ -622,7 +625,7 @@ dependencies = [
|
||||
"daemon",
|
||||
"libredox",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
]
|
||||
|
||||
@@ -678,7 +681,7 @@ dependencies = [
|
||||
"i2c-interface",
|
||||
"libredox",
|
||||
"log",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"ron",
|
||||
"serde",
|
||||
]
|
||||
@@ -695,7 +698,7 @@ dependencies = [
|
||||
"log",
|
||||
"pcid",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -775,7 +778,7 @@ dependencies = [
|
||||
"ransid",
|
||||
"redox-scheme",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
]
|
||||
|
||||
@@ -795,8 +798,9 @@ dependencies = [
|
||||
"ransid",
|
||||
"redox-scheme",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -905,7 +909,7 @@ dependencies = [
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr 2.8.2",
|
||||
"memchr 2.8.3",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
@@ -952,7 +956,7 @@ dependencies = [
|
||||
"libredox",
|
||||
"log",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"ron",
|
||||
"scheme-utils",
|
||||
"serde",
|
||||
@@ -1027,9 +1031,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.3.0"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
|
||||
checksum = "15cdd26707701c53297e2fa6afb323d55fbc1d0810c3aec078ae3ef0424c3c15"
|
||||
|
||||
[[package]]
|
||||
name = "hwd"
|
||||
@@ -1041,7 +1045,7 @@ dependencies = [
|
||||
"fdt 0.1.5",
|
||||
"libredox",
|
||||
"log",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"ron",
|
||||
]
|
||||
|
||||
@@ -1056,7 +1060,7 @@ dependencies = [
|
||||
"i2c-interface",
|
||||
"libredox",
|
||||
"log",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"ron",
|
||||
"serde",
|
||||
]
|
||||
@@ -1076,7 +1080,7 @@ dependencies = [
|
||||
"log",
|
||||
"orbclient",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"ron",
|
||||
"scheme-utils",
|
||||
"serde",
|
||||
@@ -1086,7 +1090,7 @@ dependencies = [
|
||||
name = "i2c-interface"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -1101,7 +1105,7 @@ dependencies = [
|
||||
"libredox",
|
||||
"log",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"ron",
|
||||
"scheme-utils",
|
||||
"serde",
|
||||
@@ -1142,7 +1146,7 @@ dependencies = [
|
||||
"log",
|
||||
"pcid",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1157,7 +1161,7 @@ dependencies = [
|
||||
"pcid",
|
||||
"redox-scheme",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
"spin",
|
||||
]
|
||||
@@ -1180,7 +1184,7 @@ dependencies = [
|
||||
"range-alloc",
|
||||
"redox-scheme",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"void",
|
||||
]
|
||||
|
||||
@@ -1202,7 +1206,7 @@ dependencies = [
|
||||
"libc",
|
||||
"libredox",
|
||||
"plain",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"toml",
|
||||
@@ -1219,7 +1223,7 @@ dependencies = [
|
||||
"log",
|
||||
"orbclient",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
]
|
||||
|
||||
@@ -1233,7 +1237,7 @@ dependencies = [
|
||||
"daemon",
|
||||
"libredox",
|
||||
"log",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"ron",
|
||||
"serde",
|
||||
]
|
||||
@@ -1249,7 +1253,7 @@ dependencies = [
|
||||
"i2c-interface",
|
||||
"libredox",
|
||||
"log",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"ron",
|
||||
"serde",
|
||||
]
|
||||
@@ -1269,7 +1273,7 @@ dependencies = [
|
||||
"pci_types",
|
||||
"pcid",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"ron",
|
||||
"scheme-utils",
|
||||
"serde",
|
||||
@@ -1293,10 +1297,10 @@ dependencies = [
|
||||
"daemon",
|
||||
"libc",
|
||||
"libredox",
|
||||
"rand 0.10.1",
|
||||
"rand 0.10.2",
|
||||
"redox-scheme",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
]
|
||||
|
||||
@@ -1323,7 +1327,7 @@ dependencies = [
|
||||
"libredox",
|
||||
"pcid",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1351,15 +1355,13 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
|
||||
version = "0.1.18+rb0.3.0"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"ioslice",
|
||||
"libc",
|
||||
"plain",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1383,7 +1385,16 @@ dependencies = [
|
||||
"driver-block",
|
||||
"libredox",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
source = "git+https://github.com/Amanieu/parking_lot.git?rev=0.12.3#a29dd3d4ff661f73fd5cf638584bc4c5de2ad347"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1408,7 +1419,7 @@ dependencies = [
|
||||
"daemon",
|
||||
"libredox",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
]
|
||||
|
||||
@@ -1435,9 +1446,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "nb"
|
||||
@@ -1465,7 +1476,7 @@ dependencies = [
|
||||
"redox-log",
|
||||
"redox-scheme",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
"smoltcp",
|
||||
]
|
||||
@@ -1517,11 +1528,11 @@ dependencies = [
|
||||
"futures",
|
||||
"libredox",
|
||||
"log",
|
||||
"parking_lot 0.12.5",
|
||||
"parking_lot 0.12.3",
|
||||
"partitionlib",
|
||||
"pcid",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"smallvec 1.15.2",
|
||||
]
|
||||
|
||||
@@ -1575,12 +1586,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
version = "0.12.3"
|
||||
source = "git+https://github.com/Amanieu/parking_lot.git?rev=0.12.3#a29dd3d4ff661f73fd5cf638584bc4c5de2ad347"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core 0.9.12",
|
||||
"lock_api 0.4.12",
|
||||
"parking_lot_core 0.9.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1597,15 +1607,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
version = "0.9.10"
|
||||
source = "git+https://github.com/Amanieu/parking_lot.git?rev=0.12.3#a29dd3d4ff661f73fd5cf638584bc4c5de2ad347"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"libc",
|
||||
"redox_syscall 0.5.18",
|
||||
"smallvec 1.15.2",
|
||||
"windows-link",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1648,7 +1657,7 @@ dependencies = [
|
||||
"pico-args",
|
||||
"plain",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
"serde",
|
||||
]
|
||||
@@ -1661,10 +1670,11 @@ dependencies = [
|
||||
"common",
|
||||
"config",
|
||||
"daemon",
|
||||
"libredox",
|
||||
"log",
|
||||
"pcid",
|
||||
"pico-args",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"serde",
|
||||
"toml",
|
||||
]
|
||||
@@ -1696,28 +1706,6 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -1740,7 +1728,7 @@ dependencies = [
|
||||
"orbclient",
|
||||
"redox-scheme",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1751,7 +1739,7 @@ dependencies = [
|
||||
"libredox",
|
||||
"redox-scheme",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"redox_termios",
|
||||
"scheme-utils",
|
||||
]
|
||||
@@ -1785,7 +1773,7 @@ dependencies = [
|
||||
"indexmap",
|
||||
"libredox",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
"slab",
|
||||
]
|
||||
@@ -1816,9 +1804,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
|
||||
checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80"
|
||||
dependencies = [
|
||||
"chacha20",
|
||||
"getrandom 0.4.3",
|
||||
@@ -1891,7 +1879,7 @@ dependencies = [
|
||||
"rand_core 0.5.1",
|
||||
"raw-cpuid",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
"sha2",
|
||||
]
|
||||
@@ -1929,6 +1917,14 @@ dependencies = [
|
||||
"rand_core 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redbear-netdiag"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "redbear-ufw"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "redox-initfs"
|
||||
version = "0.2.0"
|
||||
@@ -1953,10 +1949,9 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "redox-ioctl"
|
||||
version = "0.1.0"
|
||||
source = "git+https://gitlab.redox-os.org/redox-os/relibc.git#15a4a27a467a5c355b2a6ac50f2cf26e33c251c2"
|
||||
dependencies = [
|
||||
"drm-sys",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1972,19 +1967,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox-scheme"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecd26e368655cb4fdda914512c46c5640221ebe5fa6df5da19c941b9f06ed153"
|
||||
version = "0.11.2+rb0.3.0"
|
||||
dependencies = [
|
||||
"libredox",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_event"
|
||||
version = "0.4.7"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c07d0d6d291e3a951bd847b1cd4af32fc6243d64116cf7702838c02797688b7"
|
||||
checksum = "c5018d583d6d2f5499352aea8d177e9067d1eb03ab17c78169d5ba7a30001b15"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"libredox",
|
||||
@@ -2001,7 +1994,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.8.1"
|
||||
version = "0.9.0+rb0.3.0"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
]
|
||||
@@ -2021,7 +2014,7 @@ dependencies = [
|
||||
"libredox",
|
||||
"qemu-exit",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"redox_termios",
|
||||
]
|
||||
|
||||
@@ -2032,7 +2025,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr 2.8.2",
|
||||
"memchr 2.8.3",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
@@ -2044,7 +2037,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr 2.8.2",
|
||||
"memchr 2.8.3",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
@@ -2097,7 +2090,7 @@ dependencies = [
|
||||
"log",
|
||||
"pcid",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2112,7 +2105,7 @@ dependencies = [
|
||||
"log",
|
||||
"pcid",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2136,9 +2129,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||
|
||||
[[package]]
|
||||
name = "sb16d"
|
||||
@@ -2151,7 +2144,7 @@ dependencies = [
|
||||
"log",
|
||||
"redox-scheme",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
"spin",
|
||||
]
|
||||
@@ -2162,7 +2155,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"libredox",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2251,7 +2244,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr 2.8.2",
|
||||
"memchr 2.8.3",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
@@ -2329,7 +2322,7 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"lock_api 0.4.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2338,7 +2331,7 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"lock_api 0.4.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2526,7 +2519,7 @@ dependencies = [
|
||||
"libredox",
|
||||
"log",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"ron",
|
||||
"scheme-utils",
|
||||
"serde",
|
||||
@@ -2538,6 +2531,10 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "usb-core"
|
||||
version = "0.3.0"
|
||||
|
||||
[[package]]
|
||||
name = "usbctl"
|
||||
version = "0.1.0"
|
||||
@@ -2557,7 +2554,7 @@ dependencies = [
|
||||
"inputd",
|
||||
"log",
|
||||
"orbclient",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"rehid",
|
||||
"xhcid",
|
||||
]
|
||||
@@ -2568,7 +2565,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"common",
|
||||
"log",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"xhcid",
|
||||
]
|
||||
|
||||
@@ -2580,9 +2577,10 @@ dependencies = [
|
||||
"daemon",
|
||||
"driver-block",
|
||||
"libredox",
|
||||
"log",
|
||||
"plain",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"thiserror 2.0.18",
|
||||
"xhcid",
|
||||
]
|
||||
@@ -2620,7 +2618,7 @@ dependencies = [
|
||||
"orbclient",
|
||||
"pcid",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2641,7 +2639,7 @@ dependencies = [
|
||||
"orbclient",
|
||||
"ransid",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2657,7 +2655,7 @@ dependencies = [
|
||||
"log",
|
||||
"pcid",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"spin",
|
||||
"static_assertions",
|
||||
"thiserror 2.0.18",
|
||||
@@ -2676,7 +2674,7 @@ dependencies = [
|
||||
"log",
|
||||
"pcid",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"static_assertions",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
@@ -2696,7 +2694,7 @@ dependencies = [
|
||||
"orbclient",
|
||||
"pcid",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"spin",
|
||||
"static_assertions",
|
||||
"virtio-core",
|
||||
@@ -2713,7 +2711,7 @@ dependencies = [
|
||||
"libredox",
|
||||
"log",
|
||||
"pcid",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"static_assertions",
|
||||
"virtio-core",
|
||||
]
|
||||
@@ -2883,6 +2881,70 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.3"
|
||||
@@ -2906,7 +2968,7 @@ dependencies = [
|
||||
"plain",
|
||||
"redox-scheme",
|
||||
"redox_event",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"regex",
|
||||
"scheme-utils",
|
||||
"serde",
|
||||
@@ -2914,22 +2976,23 @@ dependencies = [
|
||||
"smallvec 1.15.2",
|
||||
"thiserror 2.0.18",
|
||||
"toml",
|
||||
"usb-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.52"
|
||||
version = "0.8.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f"
|
||||
checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.52"
|
||||
version = "0.8.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930"
|
||||
checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2943,7 +3006,7 @@ dependencies = [
|
||||
"daemon",
|
||||
"libredox",
|
||||
"redox-scheme",
|
||||
"redox_syscall 0.8.1",
|
||||
"redox_syscall 0.9.0+rb0.3.0",
|
||||
"scheme-utils",
|
||||
]
|
||||
|
||||
@@ -2952,7 +3015,3 @@ name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[patch.unused]]
|
||||
name = "libredox"
|
||||
version = "0.1.18"
|
||||
|
||||
+21
-6
@@ -5,6 +5,8 @@ members = [
|
||||
"config",
|
||||
"daemon",
|
||||
"dhcpd",
|
||||
"dhcpv6d",
|
||||
"netdiag",
|
||||
"init",
|
||||
"initfs",
|
||||
"initfs/tools",
|
||||
@@ -13,6 +15,7 @@ members = [
|
||||
"netstack",
|
||||
"ptyd",
|
||||
"ramfs",
|
||||
"redbear-ufw",
|
||||
"randd",
|
||||
"scheme-utils",
|
||||
"zerod",
|
||||
@@ -100,17 +103,17 @@ edid = "0.3.0" #TODO: edid is abandoned, fork it and maintain?
|
||||
fdt = "0.1.5"
|
||||
libc = "0.2.181"
|
||||
log = "0.4"
|
||||
libredox = "0.1.17"
|
||||
libredox = { path = "../libredox", default-features = true }
|
||||
orbclient = "0.3.51"
|
||||
parking_lot = "0.12"
|
||||
parking_lot = { git = "https://github.com/Amanieu/parking_lot.git", rev = "0.12.3", default-features = false }
|
||||
pico-args = "0.5"
|
||||
plain = "0.2.3"
|
||||
ransid = "0.4"
|
||||
redox_event = "0.4.6"
|
||||
redox-ioctl = { git = "https://gitlab.redox-os.org/redox-os/relibc.git" }
|
||||
redox_event = "0.4.8"
|
||||
redox-ioctl = { path = "../relibc/redox-ioctl" }
|
||||
redox-log = { git = "https://gitlab.redox-os.org/redox-os/redox-log.git" }
|
||||
redox-rt = { git = "https://gitlab.redox-os.org/redox-os/relibc.git", default-features = false }
|
||||
redox-scheme = "0.11.0"
|
||||
redox-rt = { path = "../relibc/redox-rt", default-features = false }
|
||||
redox-scheme = { path = "../redox-scheme" }
|
||||
redox_syscall = { path = "../syscall", features = ["std"] }
|
||||
redox_termios = "0.1.3"
|
||||
ron = "0.8.1"
|
||||
@@ -155,6 +158,18 @@ redox_syscall = { path = "../syscall" }
|
||||
# the local syscall fork). Now libredox::error::Error and
|
||||
# syscall::Error are the same type.
|
||||
libredox = { path = "../libredox" }
|
||||
# Red Bear OS Phase J (extended): redox-scheme 0.11.x from crates.io
|
||||
# pins redox_syscall = "0.9.0" exactly, which Cargo refuses to
|
||||
# satisfy with the local +rb0.3.0 fork (0.9.0+rb0.3.0). Without this
|
||||
# patch, Cargo pulls crates.io redox-scheme which transitively
|
||||
# pulls crates.io redox_syscall 0.9.0, leading to two different
|
||||
# `syscall::Error` types and E0277 errors in scheme-utils / daemon.
|
||||
# Patch redox-scheme to the local fork at ../redox-scheme/ which
|
||||
# has its redox_syscall and libredox dep requirements bumped to
|
||||
# the +rb0.3.0 versions. The local fork is a thin pass-through of
|
||||
# upstream redox-scheme 0.11.2 source with only the dep versions
|
||||
# updated — no behavioural changes.
|
||||
redox-scheme = { path = "../redox-scheme" }
|
||||
|
||||
[patch."https://gitlab.redox-os.org/redox-os/relibc.git"]
|
||||
#redox-ioctl = { path = "../../relibc/source/redox-ioctl" }
|
||||
|
||||
Generated
+21
-29
@@ -4,15 +4,15 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "bootstrap"
|
||||
@@ -41,13 +41,12 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
[[package]]
|
||||
name = "generic-rt"
|
||||
version = "0.1.0"
|
||||
source = "git+https://gitlab.redox-os.org/redox-os/relibc.git#2b69838a481b7be9826b41e06a0b8f1346b80f9e"
|
||||
|
||||
[[package]]
|
||||
name = "goblin"
|
||||
version = "0.10.5"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "983a6aafb3b12d4c41ea78d39e189af4298ce747353945ff5105b54a056e5cd9"
|
||||
checksum = "17582616a7718cca54cec18e534a76c7c4aec11a8b9a85695712f262fd15a4c8"
|
||||
dependencies = [
|
||||
"log",
|
||||
"plain",
|
||||
@@ -71,15 +70,13 @@ checksum = "5e571352c8a3b89074d12e3ee5173ffe162159105352aaaf1fc5764da747e31b"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.181"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
|
||||
version = "0.1.18+rb0.3.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
@@ -89,9 +86,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "linked_list_allocator"
|
||||
version = "0.10.5"
|
||||
version = "0.10.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afa463f5405ee81cdb9cc2baf37e08ec7e4c8209442b5d72c04cfb2cd6e6286"
|
||||
checksum = "2b23ac50abb8261cb38c6e2a7192d3302e0836dac1628f6a93b82b4fad185897"
|
||||
dependencies = [
|
||||
"spinning_top",
|
||||
]
|
||||
@@ -107,9 +104,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
@@ -128,9 +125,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -151,7 +148,6 @@ checksum = "436d45c2b6a5b159d43da708e62b25be3a4a3d5550d654b72216ade4c4bfd717"
|
||||
[[package]]
|
||||
name = "redox-rt"
|
||||
version = "0.1.0"
|
||||
source = "git+https://gitlab.redox-os.org/redox-os/relibc.git#2b69838a481b7be9826b41e06a0b8f1346b80f9e"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"generic-rt",
|
||||
@@ -165,9 +161,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox-scheme"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e58cb7dee1058575f320f9bfa32c7c0eda857e285c2a163a0eb595d37add09cd"
|
||||
version = "0.11.2+rb0.3.0"
|
||||
dependencies = [
|
||||
"libredox",
|
||||
"redox_syscall",
|
||||
@@ -175,9 +169,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a"
|
||||
version = "0.9.0+rb0.3.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
@@ -225,9 +217,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -236,6 +228,6 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.23"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
+13
-4
@@ -6,6 +6,11 @@ authors = ["4lDO2 <4lDO2@protonmail.com>"]
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
|
||||
[workspace.dependencies]
|
||||
libredox = { path = "../../libredox", default-features = false, features = ["base", "protocol", "redox_syscall"] }
|
||||
redox_syscall = { path = "../../syscall" }
|
||||
redox-scheme = { path = "../../redox-scheme", default-features = false }
|
||||
|
||||
[workspace]
|
||||
|
||||
[dependencies]
|
||||
@@ -14,18 +19,18 @@ hashbrown = { version = "0.15", default-features = false, features = [
|
||||
"default-hasher",
|
||||
] }
|
||||
linked_list_allocator = "0.10"
|
||||
libredox = { version = "0.1.16", default-features = false, features = ["protocol"] }
|
||||
libredox = { workspace = true }
|
||||
log = { version = "0.4", default-features = false }
|
||||
plain = "0.2"
|
||||
redox-initfs = { path = "../initfs", default-features = false }
|
||||
redox_syscall = "0.7.4"
|
||||
redox-scheme = { version = "0.11.0", default-features = false }
|
||||
redox_syscall = { workspace = true }
|
||||
redox-scheme = { workspace = true }
|
||||
redox-path = "0.3.1"
|
||||
slab = { version = "0.4.9", default-features = false }
|
||||
arrayvec = { version = "0.7.6", default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "redox")'.dependencies]
|
||||
redox-rt = { git = "https://gitlab.redox-os.org/redox-os/relibc.git", default-features = false }
|
||||
redox-rt = { path = "../../relibc/redox-rt", default-features = false }
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
@@ -35,3 +40,7 @@ opt-level = "s"
|
||||
[profile.dev]
|
||||
panic = "abort"
|
||||
opt-level = "s"
|
||||
|
||||
[patch.crates-io]
|
||||
redox_syscall = { path = "../../syscall" }
|
||||
libredox = { path = "../../libredox" }
|
||||
|
||||
+10
-10
@@ -26,7 +26,7 @@ impl log::Log for Logger {
|
||||
let line = record.line().unwrap_or(0);
|
||||
let level = record.level();
|
||||
let msg = record.args();
|
||||
let _ = syscall::write(
|
||||
let _ = libredox::call::write(
|
||||
1,
|
||||
alloc::format!("[{file}:{line} {level}] {msg}\n").as_bytes(),
|
||||
);
|
||||
@@ -70,13 +70,13 @@ pub fn main() -> ! {
|
||||
let mut env_bytes = [0_u8; 4096];
|
||||
let mut envs = {
|
||||
let fd = FdGuard::new(
|
||||
syscall::openat(
|
||||
libredox::call::openat(
|
||||
kernel_schemes
|
||||
.get(GlobalSchemes::Sys)
|
||||
.expect("failed to get sys fd")
|
||||
.as_raw_fd(),
|
||||
"env",
|
||||
O_RDONLY | O_CLOEXEC,
|
||||
(O_RDONLY | O_CLOEXEC) as i32,
|
||||
0,
|
||||
)
|
||||
.expect("bootstrap: failed to open env"),
|
||||
@@ -206,7 +206,7 @@ pub fn main() -> ! {
|
||||
|
||||
const CWD: &[u8] = b"/scheme/initfs";
|
||||
let cwd_fd = FdGuard::new(
|
||||
syscall::openat(initns_fd.as_raw_fd(), "/scheme/initfs", O_STAT, 0)
|
||||
libredox::call::openat(initns_fd.as_raw_fd(), "/scheme/initfs", O_STAT as i32, 0)
|
||||
.expect("failed to open cwd fd"),
|
||||
)
|
||||
.to_upper()
|
||||
@@ -225,7 +225,7 @@ pub fn main() -> ! {
|
||||
let path = "/scheme/initfs/bin/init";
|
||||
|
||||
let image_file = FdGuard::new(
|
||||
syscall::openat(extrainfo.ns_fd.unwrap(), path, O_RDONLY | O_CLOEXEC, 0)
|
||||
libredox::call::openat(extrainfo.ns_fd.unwrap(), path, (O_RDONLY | O_CLOEXEC) as i32, 0)
|
||||
.expect("failed to open init"),
|
||||
)
|
||||
.to_upper()
|
||||
@@ -252,10 +252,10 @@ pub fn main() -> ! {
|
||||
// null-terminated. Violating this should therefore give the "format error" ENOEXEC.
|
||||
let interp_cstr = CStr::from_bytes_with_nul(&interp_path).expect("interpreter not valid C str");
|
||||
let interp_file = FdGuard::new(
|
||||
syscall::openat(
|
||||
libredox::call::openat(
|
||||
extrainfo.ns_fd.unwrap(), // initns, not initfs!
|
||||
interp_cstr.to_str().expect("interpreter not UTF-8"),
|
||||
O_RDONLY | O_CLOEXEC,
|
||||
(O_RDONLY | O_CLOEXEC) as i32,
|
||||
0,
|
||||
)
|
||||
.expect("failed to open dynamic linker"),
|
||||
@@ -288,13 +288,13 @@ pub(crate) fn spawn(
|
||||
inner: impl FnOnce(FdGuard, Socket, FdGuard, KernelSchemeMap) -> !,
|
||||
) -> (FdGuard, FdGuard, KernelSchemeMap, FdGuard) {
|
||||
let read = FdGuard::new(
|
||||
syscall::openat(
|
||||
libredox::call::openat(
|
||||
kernel_schemes
|
||||
.get(GlobalSchemes::Pipe)
|
||||
.expect("failed to get pipe fd")
|
||||
.as_raw_fd(),
|
||||
"",
|
||||
O_CLOEXEC,
|
||||
O_CLOEXEC as i32,
|
||||
0,
|
||||
)
|
||||
.expect("failed to open sync read pipe"),
|
||||
@@ -302,7 +302,7 @@ pub(crate) fn spawn(
|
||||
|
||||
// The write pipe will not inherit O_CLOEXEC, but is closed by the daemon later.
|
||||
let write = FdGuard::new(
|
||||
syscall::dup(read.as_raw_fd(), b"write").expect("failed to open sync write pipe"),
|
||||
libredox::call::dup(read.as_raw_fd(), b"write").expect("failed to open sync write pipe"),
|
||||
);
|
||||
|
||||
match fork_impl(&ForkArgs::Init {
|
||||
|
||||
@@ -446,6 +446,22 @@ pub unsafe extern "C" fn redox_write_v1(fd: usize, ptr: *const u8, len: usize) -
|
||||
})) as isize
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn redox_openat_v1(
|
||||
fd: usize,
|
||||
buf: *const u8,
|
||||
path_len: usize,
|
||||
flags: u32,
|
||||
fcntl_flags: u32,
|
||||
) -> isize {
|
||||
let path = unsafe { core::slice::from_raw_parts(buf, path_len) };
|
||||
let path_str = match core::str::from_utf8(path) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return -(syscall::EINVAL as isize),
|
||||
};
|
||||
Error::mux(syscall::openat(fd, path_str, flags as usize, fcntl_flags as usize)) as isize
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe fn redox_dup_v1(fd: usize, buf: *const u8, len: usize) -> isize {
|
||||
Error::mux(syscall::dup(fd, unsafe {
|
||||
@@ -458,6 +474,46 @@ pub extern "C" fn redox_close_v1(fd: usize) -> isize {
|
||||
Error::mux(syscall::close(fd)) as isize
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn redox_fcntl_v0(fd: usize, cmd: usize, arg: usize) -> isize {
|
||||
Error::mux(syscall::fcntl(fd, cmd, arg)) as isize
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn redox_strerror_v1(
|
||||
dst: *mut u8,
|
||||
dst_len: *mut usize,
|
||||
error: u32,
|
||||
) -> isize {
|
||||
let msg = match error {
|
||||
x if x == syscall::EPERM as u32 => "Operation not permitted",
|
||||
x if x == syscall::ENOENT as u32 => "No such file or directory",
|
||||
x if x == syscall::EINTR as u32 => "Interrupted system call",
|
||||
x if x == syscall::EIO as u32 => "I/O error",
|
||||
x if x == syscall::EBADF as u32 => "Bad file descriptor",
|
||||
x if x == syscall::EAGAIN as u32 => "Resource temporarily unavailable",
|
||||
x if x == syscall::ENOMEM as u32 => "Cannot allocate memory",
|
||||
x if x == syscall::EACCES as u32 => "Permission denied",
|
||||
x if x == syscall::EFAULT as u32 => "Bad address",
|
||||
x if x == syscall::EBUSY as u32 => "Device or resource busy",
|
||||
x if x == syscall::EEXIST as u32 => "File exists",
|
||||
x if x == syscall::ENOTDIR as u32 => "Not a directory",
|
||||
x if x == syscall::EISDIR as u32 => "Is a directory",
|
||||
x if x == syscall::EINVAL as u32 => "Invalid argument",
|
||||
x if x == syscall::ENOSYS as u32 => "Function not implemented",
|
||||
x if x == syscall::ENOTEMPTY as u32 => "Directory not empty",
|
||||
_ => "Unknown error",
|
||||
};
|
||||
let msg_bytes = msg.as_bytes();
|
||||
unsafe {
|
||||
let avail = *dst_len;
|
||||
let copy_len = avail.min(msg_bytes.len());
|
||||
core::ptr::copy_nonoverlapping(msg_bytes.as_ptr(), dst, copy_len);
|
||||
*dst_len = copy_len;
|
||||
copy_len as isize
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn redox_sys_call_v0(
|
||||
fd: usize,
|
||||
|
||||
@@ -150,7 +150,7 @@ impl<'sock> NamespaceScheme<'sock> {
|
||||
error!("Permission denied to get scheme creation capability");
|
||||
return Err(Error::new(EACCES));
|
||||
}
|
||||
Ok(syscall::dup(self.scheme_creation_cap.as_raw_fd(), &[])?)
|
||||
Ok(libredox::call::dup(self.scheme_creation_cap.as_raw_fd(), &[])?)
|
||||
}
|
||||
_ => {
|
||||
error!("Unknown special reference: {}", reference);
|
||||
@@ -173,13 +173,11 @@ impl<'sock> NamespaceScheme<'sock> {
|
||||
return Err(Error::new(ENODEV));
|
||||
};
|
||||
|
||||
let scheme_fd = syscall::openat_with_filter(
|
||||
let scheme_fd = syscall::openat(
|
||||
cap_fd.as_raw_fd(),
|
||||
reference,
|
||||
flags,
|
||||
fcntl_flags as usize,
|
||||
ctx.uid,
|
||||
ctx.gid,
|
||||
)?;
|
||||
|
||||
Ok(scheme_fd)
|
||||
@@ -353,7 +351,7 @@ impl<'sock> SchemeSync for NamespaceScheme<'sock> {
|
||||
return Err(Error::new(ENODEV));
|
||||
};
|
||||
|
||||
syscall::unlinkat_with_filter(cap_fd.as_raw_fd(), reference, flags, ctx.uid, ctx.gid)?;
|
||||
syscall::unlinkat(cap_fd.as_raw_fd(), reference, flags)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ fn panic_handler(info: &core::panic::PanicInfo) -> ! {
|
||||
|
||||
impl Write for Writer {
|
||||
fn write_str(&mut self, s: &str) -> core::fmt::Result {
|
||||
syscall::write(1, s.as_bytes())
|
||||
libredox::call::write(1, s.as_bytes())
|
||||
.map_err(|_| core::fmt::Error)
|
||||
.map(|_| ())
|
||||
}
|
||||
|
||||
@@ -549,7 +549,8 @@ enum WaitpidTarget {
|
||||
struct RawEventQueue(FdGuard);
|
||||
impl RawEventQueue {
|
||||
pub fn new(cap_fd: usize) -> Result<Self> {
|
||||
syscall::openat(cap_fd, "", O_CREAT, 0)
|
||||
libredox::call::openat(cap_fd, "", O_CREAT as i32, 0)
|
||||
.map_err(Into::into)
|
||||
.map(FdGuard::new)
|
||||
.map(Self)
|
||||
}
|
||||
@@ -919,7 +920,7 @@ impl<'a> ProcScheme<'a> {
|
||||
mut op: OpCall,
|
||||
awoken: &mut VecDeque<VirtualId>,
|
||||
) -> Poll<Response> {
|
||||
let id = op.fd;
|
||||
let id = op.fd();
|
||||
let (payload, metadata) = op.payload_and_metadata();
|
||||
match self.handles[id] {
|
||||
Handle::Init => Response::ready_err(EBADF, op),
|
||||
@@ -2542,8 +2543,8 @@ impl<'a> ProcScheme<'a> {
|
||||
|
||||
// Useful for debugging memory leaks.
|
||||
log::trace!("NEXT FD: {}", {
|
||||
let nextfd = syscall::dup(0, &[]).unwrap();
|
||||
let _ = syscall::close(nextfd);
|
||||
let nextfd = libredox::call::dup(0, &[]).unwrap();
|
||||
let _ = libredox::call::close(nextfd);
|
||||
nextfd
|
||||
});
|
||||
log::trace!("{} processes", self.processes.len());
|
||||
|
||||
@@ -48,9 +48,9 @@ pub unsafe extern "C" fn start() -> ! {
|
||||
|
||||
// NOTE: Assuming the debug scheme root fd is always placed at this position
|
||||
let debug_fd = syscall::UPPER_FDTBL_TAG + syscall::data::GlobalSchemes::Debug as usize;
|
||||
let _ = syscall::openat(debug_fd, "", syscall::O_RDONLY, 0); // stdin
|
||||
let _ = syscall::openat(debug_fd, "", syscall::O_WRONLY, 0); // stdout
|
||||
let _ = syscall::openat(debug_fd, "", syscall::O_WRONLY, 0); // stderr
|
||||
let _ = libredox::call::openat(debug_fd, "", syscall::O_RDONLY as i32, 0); // stdin
|
||||
let _ = libredox::call::openat(debug_fd, "", syscall::O_WRONLY as i32, 0); // stdout
|
||||
let _ = libredox::call::openat(debug_fd, "", syscall::O_WRONLY as i32, 0); // stderr
|
||||
|
||||
unsafe {
|
||||
let _ = syscall::mprotect(4096, 4096, MapFlags::PROT_READ | MapFlags::MAP_PRIVATE)
|
||||
|
||||
@@ -17,3 +17,21 @@ pub struct Dhcp {
|
||||
pub magic: u32,
|
||||
pub options: [u8; 308],
|
||||
}
|
||||
|
||||
pub const DHCPDISCOVER: u8 = 1;
|
||||
pub const DHCPOFFER: u8 = 2;
|
||||
pub const DHCPREQUEST: u8 = 3;
|
||||
pub const DHCPDECLINE: u8 = 4;
|
||||
pub const DHCPACK: u8 = 5;
|
||||
pub const DHCPNAK: u8 = 6;
|
||||
pub const DHCPRELEASE: u8 = 7;
|
||||
|
||||
pub const OPT_SUBNET_MASK: u8 = 1;
|
||||
pub const OPT_ROUTER: u8 = 3;
|
||||
pub const OPT_DNS: u8 = 6;
|
||||
pub const OPT_REQUESTED_IP: u8 = 50;
|
||||
pub const OPT_LEASE_TIME: u8 = 51;
|
||||
pub const OPT_MESSAGE_TYPE: u8 = 53;
|
||||
pub const OPT_SERVER_ID: u8 = 54;
|
||||
pub const OPT_PARAM_REQUEST: u8 = 55;
|
||||
pub const OPT_END: u8 = 255;
|
||||
|
||||
+208
-329
@@ -4,7 +4,11 @@ use std::net::{SocketAddr, UdpSocket};
|
||||
use std::time::Duration;
|
||||
use std::{env, process, time};
|
||||
|
||||
use dhcp::Dhcp;
|
||||
use dhcp::{
|
||||
Dhcp, DHCPACK, DHCPDISCOVER, DHCPNAK, DHCPOFFER, DHCPREQUEST, DHCPRELEASE,
|
||||
OPT_DNS, OPT_LEASE_TIME, OPT_MESSAGE_TYPE, OPT_REQUESTED_IP, OPT_ROUTER,
|
||||
OPT_SERVER_ID, OPT_SUBNET_MASK, OPT_END,
|
||||
};
|
||||
|
||||
mod dhcp;
|
||||
|
||||
@@ -101,356 +105,243 @@ impl MacAddr {
|
||||
fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
|
||||
let current_mac = MacAddr::from_str(get_iface_cfg_value(iface, "mac")?.trim());
|
||||
|
||||
let current_ip = get_iface_cfg_value(iface, "addr/list")?
|
||||
let _current_ip = get_iface_cfg_value(iface, "addr/list")?
|
||||
.lines()
|
||||
.next()
|
||||
.map(|l| l.to_owned())
|
||||
.unwrap_or("0.0.0.0".to_string());
|
||||
|
||||
if verbose {
|
||||
println!(
|
||||
"DHCP: MAC: {} Current IP: {}",
|
||||
current_mac.to_string(),
|
||||
current_ip.trim()
|
||||
);
|
||||
println!("DHCP: MAC: {} Starting", current_mac.to_string());
|
||||
}
|
||||
|
||||
let tid = try_fmt!(
|
||||
time::SystemTime::now().duration_since(time::UNIX_EPOCH),
|
||||
"failed to get time"
|
||||
)
|
||||
.subsec_nanos();
|
||||
).subsec_nanos();
|
||||
|
||||
let socket = try_fmt!(UdpSocket::bind(("0.0.0.0", 68)), "failed to bind udp");
|
||||
try_fmt!(
|
||||
socket.connect(SocketAddr::from(([255, 255, 255, 255], 67))),
|
||||
"failed to connect udp"
|
||||
);
|
||||
try_fmt!(
|
||||
socket.set_read_timeout(Some(Duration::new(30, 0))),
|
||||
"failed to set read timeout"
|
||||
);
|
||||
try_fmt!(
|
||||
socket.set_write_timeout(Some(Duration::new(30, 0))),
|
||||
"failed to set write timeout"
|
||||
);
|
||||
try_fmt!(socket.connect(SocketAddr::from(([255, 255, 255, 255], 67))), "failed to connect");
|
||||
try_fmt!(socket.set_read_timeout(Some(Duration::new(30, 0))), "failed to set read timeout");
|
||||
try_fmt!(socket.set_write_timeout(Some(Duration::new(30, 0))), "failed to set write timeout");
|
||||
|
||||
let mut subnet_option: Option<[u8; 4]> = None;
|
||||
let mut router_option: Option<[u8; 4]> = None;
|
||||
let mut dns_option: Option<[u8; 4]> = None;
|
||||
let mut server_id_option: Option<[u8; 4]> = None;
|
||||
let mut lease_time_secs: u32 = 86400;
|
||||
|
||||
// DHCPDISCOVER
|
||||
{
|
||||
let mut discover = Dhcp {
|
||||
op: 1,
|
||||
htype: 1,
|
||||
hlen: 6,
|
||||
hops: 0,
|
||||
tid,
|
||||
secs: 0,
|
||||
flags: 0x8000u16.to_be(),
|
||||
ciaddr: [0, 0, 0, 0],
|
||||
yiaddr: [0, 0, 0, 0],
|
||||
siaddr: [0, 0, 0, 0],
|
||||
giaddr: [0, 0, 0, 0],
|
||||
chaddr: [
|
||||
current_mac.bytes[0],
|
||||
current_mac.bytes[1],
|
||||
current_mac.bytes[2],
|
||||
current_mac.bytes[3],
|
||||
current_mac.bytes[4],
|
||||
current_mac.bytes[5],
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
],
|
||||
sname: [0; 64],
|
||||
file: [0; 128],
|
||||
magic: 0x63825363u32.to_be(),
|
||||
options: [0; 308],
|
||||
};
|
||||
|
||||
for (s, d) in [
|
||||
// DHCP Message Type (Discover)
|
||||
53, 1, 1, // End
|
||||
255,
|
||||
]
|
||||
.iter()
|
||||
.zip(discover.options.iter_mut())
|
||||
{
|
||||
*d = *s;
|
||||
}
|
||||
|
||||
let discover_data = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
(&discover as *const Dhcp) as *const u8,
|
||||
std::mem::size_of::<Dhcp>(),
|
||||
)
|
||||
};
|
||||
|
||||
let _sent = try_fmt!(socket.send(discover_data), "failed to send discover");
|
||||
|
||||
if verbose {
|
||||
println!("DHCP: Sent Discover");
|
||||
}
|
||||
let mut discover = Dhcp::default();
|
||||
init_dhcp_header(&mut discover, current_mac, tid);
|
||||
let disc_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPDISCOVER, OPT_END];
|
||||
discover.options[..disc_opts.len()].copy_from_slice(disc_opts);
|
||||
try_fmt!(send_dhcp(&discover, &socket), "failed to send discover");
|
||||
if verbose { println!("DHCP: Sent Discover"); }
|
||||
}
|
||||
|
||||
// Recv DHCPOFFER
|
||||
let mut offer_data = [0; 65536];
|
||||
try_fmt!(socket.recv(&mut offer_data), "failed to receive offer");
|
||||
let offer = unsafe { &*(offer_data.as_ptr() as *const Dhcp) };
|
||||
if verbose {
|
||||
println!(
|
||||
"DHCP: Offer IP: {:?}, Server IP: {:?}",
|
||||
offer.yiaddr, offer.siaddr
|
||||
);
|
||||
if verbose { println!("DHCP: Offer IP: {:?}", offer.yiaddr); }
|
||||
|
||||
parse_options(&offer.options, &mut |code, data| {
|
||||
match code {
|
||||
OPT_SUBNET_MASK if data.len() == 4 && subnet_option.is_none() => {
|
||||
subnet_option = Some([data[0], data[1], data[2], data[3]]);
|
||||
}
|
||||
OPT_ROUTER if data.len() == 4 && router_option.is_none() => {
|
||||
router_option = Some([data[0], data[1], data[2], data[3]]);
|
||||
}
|
||||
OPT_DNS if data.len() == 4 && dns_option.is_none() => {
|
||||
dns_option = Some([data[0], data[1], data[2], data[3]]);
|
||||
}
|
||||
OPT_LEASE_TIME if data.len() == 4 => {
|
||||
lease_time_secs = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
|
||||
}
|
||||
OPT_SERVER_ID if data.len() == 4 && server_id_option.is_none() => {
|
||||
server_id_option = Some([data[0], data[1], data[2], data[3]]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
|
||||
let mask_len = compute_prefix_len(subnet_option);
|
||||
let new_ips = format!(
|
||||
"{}.{}.{}.{}/{}\n",
|
||||
offer.yiaddr[0], offer.yiaddr[1], offer.yiaddr[2], offer.yiaddr[3], mask_len
|
||||
);
|
||||
try_fmt!(set_iface_cfg_value(iface, "addr/set", &new_ips), "failed to set ip");
|
||||
apply_dhcp_config(iface, router_option, dns_option, verbose)?;
|
||||
|
||||
let server_id = server_id_option.unwrap_or([0, 0, 0, 0]);
|
||||
|
||||
// DHCPREQUEST
|
||||
{
|
||||
let mut request = Dhcp::default();
|
||||
init_dhcp_header(&mut request, current_mac, tid);
|
||||
let req_opts: &[u8] = &[
|
||||
OPT_MESSAGE_TYPE, 1, DHCPREQUEST,
|
||||
OPT_REQUESTED_IP, 4,
|
||||
offer.yiaddr[0], offer.yiaddr[1], offer.yiaddr[2], offer.yiaddr[3],
|
||||
OPT_SERVER_ID, 4, server_id[0], server_id[1], server_id[2], server_id[3],
|
||||
OPT_END,
|
||||
];
|
||||
request.options[..req_opts.len()].copy_from_slice(req_opts);
|
||||
try_fmt!(send_dhcp(&request, &socket), "failed to send request");
|
||||
if verbose { println!("DHCP: Sent Request"); }
|
||||
}
|
||||
|
||||
let mut subnet_option = None;
|
||||
let mut router_option = None;
|
||||
let mut dns_option = None;
|
||||
let mut server_id_option = None;
|
||||
// Recv DHCPACK
|
||||
let mut ack_data = [0; 65536];
|
||||
try_fmt!(socket.recv(&mut ack_data), "failed to receive ack");
|
||||
if verbose { println!("DHCP: lease acquired, {}s lease time", lease_time_secs); }
|
||||
|
||||
// RFC 2131 lease lifecycle: RENEW at T1, REBIND at T2
|
||||
let t1 = Duration::from_secs(lease_time_secs as u64 / 2);
|
||||
let t2 = Duration::from_secs((lease_time_secs as u64 * 7) / 8);
|
||||
let now = time::Instant::now();
|
||||
let t1_deadline = now + t1;
|
||||
|
||||
let mut remaining = t1_deadline.saturating_duration_since(time::Instant::now());
|
||||
while remaining > Duration::ZERO {
|
||||
std::thread::sleep(std::cmp::min(remaining, Duration::from_secs(60)));
|
||||
remaining = t1_deadline.saturating_duration_since(time::Instant::now());
|
||||
}
|
||||
|
||||
if verbose { println!("DHCP: entering RENEW state"); }
|
||||
{
|
||||
let mut options = offer.options.iter();
|
||||
while let Some(option) = options.next() {
|
||||
match *option {
|
||||
0 => (),
|
||||
255 => break,
|
||||
_ => {
|
||||
if let Some(len) = options.next() {
|
||||
if *len as usize <= options.as_slice().len() {
|
||||
let data = &options.as_slice()[..*len as usize];
|
||||
for _data_i in 0..*len {
|
||||
options.next();
|
||||
}
|
||||
match *option {
|
||||
1 => {
|
||||
if verbose {
|
||||
println!("DHCP: Subnet Mask: {data:?}");
|
||||
}
|
||||
if data.len() == 4 && subnet_option.is_none() {
|
||||
subnet_option = Some(Vec::from(data));
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
if verbose {
|
||||
println!("DHCP: Router: {data:?}");
|
||||
}
|
||||
if data.len() == 4 && router_option.is_none() {
|
||||
router_option = Some(Vec::from(data));
|
||||
}
|
||||
}
|
||||
6 => {
|
||||
if verbose {
|
||||
println!("DHCP: Domain Name Server: {data:?}");
|
||||
}
|
||||
if data.len() == 4 && dns_option.is_none() {
|
||||
dns_option = Some(Vec::from(data));
|
||||
}
|
||||
}
|
||||
51 => {
|
||||
if verbose {
|
||||
println!("DHCP: Lease Time: {data:?}");
|
||||
}
|
||||
}
|
||||
53 => {
|
||||
if verbose {
|
||||
println!("DHCP: Message Type: {data:?}");
|
||||
}
|
||||
}
|
||||
54 => {
|
||||
if verbose {
|
||||
println!("DHCP: Server ID: {data:?}");
|
||||
}
|
||||
if data.len() == 4 {
|
||||
// Store the server ID
|
||||
server_id_option =
|
||||
Some([data[0], data[1], data[2], data[3]]);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if verbose {
|
||||
println!("DHCP: {option}: {data:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut renew = Dhcp::default();
|
||||
init_dhcp_header(&mut renew, current_mac, tid.wrapping_add(1));
|
||||
renew.ciaddr = offer.yiaddr;
|
||||
let rn_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST, OPT_END];
|
||||
renew.options[..rn_opts.len()].copy_from_slice(rn_opts);
|
||||
try_fmt!(send_dhcp(&renew, &socket), "failed to send renew");
|
||||
}
|
||||
|
||||
socket.set_read_timeout(Some(t2.saturating_sub(t1))).ok();
|
||||
match socket.recv(&mut ack_data) {
|
||||
Ok(_) => {
|
||||
let response = unsafe { &*(ack_data.as_ptr() as *const Dhcp) };
|
||||
match get_message_type(&response.options) {
|
||||
Some(DHCPACK) => { if verbose { println!("DHCP: renewed"); } }
|
||||
Some(DHCPNAK) => {
|
||||
if verbose { println!("DHCP: NAK, restarting"); }
|
||||
return dhcp(iface, verbose);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mask_len = if let Some(subnet) = subnet_option {
|
||||
let mut subnet: u32 = (subnet[0] as u32) << 24
|
||||
| (subnet[1] as u32) << 16
|
||||
| (subnet[2] as u32) << 8
|
||||
| subnet[3] as u32;
|
||||
subnet = !subnet;
|
||||
subnet.leading_zeros()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let new_ips = format!(
|
||||
"{}.{}.{}.{}/{}\n",
|
||||
offer.yiaddr[0], offer.yiaddr[1], offer.yiaddr[2], offer.yiaddr[3], mask_len
|
||||
);
|
||||
try_fmt!(
|
||||
set_iface_cfg_value(iface, "addr/set", &new_ips),
|
||||
"failed to set ip"
|
||||
);
|
||||
|
||||
if verbose {
|
||||
let new_ip = try_fmt!(get_iface_cfg_value(iface, "addr/list"), "failed to get ip");
|
||||
println!("DHCP: New IP: {}", new_ip.trim());
|
||||
}
|
||||
|
||||
if let Some(router) = router_option {
|
||||
let default_route = format!(
|
||||
"default via {}.{}.{}.{}",
|
||||
router[0], router[1], router[2], router[3]
|
||||
);
|
||||
|
||||
try_fmt!(
|
||||
set_cfg_value("route/add", &default_route),
|
||||
"failed to set default route"
|
||||
);
|
||||
|
||||
if verbose {
|
||||
let new_router = try_fmt!(get_cfg_value("route/list"), "failed to get ip router");
|
||||
println!("DHCP: New Router: {}", new_router.trim());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(mut dns) = dns_option {
|
||||
if dns[0] == 127 {
|
||||
let quad9 = [9, 9, 9, 9].to_vec();
|
||||
if verbose {
|
||||
println!(
|
||||
"DHCP: Received sarcastic DNS suggestion {}.{}.{}.{}, using {}.{}.{}.{} instead",
|
||||
dns[0], dns[1], dns[2], dns[3], quad9[0], quad9[1], quad9[2], quad9[3]
|
||||
);
|
||||
}
|
||||
dns = quad9;
|
||||
}
|
||||
|
||||
let nameserver = format!("{}.{}.{}.{}", dns[0], dns[1], dns[2], dns[3]);
|
||||
|
||||
try_fmt!(
|
||||
set_cfg_value("resolv/nameserver", &nameserver),
|
||||
"failed to set name server"
|
||||
);
|
||||
|
||||
if verbose {
|
||||
let new_dns = try_fmt!(get_cfg_value("resolv/nameserver"), "failed to get dns");
|
||||
println!("DHCP: New DNS: {}", new_dns.trim());
|
||||
Err(_) => {
|
||||
if verbose { println!("DHCP: entering REBIND state"); }
|
||||
let bind_socket = try_fmt!(UdpSocket::bind(("0.0.0.0", 68)), "failed to bind rebind");
|
||||
try_fmt!(bind_socket.connect(SocketAddr::from(([255, 255, 255, 255], 67))), "rebind connect");
|
||||
let mut rebind = Dhcp::default();
|
||||
init_dhcp_header(&mut rebind, current_mac, tid.wrapping_add(2));
|
||||
rebind.ciaddr = offer.yiaddr;
|
||||
let rb_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST, OPT_END];
|
||||
rebind.options[..rb_opts.len()].copy_from_slice(rb_opts);
|
||||
let _ = send_dhcp(&rebind, &bind_socket);
|
||||
bind_socket.set_read_timeout(Some(Duration::from_secs(10))).ok();
|
||||
if let Ok(_) = bind_socket.recv(&mut ack_data) {
|
||||
if verbose { println!("DHCP: rebound"); }
|
||||
} else {
|
||||
if verbose { println!("DHCP: lease expired, restarting"); }
|
||||
return dhcp(iface, verbose);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut request = Dhcp {
|
||||
op: 1,
|
||||
htype: 1,
|
||||
hlen: 6,
|
||||
hops: 0,
|
||||
tid,
|
||||
secs: 0,
|
||||
flags: 0,
|
||||
ciaddr: [0; 4],
|
||||
yiaddr: [0; 4],
|
||||
siaddr: [0; 4],
|
||||
giaddr: [0; 4],
|
||||
chaddr: [
|
||||
current_mac.bytes[0],
|
||||
current_mac.bytes[1],
|
||||
current_mac.bytes[2],
|
||||
current_mac.bytes[3],
|
||||
current_mac.bytes[4],
|
||||
current_mac.bytes[5],
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
],
|
||||
sname: [0; 64],
|
||||
file: [0; 128],
|
||||
magic: 0x63825363u32.to_be(),
|
||||
options: [0; 308],
|
||||
};
|
||||
|
||||
// If the server_id_option was None, use "0.0.0.0"
|
||||
let server_id = server_id_option.unwrap_or([0, 0, 0, 0]);
|
||||
|
||||
for (s, d) in [
|
||||
// DHCP Message Type (Request)
|
||||
53,
|
||||
1,
|
||||
3,
|
||||
// Requested IP Address
|
||||
50,
|
||||
4,
|
||||
offer.yiaddr[0],
|
||||
offer.yiaddr[1],
|
||||
offer.yiaddr[2],
|
||||
offer.yiaddr[3],
|
||||
// Server Identifier - use Option 54 from the Offer
|
||||
54,
|
||||
4,
|
||||
server_id[0],
|
||||
server_id[1],
|
||||
server_id[2],
|
||||
server_id[3],
|
||||
// End
|
||||
255,
|
||||
]
|
||||
.iter()
|
||||
.zip(request.options.iter_mut())
|
||||
{
|
||||
*d = *s;
|
||||
}
|
||||
|
||||
let request_data = unsafe {
|
||||
std::slice::from_raw_parts(
|
||||
(&request as *const Dhcp) as *const u8,
|
||||
std::mem::size_of::<Dhcp>(),
|
||||
)
|
||||
};
|
||||
|
||||
let _sent = try_fmt!(socket.send(request_data), "failed to send request");
|
||||
|
||||
if verbose {
|
||||
println!("DHCP: Sent Request");
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut ack_data = [0; 65536];
|
||||
try_fmt!(socket.recv(&mut ack_data), "failed to receive ack");
|
||||
let ack = unsafe { &*(ack_data.as_ptr() as *const Dhcp) };
|
||||
if verbose {
|
||||
println!(
|
||||
"DHCP: Ack IP: {:?}, Server IP: {:?}",
|
||||
ack.yiaddr, ack.siaddr
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_dhcp_config(
|
||||
iface: &str,
|
||||
router: Option<[u8; 4]>,
|
||||
dns: Option<[u8; 4]>,
|
||||
verbose: bool,
|
||||
) -> Result<(), String> {
|
||||
if let Some(router) = router {
|
||||
let route = format!("default via {}.{}.{}.{}", router[0], router[1], router[2], router[3]);
|
||||
try_fmt!(set_cfg_value("route/add", &route), "failed to set route");
|
||||
}
|
||||
if let Some(mut dns) = dns {
|
||||
if dns[0] == 127 {
|
||||
dns = [9, 9, 9, 9];
|
||||
if verbose { println!("DHCP: replaced loopback DNS with Quad9"); }
|
||||
}
|
||||
let ns = format!("{}.{}.{}.{}", dns[0], dns[1], dns[2], dns[3]);
|
||||
try_fmt!(set_cfg_value("resolv/nameserver", &ns), "failed to set DNS");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_prefix_len(subnet: Option<[u8; 4]>) -> u32 {
|
||||
let Some(subnet) = subnet else { return 24 };
|
||||
let inverted: u32 = !u32::from_be_bytes(subnet);
|
||||
inverted.leading_zeros()
|
||||
}
|
||||
|
||||
fn parse_options(options: &[u8], cb: &mut dyn FnMut(u8, &[u8])) {
|
||||
let mut i = 0;
|
||||
while i < options.len() {
|
||||
let code = options[i];
|
||||
if code == 0 { i += 1; continue; }
|
||||
if code == OPT_END { break; }
|
||||
i += 1;
|
||||
if i >= options.len() { break; }
|
||||
let len = options[i] as usize;
|
||||
i += 1;
|
||||
if i + len > options.len() { break; }
|
||||
cb(code, &options[i..i + len]);
|
||||
i += len;
|
||||
}
|
||||
}
|
||||
|
||||
fn get_message_type(options: &[u8]) -> Option<u8> {
|
||||
let mut msg_type = None;
|
||||
parse_options(options, &mut |code, data| {
|
||||
if code == OPT_MESSAGE_TYPE && data.len() == 1 {
|
||||
msg_type = Some(data[0]);
|
||||
}
|
||||
});
|
||||
msg_type
|
||||
}
|
||||
|
||||
fn init_dhcp_header(pkt: &mut Dhcp, mac: MacAddr, tid: u32) {
|
||||
*pkt = Dhcp::default();
|
||||
pkt.op = 1;
|
||||
pkt.htype = 1;
|
||||
pkt.hlen = 6;
|
||||
pkt.tid = tid;
|
||||
pkt.flags = 0x8000u16.to_be();
|
||||
pkt.chaddr[..6].copy_from_slice(&mac.bytes);
|
||||
pkt.magic = 0x63825363u32.to_be();
|
||||
}
|
||||
|
||||
fn send_dhcp(pkt: &Dhcp, socket: &UdpSocket) -> Result<(), String> {
|
||||
let data = unsafe {
|
||||
std::slice::from_raw_parts(pkt as *const Dhcp as *const u8, std::mem::size_of::<Dhcp>())
|
||||
};
|
||||
socket.send(data).map(|_| ()).map_err(|e| format!("send: {}", e))
|
||||
}
|
||||
|
||||
impl Default for Dhcp {
|
||||
fn default() -> Self {
|
||||
Dhcp {
|
||||
op: 0, htype: 0, hlen: 0, hops: 0, tid: 0, secs: 0, flags: 0,
|
||||
ciaddr: [0; 4], yiaddr: [0; 4], siaddr: [0; 4], giaddr: [0; 4],
|
||||
chaddr: [0; 16], sname: [0; 64], file: [0; 128], magic: 0,
|
||||
options: [0; 308],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut verbose = false;
|
||||
let iface = "eth0";
|
||||
|
||||
//TODO: parse iface from the args
|
||||
for arg in env::args().skip(1) {
|
||||
match arg.as_ref() {
|
||||
"-v" => verbose = true,
|
||||
@@ -470,11 +361,8 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn from_str_test() {
|
||||
let mac = MacAddr {
|
||||
bytes: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab],
|
||||
};
|
||||
let mac = MacAddr { bytes: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab] };
|
||||
let empty_mac = MacAddr::default();
|
||||
|
||||
assert_eq!(mac, MacAddr::from_str("01:23:45:67:89:ab"));
|
||||
assert_eq!(mac, MacAddr::from_str("1:23:45:67:89:ab"));
|
||||
assert_eq!(mac, MacAddr::from_str("01:23:45:67:89:AB"));
|
||||
@@ -483,15 +371,6 @@ mod test {
|
||||
assert_eq!(empty_mac, MacAddr::from_str("01:23:45:67:89"));
|
||||
assert_eq!(empty_mac, MacAddr::from_str("01:23:45:67:89:ab:cd"));
|
||||
assert_eq!(empty_mac, MacAddr::from_str("x1:23:45:67:89:ab"));
|
||||
assert_eq!(empty_mac, MacAddr::from_str("01:23-45-67-89-ab"));
|
||||
assert_eq!(empty_mac, MacAddr::from_str("01-23-45-67-89-ag"));
|
||||
assert_eq!(empty_mac, MacAddr::from_str("01.23.45.67.89.ab"));
|
||||
assert_eq!(empty_mac, MacAddr::from_str("01234-23-45-67-89-ab"));
|
||||
assert_eq!(empty_mac, MacAddr::from_str("01--23-45-67-89-ab"));
|
||||
assert_eq!(empty_mac, MacAddr::from_str("12"));
|
||||
assert_eq!(empty_mac, MacAddr::from_str("0:0:0:0:0:0"));
|
||||
|
||||
assert_eq!(mac, MacAddr::from_str(&mac.to_string()));
|
||||
assert_eq!(empty_mac, MacAddr::from_str(&empty_mac.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
[package]
|
||||
name = "dhcpv6d"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
@@ -0,0 +1,286 @@
|
||||
//! dhcpv6d — DHCPv6 client daemon for Red Bear OS.
|
||||
//!
|
||||
//! Mirrors Linux 7.1's systemd-networkd `sd-dhcp6-client.c`.
|
||||
//! Implements RFC 8415 state machine:
|
||||
//! SOLICIT → ADVERTISE → REQUEST → REPLY
|
||||
//!
|
||||
//! Reference:
|
||||
//! - `sd-dhcp6-client.c` — DHCPv6 client state machine
|
||||
//! - RFC 8415 §17 — client behavior
|
||||
//! - RFC 3315 — original DHCPv6 specification
|
||||
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{SocketAddr, UdpSocket};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use std::{env, process, time};
|
||||
|
||||
const DHCPV6_CLIENT_PORT: u16 = 546;
|
||||
const DHCPV6_SERVER_PORT: u16 = 547;
|
||||
const MULTICAST_ADDR: &str = "ff02::1:2";
|
||||
|
||||
const SOLICIT: u8 = 1;
|
||||
const ADVERTISE: u8 = 2;
|
||||
const REQUEST: u8 = 3;
|
||||
const REPLY: u8 = 7;
|
||||
const RELEASE: u8 = 8;
|
||||
const INFO_REQUEST: u8 = 11;
|
||||
|
||||
const OPT_CLIENTID: u16 = 1;
|
||||
const OPT_SERVERID: u16 = 2;
|
||||
const OPT_IA_NA: u16 = 3;
|
||||
const OPT_IAADDR: u16 = 5;
|
||||
const OPT_ORO: u16 = 6;
|
||||
const OPT_DNS: u16 = 23;
|
||||
const OPT_DOMAIN: u16 = 24;
|
||||
|
||||
macro_rules! try_fmt {
|
||||
($e:expr, $m:expr) => {
|
||||
match $e {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => return Err(format!("{}: {}", $m, err)),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn set_cfg(path: &str, value: &str) -> Result<(), String> {
|
||||
let full = format!("/scheme/netcfg/{path}");
|
||||
let mut f = OpenOptions::new().write(true).create(false).open(&full)
|
||||
.map_err(|_| format!("open {}", full))?;
|
||||
f.write_all(value.as_bytes()).map_err(|_| format!("write {}", full))?;
|
||||
f.sync_data().map_err(|_| "sync failed".into())
|
||||
}
|
||||
|
||||
fn get_mac() -> Result<[u8; 6], String> {
|
||||
let s = fs::read_to_string("/scheme/netcfg/ifaces/eth0/mac")
|
||||
.map_err(|e| format!("read mac: {}", e))?;
|
||||
let mut bytes = [0u8; 6];
|
||||
for (i, part) in s.trim().split(&[':', '-'][..]).take(6).enumerate() {
|
||||
bytes[i] = u8::from_str_radix(part, 16).map_err(|_| "bad mac".to_string())?;
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
fn build_duid(mac: [u8; 6]) -> Vec<u8> {
|
||||
let time = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as u32;
|
||||
let mut duid = vec![0u8; 14];
|
||||
duid[0..2].copy_from_slice(&[0x00, 0x01]);
|
||||
duid[2..4].copy_from_slice(&[0x00, 0x01]);
|
||||
duid[4..8].copy_from_slice(&time.to_be_bytes());
|
||||
duid[8..14].copy_from_slice(&mac);
|
||||
duid
|
||||
}
|
||||
|
||||
fn put_u16(buf: &mut Vec<u8>, val: u16) {
|
||||
buf.extend_from_slice(&val.to_be_bytes());
|
||||
}
|
||||
|
||||
fn put_u32(buf: &mut Vec<u8>, val: u32) {
|
||||
buf.extend_from_slice(&val.to_be_bytes());
|
||||
}
|
||||
|
||||
fn put_option(buf: &mut Vec<u8>, code: u16, data: &[u8]) {
|
||||
put_u16(buf, code);
|
||||
put_u16(buf, data.len() as u16);
|
||||
buf.extend_from_slice(data);
|
||||
}
|
||||
|
||||
struct Dhcp6Packet {
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Dhcp6Packet {
|
||||
fn new(msg_type: u8, tid: [u8; 3], duid: &[u8]) -> Self {
|
||||
let mut data = vec![msg_type];
|
||||
data.extend_from_slice(&tid);
|
||||
put_option(&mut data, OPT_CLIENTID, duid);
|
||||
put_option(&mut data, OPT_ORO, &[0, OPT_DNS as u8, 0, OPT_DOMAIN as u8]);
|
||||
Dhcp6Packet { data }
|
||||
}
|
||||
|
||||
fn add_ia_na(&mut self, iaid: u32, t1: u32, t2: u32) {
|
||||
let mut ia = Vec::new();
|
||||
put_u32(&mut ia, iaid);
|
||||
put_u32(&mut ia, t1);
|
||||
put_u32(&mut ia, t2);
|
||||
put_option(&mut self.data, OPT_IA_NA, &ia);
|
||||
}
|
||||
}
|
||||
|
||||
struct Dhcp6Reply {
|
||||
msg_type: u8,
|
||||
server_id: Option<Vec<u8>>,
|
||||
addresses: Vec<([u8; 16], u32, u32)>,
|
||||
dns_servers: Vec<[u8; 16]>,
|
||||
domains: Vec<String>,
|
||||
}
|
||||
|
||||
fn parse_reply(data: &[u8]) -> Result<Dhcp6Reply, String> {
|
||||
if data.len() < 4 {
|
||||
return Err("packet too short".into());
|
||||
}
|
||||
let msg_type = data[0];
|
||||
let mut reply = Dhcp6Reply {
|
||||
msg_type,
|
||||
server_id: None,
|
||||
addresses: Vec::new(),
|
||||
dns_servers: Vec::new(),
|
||||
domains: Vec::new(),
|
||||
};
|
||||
let mut pos = 4;
|
||||
while pos + 4 <= data.len() {
|
||||
let code = u16::from_be_bytes([data[pos], data[pos + 1]]);
|
||||
let len = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
|
||||
pos += 4;
|
||||
if pos + len > data.len() {
|
||||
break;
|
||||
}
|
||||
let opt_data = &data[pos..pos + len];
|
||||
match code {
|
||||
OPT_SERVERID => reply.server_id = Some(opt_data.to_vec()),
|
||||
OPT_DNS => {
|
||||
for chunk in opt_data.chunks(16) {
|
||||
if chunk.len() == 16 {
|
||||
let mut addr = [0u8; 16];
|
||||
addr.copy_from_slice(chunk);
|
||||
reply.dns_servers.push(addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
OPT_IA_NA => {
|
||||
if opt_data.len() >= 12 {
|
||||
let t1 = u32::from_be_bytes([opt_data[4], opt_data[5], opt_data[6], opt_data[7]]);
|
||||
let t2 = u32::from_be_bytes([opt_data[8], opt_data[9], opt_data[10], opt_data[11]]);
|
||||
let mut inner = &opt_data[12..];
|
||||
while inner.len() >= 4 {
|
||||
let inner_code = u16::from_be_bytes([inner[0], inner[1]]);
|
||||
let inner_len = u16::from_be_bytes([inner[2], inner[3]]) as usize;
|
||||
inner = &inner[4..];
|
||||
if inner.len() < inner_len { break; }
|
||||
if inner_code == OPT_IAADDR && inner_len >= 24 {
|
||||
let mut addr = [0u8; 16];
|
||||
addr.copy_from_slice(&inner[..16]);
|
||||
let pref = u32::from_be_bytes([inner[16], inner[17], inner[18], inner[19]]);
|
||||
let valid = u32::from_be_bytes([inner[20], inner[21], inner[22], inner[23]]);
|
||||
reply.addresses.push((addr, pref, valid));
|
||||
}
|
||||
inner = &inner[inner_len..];
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
pos += len;
|
||||
}
|
||||
Ok(reply)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut verbose = false;
|
||||
for arg in env::args().skip(1) {
|
||||
if arg == "-v" || arg == "--verbose" { verbose = true; }
|
||||
}
|
||||
|
||||
if let Err(e) = run(verbose) {
|
||||
eprintln!("dhcpv6d: {}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run(verbose: bool) -> Result<(), String> {
|
||||
let mac = get_mac()?;
|
||||
let duid = build_duid(mac);
|
||||
|
||||
if verbose {
|
||||
println!("dhcpv6d: DUID {:02x?}", duid);
|
||||
}
|
||||
|
||||
let socket = try_fmt!(
|
||||
UdpSocket::bind((MULTICAST_ADDR, DHCPV6_CLIENT_PORT)),
|
||||
"bind"
|
||||
);
|
||||
try_fmt!(
|
||||
socket.connect(SocketAddr::new(
|
||||
MULTICAST_ADDR.parse().map_err(|_| "bad addr")?,
|
||||
DHCPV6_SERVER_PORT,
|
||||
)),
|
||||
"connect"
|
||||
);
|
||||
try_fmt!(socket.set_read_timeout(Some(Duration::from_secs(5))), "timeout");
|
||||
|
||||
let tid = [
|
||||
(mac[0] ^ mac[1]) as u8,
|
||||
(mac[2] ^ mac[3]) as u8,
|
||||
(mac[4] ^ mac[5]) as u8,
|
||||
];
|
||||
|
||||
// SOLICIT
|
||||
let mut solicit = Dhcp6Packet::new(SOLICIT, tid, &duid);
|
||||
solicit.add_ia_na(1, 0, 0);
|
||||
try_fmt!(socket.send(&solicit.data), "send solicit");
|
||||
if verbose { println!("dhcpv6d: sent SOLICIT"); }
|
||||
|
||||
// Recv ADVERTISE
|
||||
let mut buf = [0u8; 65536];
|
||||
let n = try_fmt!(socket.recv(&mut buf), "recv advertise");
|
||||
let adv = parse_reply(&buf[..n])?;
|
||||
if verbose {
|
||||
println!("dhcpv6d: received ADVERTISE, {} addresses", adv.addresses.len());
|
||||
}
|
||||
|
||||
// REQUEST
|
||||
let mut request = Dhcp6Packet::new(REQUEST, tid, &duid);
|
||||
if let Some(ref sid) = adv.server_id {
|
||||
put_option(&mut request.data, OPT_SERVERID, sid);
|
||||
}
|
||||
request.add_ia_na(1, 0, 0);
|
||||
try_fmt!(socket.send(&request.data), "send request");
|
||||
if verbose { println!("dhcpv6d: sent REQUEST"); }
|
||||
|
||||
// Recv REPLY
|
||||
let n = try_fmt!(socket.recv(&mut buf), "recv reply");
|
||||
let reply = parse_reply(&buf[..n])?;
|
||||
if verbose {
|
||||
println!("dhcpv6d: received REPLY, {} addresses", reply.addresses.len());
|
||||
}
|
||||
|
||||
for (addr, pref, valid) in &reply.addresses {
|
||||
let addr_str = format!(
|
||||
"{:04x}:{:04x}:{:04x}:{:04x}:{:04x}:{:04x}:{:04x}:{:04x}",
|
||||
u16::from_be_bytes([addr[0], addr[1]]),
|
||||
u16::from_be_bytes([addr[2], addr[3]]),
|
||||
u16::from_be_bytes([addr[4], addr[5]]),
|
||||
u16::from_be_bytes([addr[6], addr[7]]),
|
||||
u16::from_be_bytes([addr[8], addr[9]]),
|
||||
u16::from_be_bytes([addr[10], addr[11]]),
|
||||
u16::from_be_bytes([addr[12], addr[13]]),
|
||||
u16::from_be_bytes([addr[14], addr[15]]),
|
||||
);
|
||||
let cidr = format!("{}/128\n", addr_str);
|
||||
try_fmt!(set_cfg("ifaces/eth0/addr/set", &cidr), "set addr");
|
||||
if verbose {
|
||||
println!("dhcpv6d: configured {} (pref={}s valid={}s)", addr_str, pref, valid);
|
||||
}
|
||||
}
|
||||
|
||||
for dns in &reply.dns_servers {
|
||||
let dns_str = format!(
|
||||
"{:04x}:{:04x}:{:04x}:{:04x}:{:04x}:{:04x}:{:04x}:{:04x}",
|
||||
u16::from_be_bytes([dns[0], dns[1]]),
|
||||
u16::from_be_bytes([dns[2], dns[3]]),
|
||||
u16::from_be_bytes([dns[4], dns[5]]),
|
||||
u16::from_be_bytes([dns[6], dns[7]]),
|
||||
u16::from_be_bytes([dns[8], dns[9]]),
|
||||
u16::from_be_bytes([dns[10], dns[11]]),
|
||||
u16::from_be_bytes([dns[12], dns[13]]),
|
||||
u16::from_be_bytes([dns[14], dns[15]]),
|
||||
);
|
||||
try_fmt!(set_cfg("resolv/nameserver6", &dns_str), "set DNS");
|
||||
if verbose { println!("dhcpv6d: DNS6 {}", dns_str); }
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -402,6 +402,12 @@ pub struct AcpiContext {
|
||||
pub next_ctx: RwLock<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct PowerCache {
|
||||
pub batteries: Vec<String>,
|
||||
pub adapter: Option<String>,
|
||||
}
|
||||
|
||||
impl AcpiContext {
|
||||
pub fn aml_eval(
|
||||
&self,
|
||||
@@ -766,6 +772,42 @@ impl AcpiContext {
|
||||
adapters
|
||||
}
|
||||
|
||||
pub fn power_devices(&self) -> PowerCache {
|
||||
let mut cache = PowerCache::default();
|
||||
let Ok(aml_symbols) = self.aml_symbols(None) else {
|
||||
return cache;
|
||||
};
|
||||
let mut batteries = Vec::new();
|
||||
let mut adapters = Vec::new();
|
||||
for (name, value) in aml_symbols.symbols_cache().iter() {
|
||||
if value.contains("Method(") && name.contains("._BST") {
|
||||
let base = name.trim_end_matches("._BST");
|
||||
if is_battery_candidate(base) {
|
||||
batteries.push(base.to_string());
|
||||
}
|
||||
}
|
||||
if value.contains("Method(") && name.contains("._PSR") {
|
||||
let base = name.trim_end_matches("._PSR");
|
||||
if is_adapter_candidate(base) {
|
||||
adapters.push(base.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
batteries.sort();
|
||||
batteries.dedup();
|
||||
adapters.sort();
|
||||
adapters.dedup();
|
||||
cache.batteries = batteries
|
||||
.into_iter()
|
||||
.map(|name| name.rsplit('.').next().unwrap_or(&name).to_string())
|
||||
.collect();
|
||||
cache.adapter = adapters
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|name| name.rsplit('.').next().unwrap_or(&name).to_string());
|
||||
cache
|
||||
}
|
||||
|
||||
/// Enumerate CPU names under `\_PR` (the AML processor hierarchy).
|
||||
/// Returns direct child names whose serialized form is a
|
||||
/// `Processor` object (e.g. "\_PR.CPU0", "\_PR.CPU1"). Returns an
|
||||
@@ -1150,6 +1192,114 @@ impl AcpiContext {
|
||||
Err(e) => Err(AmlEvalError::from(e)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn battery_status_text(&self, battery_name: &str, file: &str) -> String {
|
||||
let path = format!("\\_SB.{}.{}", battery_name, file);
|
||||
match file {
|
||||
"state" => self.battery_state_text(&path),
|
||||
"percentage" => self.battery_percentage_text(battery_name),
|
||||
_ => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn battery_state_text(&self, battery_name: &str) -> String {
|
||||
let path = power_object_path(battery_name, "_BST");
|
||||
let result = self.eval_power_method(&path);
|
||||
let state = match result {
|
||||
Ok(Some(obj)) => obj.as_integer().unwrap_or(0) as u32,
|
||||
_ => 0,
|
||||
};
|
||||
format!("{}\n", state)
|
||||
}
|
||||
|
||||
pub(crate) fn battery_percentage_text(&self, battery_name: &str) -> String {
|
||||
let cache = self.power_battery_info(battery_name);
|
||||
format!("{}\n", cache.percentage.unwrap_or(0))
|
||||
}
|
||||
|
||||
fn power_battery_info(&self, battery_name: &str) -> BatteryInfo {
|
||||
BatteryInfo::new(self, battery_name)
|
||||
}
|
||||
|
||||
pub(crate) fn adapter_online_text(&self, adapter_name: &str) -> String {
|
||||
let path = power_object_path(adapter_name, "_PSR");
|
||||
let online = self
|
||||
.eval_power_method(&path)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|obj| obj.as_integer().ok())
|
||||
.unwrap_or(0);
|
||||
format!("{}\n", online)
|
||||
}
|
||||
|
||||
fn eval_power_method(&self, path: &str) -> Result<Option<Object>, AmlEvalError> {
|
||||
let mut symbols = self.aml_symbols.write();
|
||||
let aml_name = AmlName::from_str(path).map_err(|_| AmlEvalError::DeserializationError)?;
|
||||
let interpreter = symbols.aml_context_mut(None)?;
|
||||
interpreter.acquire_global_lock(16)?;
|
||||
let result = interpreter.evaluate_if_present(aml_name, Vec::new());
|
||||
interpreter
|
||||
.release_global_lock()
|
||||
.expect("acpid: failed to release AML global lock");
|
||||
match result {
|
||||
Ok(Some(obj)) => Ok(Some(obj.unwrap_reference().deref().clone())),
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(AmlEvalError::from(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn power_object_path(name: &str, method: &str) -> String {
|
||||
if name.starts_with('\\') {
|
||||
format!("{}.{}", name, method)
|
||||
} else {
|
||||
format!("\\_SB.{}.{}", name, method)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct BatteryInfo {
|
||||
percentage: Option<u32>,
|
||||
}
|
||||
|
||||
impl BatteryInfo {
|
||||
fn new(ctx: &AcpiContext, battery_name: &str) -> Self {
|
||||
let mut info = Self::default();
|
||||
let path = power_object_path(battery_name, "_BIF");
|
||||
let alt = power_object_path(battery_name, "_BIX");
|
||||
if let Some((remaining, full)) = read_battery_capacity(ctx, &path).or_else(|| read_battery_capacity(ctx, &alt)) {
|
||||
if full > 0 {
|
||||
info.percentage = Some(((remaining.saturating_mul(100)) / full).min(100) as u32);
|
||||
}
|
||||
}
|
||||
info
|
||||
}
|
||||
}
|
||||
|
||||
fn is_battery_candidate(base: &str) -> bool {
|
||||
base.ends_with("BAT") || base.ends_with("BAT0") || base.ends_with("BAT1")
|
||||
}
|
||||
|
||||
fn is_adapter_candidate(base: &str) -> bool {
|
||||
base.ends_with("AC") || base.ends_with("ACAD") || base.ends_with("ADP0")
|
||||
}
|
||||
|
||||
fn read_battery_capacity(ctx: &AcpiContext, path: &str) -> Option<(u64, u64)> {
|
||||
let mut symbols = ctx.aml_symbols.write();
|
||||
let aml_name = AmlName::from_str(path).ok()?;
|
||||
let interpreter = symbols.aml_context_mut(None).ok()?;
|
||||
interpreter.acquire_global_lock(16).ok()?;
|
||||
let result = interpreter.evaluate_if_present(aml_name, Vec::new());
|
||||
interpreter.release_global_lock().ok()?;
|
||||
let obj = result.ok()?.unwrap();
|
||||
let binding = obj.unwrap_reference();
|
||||
let package = binding.deref();
|
||||
let elements = obj_as_package(package)?;
|
||||
let vals: Vec<u64> = elements.iter().filter_map(elem_as_integer).collect();
|
||||
if vals.len() < 5 {
|
||||
return None;
|
||||
}
|
||||
Some((vals.get(2).copied()?, vals.get(4).copied()?))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -23,7 +23,7 @@ use syscall::flag::{MODE_DIR, MODE_FILE};
|
||||
use syscall::flag::{O_ACCMODE, O_DIRECTORY, O_RDONLY, O_STAT, O_SYMLINK};
|
||||
use syscall::{EOVERFLOW, EPERM};
|
||||
|
||||
use crate::acpi::{AcpiContext, AmlSymbols, SdtSignature};
|
||||
use crate::acpi::{AcpiContext, AmlSymbols, PowerCache, SdtSignature};
|
||||
use crate::dmi::DMI_FIELDS;
|
||||
|
||||
pub struct AcpiScheme<'acpi, 'sock> {
|
||||
@@ -35,6 +35,7 @@ pub struct AcpiScheme<'acpi, 'sock> {
|
||||
/// can call `kstop_reason` (kcall 2) to query the kernel
|
||||
/// for the reason of the most recent kstop event.
|
||||
kstop_fd: Option<Fd>,
|
||||
power_cache: PowerCache,
|
||||
}
|
||||
|
||||
struct Handle<'a> {
|
||||
@@ -60,6 +61,9 @@ enum HandleKind<'a> {
|
||||
/// battery controllers. On desktops and QEMU the listing is
|
||||
/// empty.
|
||||
Power,
|
||||
PowerBatteries,
|
||||
PowerBattery { name: String, file: PowerFileKind },
|
||||
PowerAdapter { name: String, file: PowerFileKind },
|
||||
/// `/scheme/acpi/dmi` -- key=value text dump of the SMBIOS identity
|
||||
/// fields (consumed by `redox-driver-sys` quirks loader).
|
||||
Dmi,
|
||||
@@ -81,6 +85,13 @@ enum HandleKind<'a> {
|
||||
DmiDir,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum PowerFileKind {
|
||||
State,
|
||||
Percentage,
|
||||
Online,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ProcFileKind {
|
||||
Pss,
|
||||
@@ -99,7 +110,8 @@ impl HandleKind<'_> {
|
||||
Self::Symbol { .. } => false,
|
||||
Self::SchemeRoot => false,
|
||||
Self::RegisterPci => false,
|
||||
Self::Thermal | Self::Power | Self::Processor | Self::DmiDir => true,
|
||||
Self::Thermal | Self::Power | Self::Processor | Self::DmiDir | Self::PowerBatteries => true,
|
||||
Self::PowerBattery { .. } | Self::PowerAdapter { .. } => false,
|
||||
Self::Dmi => true,
|
||||
Self::DmiField(_) => false,
|
||||
Self::ProcFile { .. } => false,
|
||||
@@ -123,6 +135,7 @@ impl HandleKind<'_> {
|
||||
Self::DmiField(field) => dmi_field_contents(acpi_ctx.dmi_info(), field)
|
||||
.map(|s| s.len())
|
||||
.unwrap_or(0),
|
||||
Self::PowerBatteries | Self::PowerBattery { .. } | Self::PowerAdapter { .. } => 2,
|
||||
// Directories
|
||||
Self::TopLevel | Self::Symbols(_) | Self::Tables => 0,
|
||||
Self::Thermal | Self::Power | Self::Processor | Self::DmiDir => 0,
|
||||
@@ -144,9 +157,17 @@ impl<'acpi, 'sock> AcpiScheme<'acpi, 'sock> {
|
||||
pci_fd: None,
|
||||
socket,
|
||||
kstop_fd: None,
|
||||
power_cache: PowerCache::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn power_cache(&mut self) -> &PowerCache {
|
||||
if self.power_cache.batteries.is_empty() && self.power_cache.adapter.is_none() {
|
||||
self.power_cache = self.ctx.power_devices();
|
||||
}
|
||||
&self.power_cache
|
||||
}
|
||||
|
||||
/// Phase I.5: register the kstop handle fd. Called by the
|
||||
/// main loop right after opening the kstop handle.
|
||||
pub fn set_kstop_fd(&mut self, fd: Fd) {
|
||||
@@ -172,7 +193,7 @@ impl<'acpi, 'sock> AcpiScheme<'acpi, 'sock> {
|
||||
let handle = self.kstop_fd.as_ref().ok_or(syscall::error::Error::new(syscall::error::EBADF))?;
|
||||
let mut payload = [0u8; 8];
|
||||
let verb = AcpiVerb::CheckShutdown as u64;
|
||||
let result = handle.call_ro(&mut payload, CallFlags::empty(), &[verb])?;
|
||||
let _result = handle.call_ro(&mut payload, CallFlags::empty(), &[verb])?;
|
||||
Ok(u64::from_ne_bytes(payload))
|
||||
}
|
||||
|
||||
@@ -352,6 +373,22 @@ impl SchemeSync for AcpiScheme<'_, '_> {
|
||||
["power"] => HandleKind::Power,
|
||||
["dmi"] => HandleKind::Dmi,
|
||||
["processor"] => HandleKind::Processor,
|
||||
["power", "batteries"] => HandleKind::PowerBatteries,
|
||||
["power", "batteries", name, file] => {
|
||||
let file = match *file {
|
||||
"state" => PowerFileKind::State,
|
||||
"percentage" => PowerFileKind::Percentage,
|
||||
_ => return Err(Error::new(ENOENT)),
|
||||
};
|
||||
HandleKind::PowerBattery { name: (*name).to_owned(), file }
|
||||
}
|
||||
["power", "adapters", name, file] => {
|
||||
let file = match *file {
|
||||
"online" => PowerFileKind::Online,
|
||||
_ => return Err(Error::new(ENOENT)),
|
||||
};
|
||||
HandleKind::PowerAdapter { name: (*name).to_owned(), file }
|
||||
}
|
||||
|
||||
["tables", table] => {
|
||||
let signature = parse_table(table.as_bytes()).ok_or(Error::new(ENOENT))?;
|
||||
@@ -512,7 +549,22 @@ impl SchemeSync for AcpiScheme<'_, '_> {
|
||||
.unwrap_or_default();
|
||||
dmi_buf.as_bytes()
|
||||
}
|
||||
HandleKind::Processor | HandleKind::DmiDir | HandleKind::Thermal | HandleKind::Power | HandleKind::Symbols(_) | HandleKind::RegisterPci | HandleKind::TopLevel | HandleKind::SchemeRoot => {
|
||||
HandleKind::PowerBattery { name, file } => {
|
||||
dmi_buf = match file {
|
||||
PowerFileKind::State => self.ctx.battery_state_text(name),
|
||||
PowerFileKind::Percentage => self.ctx.battery_percentage_text(name),
|
||||
PowerFileKind::Online => String::new(),
|
||||
};
|
||||
dmi_buf.as_bytes()
|
||||
}
|
||||
HandleKind::PowerAdapter { name, file } => {
|
||||
dmi_buf = match file {
|
||||
PowerFileKind::Online => self.ctx.adapter_online_text(name),
|
||||
PowerFileKind::State | PowerFileKind::Percentage => String::new(),
|
||||
};
|
||||
dmi_buf.as_bytes()
|
||||
}
|
||||
HandleKind::Processor | HandleKind::DmiDir | HandleKind::Thermal | HandleKind::Power | HandleKind::PowerBatteries | HandleKind::Symbols(_) | HandleKind::RegisterPci | HandleKind::TopLevel | HandleKind::SchemeRoot => {
|
||||
return Err(Error::new(EISDIR));
|
||||
}
|
||||
HandleKind::ProcFile { cpu, kind } => {
|
||||
@@ -650,15 +702,42 @@ impl SchemeSync for AcpiScheme<'_, '_> {
|
||||
// Enumerate PowerResource entries. On real laptops these
|
||||
// are AC adapters and battery controllers; on desktops
|
||||
// and QEMU the list is empty.
|
||||
let adapters = self.ctx.power_adapters();
|
||||
for (idx, adapter) in adapters.iter().enumerate().skip(opaque_offset as usize) {
|
||||
let cache = self.power_cache().clone();
|
||||
if !cache.batteries.is_empty() {
|
||||
buf.entry(DirEntry {
|
||||
inode: 0,
|
||||
next_opaque_id: idx as u64 + 1,
|
||||
name: adapter.as_str(),
|
||||
next_opaque_id: 1,
|
||||
name: "batteries",
|
||||
kind: DirentKind::Directory,
|
||||
})?;
|
||||
}
|
||||
if cache.adapter.is_some() {
|
||||
buf.entry(DirEntry {
|
||||
inode: 0,
|
||||
next_opaque_id: 2,
|
||||
name: "adapters",
|
||||
kind: DirentKind::Directory,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
HandleKind::PowerBatteries => {
|
||||
let batteries = &self.power_cache().batteries;
|
||||
for (idx, battery) in batteries.iter().enumerate().skip(opaque_offset as usize) {
|
||||
buf.entry(DirEntry {
|
||||
inode: 0,
|
||||
next_opaque_id: idx as u64 + 1,
|
||||
name: battery.as_str(),
|
||||
kind: DirentKind::Directory,
|
||||
})?;
|
||||
}
|
||||
}
|
||||
HandleKind::PowerBattery { .. } => {
|
||||
for (idx, file) in ["state", "percentage"].iter().enumerate().skip(opaque_offset as usize) {
|
||||
buf.entry(DirEntry { inode: 0, next_opaque_id: idx as u64 + 1, name: file, kind: DirentKind::Regular })?;
|
||||
}
|
||||
}
|
||||
HandleKind::PowerAdapter { .. } => {
|
||||
buf.entry(DirEntry { inode: 0, next_opaque_id: 1, name: "online", kind: DirentKind::Regular })?;
|
||||
}
|
||||
HandleKind::Dmi => {
|
||||
// Consumers should `read_to_string("/scheme/acpi/dmi")`
|
||||
|
||||
@@ -13,6 +13,12 @@ use common::timeout::Timeout;
|
||||
use redox_scheme::scheme::SchemeSync;
|
||||
use redox_scheme::CallerCtx;
|
||||
use redox_scheme::OpenResult;
|
||||
use crate::hda::verbs::{
|
||||
AC_VERB_PARAMETERS, AC_VERB_GET_CONNECT_SEL, AC_VERB_GET_CONNECT_LIST,
|
||||
AC_VERB_GET_CONFIG_DEFAULT,
|
||||
AC_PAR_NODE_COUNT, AC_PAR_AUDIO_WIDGET_CAP, AC_PAR_CONNLIST_LEN,
|
||||
AC_PAR_FUNCTION_TYPE, AC_CLIST_LONG, AC_CLIST_LENGTH,
|
||||
};
|
||||
use scheme_utils::{FpathWriter, HandleMap};
|
||||
use syscall::error::{Error, Result, EACCES, EBADF, EIO, ENODEV, EWOULDBLOCK};
|
||||
|
||||
@@ -254,7 +260,7 @@ impl IntelHDA {
|
||||
|
||||
node.addr = addr;
|
||||
|
||||
temp = self.cmd.cmd12(addr, 0xF00, 0x04)?;
|
||||
temp = self.cmd.cmd12(addr, AC_VERB_PARAMETERS, AC_PAR_NODE_COUNT)?;
|
||||
|
||||
node.subnode_count = (temp & 0xff) as u16;
|
||||
node.subnode_start = ((temp >> 16) & 0xff) as u16;
|
||||
@@ -262,41 +268,37 @@ impl IntelHDA {
|
||||
if addr == (0, 0) {
|
||||
return Ok(node);
|
||||
}
|
||||
temp = self.cmd.cmd12(addr, 0xF00, 0x04)?;
|
||||
temp = self.cmd.cmd12(addr, AC_VERB_PARAMETERS, AC_PAR_FUNCTION_TYPE)?;
|
||||
|
||||
node.function_group_type = (temp & 0xff) as u8;
|
||||
|
||||
temp = self.cmd.cmd12(addr, 0xF00, 0x09)?;
|
||||
temp = self.cmd.cmd12(addr, AC_VERB_PARAMETERS, AC_PAR_AUDIO_WIDGET_CAP)?;
|
||||
node.capabilities = temp as u32;
|
||||
|
||||
temp = self.cmd.cmd12(addr, 0xF00, 0x0E)?;
|
||||
temp = self.cmd.cmd12(addr, AC_VERB_PARAMETERS, AC_PAR_CONNLIST_LEN)?;
|
||||
|
||||
node.conn_list_len = (temp & 0xFF) as u8;
|
||||
|
||||
node.connections = self.node_get_connection_list(&node)?;
|
||||
|
||||
node.connection_default = self.cmd.cmd12(addr, 0xF01, 0x00)? as u8;
|
||||
node.connection_default = self.cmd.cmd12(addr, AC_VERB_GET_CONNECT_SEL, 0x00)? as u8;
|
||||
|
||||
node.config_default = self.cmd.cmd12(addr, 0xF1C, 0x00)? as u32;
|
||||
node.config_default = self.cmd.cmd12(addr, AC_VERB_GET_CONFIG_DEFAULT, 0x00)? as u32;
|
||||
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
pub fn node_get_connection_list(&mut self, node: &HDANode) -> Result<Vec<WidgetAddr>> {
|
||||
let len_field: u8 = (self.cmd.cmd12(node.addr, 0xF00, 0x0E)? & 0xFF) as u8;
|
||||
|
||||
// Highest bit is if addresses are represented in longer notation
|
||||
// lower 7 is actual count
|
||||
|
||||
let count: u8 = len_field & 0x7F;
|
||||
let use_long_addr: bool = (len_field >> 7) & 0x1 == 1;
|
||||
let len_field: u8 = (self.cmd.cmd12(node.addr, AC_VERB_PARAMETERS, AC_PAR_CONNLIST_LEN)? & 0xFF) as u8;
|
||||
let count: u8 = len_field & (AC_CLIST_LENGTH as u8);
|
||||
let use_long_addr: bool = (len_field & (AC_CLIST_LONG as u8)) != 0;
|
||||
|
||||
let mut current: u8 = 0;
|
||||
|
||||
let mut list = Vec::<WidgetAddr>::new();
|
||||
|
||||
while current < count {
|
||||
let response: u32 = (self.cmd.cmd12(node.addr, 0xF02, current)? & 0xFFFFFFFF) as u32;
|
||||
let response: u32 = (self.cmd.cmd12(node.addr, AC_VERB_GET_CONNECT_LIST, current)? & 0xFFFFFFFF) as u32;
|
||||
|
||||
if use_long_addr {
|
||||
for i in 0..2 {
|
||||
|
||||
@@ -4,6 +4,7 @@ pub mod common;
|
||||
pub mod device;
|
||||
pub mod node;
|
||||
pub mod stream;
|
||||
pub mod verbs;
|
||||
|
||||
pub use self::node::*;
|
||||
pub use self::stream::*;
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
// HDA verb and parameter constants — ported from Linux 7.1 include/sound/hda_verbs.h.
|
||||
// The hda_verbs.h header defines the HDA specification's verb IDs, parameter IDs,
|
||||
// and capability bitfields for codec communication via CORB/RIRB.
|
||||
|
||||
// ---- Widget types (hda_verbs.h:25) ----
|
||||
pub const AC_WID_AUD_OUT: u8 = 0x00;
|
||||
pub const AC_WID_AUD_IN: u8 = 0x01;
|
||||
pub const AC_WID_AUD_MIX: u8 = 0x02;
|
||||
pub const AC_WID_AUD_SEL: u8 = 0x03;
|
||||
pub const AC_WID_PIN: u8 = 0x04;
|
||||
pub const AC_WID_POWER: u8 = 0x05;
|
||||
pub const AC_WID_VOL_KNB: u8 = 0x06;
|
||||
pub const AC_WID_BEEP: u8 = 0x07;
|
||||
pub const AC_WID_VENDOR: u8 = 0x0f;
|
||||
|
||||
// ---- GET verbs (hda_verbs.h:40-84) ----
|
||||
pub const AC_VERB_GET_STREAM_FORMAT: u32 = 0x0a00;
|
||||
pub const AC_VERB_GET_AMP_GAIN_MUTE: u32 = 0x0b00;
|
||||
pub const AC_VERB_GET_PROC_COEF: u32 = 0x0c00;
|
||||
pub const AC_VERB_GET_COEF_INDEX: u32 = 0x0d00;
|
||||
pub const AC_VERB_PARAMETERS: u32 = 0x0f00;
|
||||
pub const AC_VERB_GET_CONNECT_SEL: u32 = 0x0f01;
|
||||
pub const AC_VERB_GET_CONNECT_LIST: u32 = 0x0f02;
|
||||
pub const AC_VERB_GET_PROC_STATE: u32 = 0x0f03;
|
||||
pub const AC_VERB_GET_SDI_SELECT: u32 = 0x0f04;
|
||||
pub const AC_VERB_GET_POWER_STATE: u32 = 0x0f05;
|
||||
pub const AC_VERB_GET_CONV: u32 = 0x0f06;
|
||||
pub const AC_VERB_GET_PIN_WIDGET_CONTROL: u32 = 0x0f07;
|
||||
pub const AC_VERB_GET_UNSOLICITED_RESPONSE: u32 = 0x0f08;
|
||||
pub const AC_VERB_GET_PIN_SENSE: u32 = 0x0f09;
|
||||
pub const AC_VERB_GET_BEEP_CONTROL: u32 = 0x0f0a;
|
||||
pub const AC_VERB_GET_EAPD_BTLENABLE: u32 = 0x0f0c;
|
||||
pub const AC_VERB_GET_DIGI_CONVERT_1: u32 = 0x0f0d;
|
||||
pub const AC_VERB_GET_VOLUME_KNOB_CONTROL: u32 = 0x0f0f;
|
||||
pub const AC_VERB_GET_CONFIG_DEFAULT: u32 = 0x0f1c;
|
||||
pub const AC_VERB_GET_SUBSYSTEM_ID: u32 = 0x0f20;
|
||||
pub const AC_VERB_GET_STRIPE_CONTROL: u32 = 0x0f24;
|
||||
pub const AC_VERB_GET_CVT_CHAN_COUNT: u32 = 0x0f2d;
|
||||
pub const AC_VERB_GET_HDMI_DIP_SIZE: u32 = 0x0f2e;
|
||||
pub const AC_VERB_GET_HDMI_ELDD: u32 = 0x0f2f;
|
||||
pub const AC_VERB_GET_DEVICE_SEL: u32 = 0x0f35;
|
||||
pub const AC_VERB_GET_DEVICE_LIST: u32 = 0x0f36;
|
||||
|
||||
// ---- SET verbs (hda_verbs.h:89-131) ----
|
||||
pub const AC_VERB_SET_STREAM_FORMAT: u32 = 0x200;
|
||||
pub const AC_VERB_SET_AMP_GAIN_MUTE: u32 = 0x300;
|
||||
pub const AC_VERB_SET_PROC_COEF: u32 = 0x400;
|
||||
pub const AC_VERB_SET_COEF_INDEX: u32 = 0x500;
|
||||
pub const AC_VERB_SET_CONNECT_SEL: u32 = 0x701;
|
||||
pub const AC_VERB_SET_PROC_STATE: u32 = 0x703;
|
||||
pub const AC_VERB_SET_SDI_SELECT: u32 = 0x704;
|
||||
pub const AC_VERB_SET_POWER_STATE: u32 = 0x705;
|
||||
pub const AC_VERB_SET_CHANNEL_STREAMID: u32 = 0x706;
|
||||
pub const AC_VERB_SET_PIN_WIDGET_CONTROL: u32 = 0x707;
|
||||
pub const AC_VERB_SET_UNSOLICITED_ENABLE: u32 = 0x708;
|
||||
pub const AC_VERB_SET_PIN_SENSE: u32 = 0x709;
|
||||
pub const AC_VERB_SET_BEEP_CONTROL: u32 = 0x70a;
|
||||
pub const AC_VERB_SET_EAPD_BTLENABLE: u32 = 0x70c;
|
||||
pub const AC_VERB_SET_DIGI_CONVERT_1: u32 = 0x70d;
|
||||
pub const AC_VERB_SET_DIGI_CONVERT_2: u32 = 0x70e;
|
||||
pub const AC_VERB_SET_VOLUME_KNOB_CONTROL: u32 = 0x70f;
|
||||
pub const AC_VERB_SET_CONFIG_DEFAULT_BYTES_0: u32 = 0x71c;
|
||||
pub const AC_VERB_SET_CONFIG_DEFAULT_BYTES_1: u32 = 0x71d;
|
||||
pub const AC_VERB_SET_CONFIG_DEFAULT_BYTES_2: u32 = 0x71e;
|
||||
pub const AC_VERB_SET_CONFIG_DEFAULT_BYTES_3: u32 = 0x71f;
|
||||
pub const AC_VERB_SET_EAPD: u32 = 0x788;
|
||||
pub const AC_VERB_SET_CODEC_RESET: u32 = 0x7ff;
|
||||
pub const AC_VERB_SET_STRIPE_CONTROL: u32 = 0x724;
|
||||
pub const AC_VERB_SET_CVT_CHAN_COUNT: u32 = 0x72d;
|
||||
|
||||
// ---- Parameter IDs for AC_VERB_PARAMETERS (hda_verbs.h:136-154) ----
|
||||
pub const AC_PAR_VENDOR_ID: u8 = 0x00;
|
||||
pub const AC_PAR_SUBSYSTEM_ID: u8 = 0x01;
|
||||
pub const AC_PAR_REV_ID: u8 = 0x02;
|
||||
pub const AC_PAR_NODE_COUNT: u8 = 0x04;
|
||||
pub const AC_PAR_FUNCTION_TYPE: u8 = 0x05;
|
||||
pub const AC_PAR_AUDIO_FG_CAP: u8 = 0x08;
|
||||
pub const AC_PAR_AUDIO_WIDGET_CAP: u8 = 0x09;
|
||||
pub const AC_PAR_PCM: u8 = 0x0a;
|
||||
pub const AC_PAR_STREAM: u8 = 0x0b;
|
||||
pub const AC_PAR_PIN_CAP: u8 = 0x0c;
|
||||
pub const AC_PAR_AMP_IN_CAP: u8 = 0x0d;
|
||||
pub const AC_PAR_CONNLIST_LEN: u8 = 0x0e;
|
||||
pub const AC_PAR_POWER_STATE: u8 = 0x0f;
|
||||
pub const AC_PAR_PROC_CAP: u8 = 0x10;
|
||||
pub const AC_PAR_GPIO_CAP: u8 = 0x11;
|
||||
pub const AC_PAR_AMP_OUT_CAP: u8 = 0x12;
|
||||
pub const AC_PAR_VOL_KNB_CAP: u8 = 0x13;
|
||||
pub const AC_PAR_DEVLIST_LEN: u8 = 0x15;
|
||||
pub const AC_PAR_HDMI_LPCM_CAP: u8 = 0x20;
|
||||
|
||||
// ---- Audio Widget Capabilities (hda_verbs.h:171-188) ----
|
||||
pub const AC_WCAP_STEREO: u32 = 1 << 0;
|
||||
pub const AC_WCAP_IN_AMP: u32 = 1 << 1;
|
||||
pub const AC_WCAP_OUT_AMP: u32 = 1 << 2;
|
||||
pub const AC_WCAP_AMP_OVRD: u32 = 1 << 3;
|
||||
pub const AC_WCAP_FORMAT_OVRD: u32 = 1 << 4;
|
||||
pub const AC_WCAP_STRIPE: u32 = 1 << 5;
|
||||
pub const AC_WCAP_PROC_WID: u32 = 1 << 6;
|
||||
pub const AC_WCAP_UNSOL_CAP: u32 = 1 << 7;
|
||||
pub const AC_WCAP_CONN_LIST: u32 = 1 << 8;
|
||||
pub const AC_WCAP_DIGITAL: u32 = 1 << 9;
|
||||
pub const AC_WCAP_POWER: u32 = 1 << 10;
|
||||
pub const AC_WCAP_LR_SWAP: u32 = 1 << 11;
|
||||
pub const AC_WCAP_CP_CAPS: u32 = 1 << 12;
|
||||
pub const AC_WCAP_CHAN_CNT_EXT: u32 = 7 << 13;
|
||||
pub const AC_WCAP_DELAY: u32 = 0xf << 16;
|
||||
pub const AC_WCAP_DELAY_SHIFT: u8 = 16;
|
||||
pub const AC_WCAP_TYPE: u32 = 0xf << 20;
|
||||
pub const AC_WCAP_TYPE_SHIFT: u8 = 20;
|
||||
|
||||
// ---- Pin Capabilities (hda_verbs.h:262-289) ----
|
||||
pub const AC_PINCAP_IMP_SENSE: u32 = 1 << 0;
|
||||
pub const AC_PINCAP_TRIG_REQ: u32 = 1 << 1;
|
||||
pub const AC_PINCAP_PRES_DETECT: u32 = 1 << 2;
|
||||
pub const AC_PINCAP_HP_DRV: u32 = 1 << 3;
|
||||
pub const AC_PINCAP_OUT: u32 = 1 << 4;
|
||||
pub const AC_PINCAP_IN: u32 = 1 << 5;
|
||||
pub const AC_PINCAP_BALANCE: u32 = 1 << 6;
|
||||
pub const AC_PINCAP_HDMI: u32 = 1 << 7;
|
||||
pub const AC_PINCAP_DP: u32 = 1 << 24;
|
||||
pub const AC_PINCAP_VREF: u32 = 0x37 << 8;
|
||||
pub const AC_PINCAP_VREF_SHIFT: u8 = 8;
|
||||
pub const AC_PINCAP_EAPD: u32 = 1 << 16;
|
||||
pub const AC_PINCAP_HBR: u32 = 1 << 27;
|
||||
|
||||
// ---- Pin Widget Control (hda_verbs.h:400-411) ----
|
||||
pub const AC_PINCTL_EPT: u32 = 0x3;
|
||||
pub const AC_PINCTL_EPT_NATIVE: u8 = 0;
|
||||
pub const AC_PINCTL_EPT_HBR: u8 = 3;
|
||||
pub const AC_PINCTL_IN_EN: u8 = 1 << 5;
|
||||
pub const AC_PINCTL_OUT_EN: u8 = 1 << 6;
|
||||
pub const AC_PINCTL_HP_EN: u8 = 1 << 7;
|
||||
|
||||
// ---- Pin Sense (hda_verbs.h:414-416) ----
|
||||
pub const AC_PINSENSE_IMPEDANCE_MASK: u32 = 0x7fff_ffff;
|
||||
pub const AC_PINSENSE_PRESENCE: u32 = 1 << 31;
|
||||
pub const AC_PINSENSE_ELDV: u32 = 1 << 30;
|
||||
|
||||
// ---- Power State (hda_verbs.h:311-330) ----
|
||||
pub const AC_PWRST_D0SUP: u32 = 1 << 0;
|
||||
pub const AC_PWRST_D1SUP: u32 = 1 << 1;
|
||||
pub const AC_PWRST_D2SUP: u32 = 1 << 2;
|
||||
pub const AC_PWRST_D3SUP: u32 = 1 << 3;
|
||||
pub const AC_PWRST_D3COLDSUP: u32 = 1 << 4;
|
||||
pub const AC_PWRST_S3D3COLDSUP: u32 = 1 << 29;
|
||||
pub const AC_PWRST_CLKSTOP: u32 = 1 << 30;
|
||||
pub const AC_PWRST_EPSS: u32 = 1 << 31;
|
||||
pub const AC_PWRST_SETTING: u32 = 0xf;
|
||||
pub const AC_PWRST_ACTUAL: u32 = 0xf << 4;
|
||||
pub const AC_PWRST_ACTUAL_SHIFT: u8 = 4;
|
||||
pub const AC_PWRST_D0: u32 = 0x00;
|
||||
pub const AC_PWRST_D1: u32 = 0x01;
|
||||
pub const AC_PWRST_D2: u32 = 0x02;
|
||||
pub const AC_PWRST_D3: u32 = 0x03;
|
||||
pub const AC_PWRST_ERROR: u32 = 1 << 8;
|
||||
pub const AC_PWRST_CLK_STOP_OK: u32 = 1 << 9;
|
||||
pub const AC_PWRST_SETTING_RESET: u32 = 1 << 10;
|
||||
|
||||
// ---- Amplifier (hda_verbs.h:366-380) ----
|
||||
pub const AC_AMP_MUTE: u8 = 1 << 7;
|
||||
pub const AC_AMP_GAIN: u8 = 0x7f;
|
||||
pub const AC_AMP_GET_LEFT: u32 = 1 << 13;
|
||||
pub const AC_AMP_GET_OUTPUT: u32 = 1 << 15;
|
||||
|
||||
// ---- PCM/Stream format (hda_verbs.h:191-235) ----
|
||||
pub const AC_SUPPCM_BITS_8: u32 = 1 << 16;
|
||||
pub const AC_SUPPCM_BITS_16: u32 = 1 << 17;
|
||||
pub const AC_SUPPCM_BITS_20: u32 = 1 << 18;
|
||||
pub const AC_SUPPCM_BITS_24: u32 = 1 << 19;
|
||||
pub const AC_SUPPCM_BITS_32: u32 = 1 << 20;
|
||||
pub const AC_SUPFMT_PCM: u32 = 1;
|
||||
|
||||
// ---- DIGITAL1 (hda_verbs.h:383-391) ----
|
||||
pub const AC_DIG1_ENABLE: u32 = 1;
|
||||
pub const AC_DIG1_V: u32 = 1 << 1;
|
||||
pub const AC_DIG1_EMPHASIS: u32 = 1 << 3;
|
||||
pub const AC_DIG1_COPYRIGHT: u32 = 1 << 4;
|
||||
pub const AC_DIG1_NONAUDIO: u32 = 1 << 5;
|
||||
pub const AC_DIG1_PROFESSIONAL: u32 = 1 << 6;
|
||||
pub const AC_DIG1_LEVEL: u32 = 1 << 7;
|
||||
|
||||
// ---- Connection List (hda_verbs.h:307-308) ----
|
||||
pub const AC_CLIST_LENGTH: u32 = 0x7f;
|
||||
pub const AC_CLIST_LONG: u32 = 1 << 7;
|
||||
|
||||
// ---- Function Group (hda_verbs.h:161-168) ----
|
||||
pub const AC_GRP_AUDIO_FUNCTION: u8 = 0x01;
|
||||
pub const AC_GRP_MODEM_FUNCTION: u8 = 0x02;
|
||||
pub const AC_FGT_UNSOL_CAP: u32 = 1 << 8;
|
||||
pub const AC_AFG_OUT_DELAY: u32 = 0xf;
|
||||
pub const AC_AFG_IN_DELAY: u32 = 0xf << 8;
|
||||
pub const AC_AFG_BEEP_GEN: u32 = 1 << 16;
|
||||
|
||||
// ---- Stream Format (hda_verbs.h:221-235) ----
|
||||
pub const AC_FMT_CHAN_SHIFT: u8 = 0;
|
||||
pub const AC_FMT_CHAN_MASK: u32 = 0x0f;
|
||||
pub const AC_FMT_BITS_SHIFT: u8 = 4;
|
||||
pub const AC_FMT_BITS_MASK: u32 = 7 << 4;
|
||||
pub const AC_FMT_BITS_8: u32 = 0;
|
||||
pub const AC_FMT_BITS_16: u32 = 1 << 4;
|
||||
pub const AC_FMT_BITS_20: u32 = 2 << 4;
|
||||
pub const AC_FMT_BITS_24: u32 = 3 << 4;
|
||||
pub const AC_FMT_BITS_32: u32 = 4 << 4;
|
||||
pub const AC_FMT_BASE_48K: u32 = 0;
|
||||
pub const AC_FMT_BASE_44K: u32 = 1 << 14;
|
||||
@@ -294,14 +294,14 @@ pub fn acquire_port_io_rights() -> Result<()> {
|
||||
extern "C" {
|
||||
fn redox_cur_thrfd_v0() -> usize;
|
||||
}
|
||||
let kernel_fd = syscall::dup(unsafe { redox_cur_thrfd_v0() }, b"open_via_dup")?;
|
||||
let kernel_fd = libredox::call::dup(unsafe { redox_cur_thrfd_v0() }, b"open_via_dup")?;
|
||||
let res = libredox::call::call_wo(
|
||||
kernel_fd,
|
||||
&[],
|
||||
syscall::CallFlags::empty(),
|
||||
&[ProcSchemeVerb::Iopl as u64],
|
||||
);
|
||||
let _ = syscall::close(kernel_fd);
|
||||
let _ = libredox::call::close(kernel_fd);
|
||||
res?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ redox_event.workspace = true
|
||||
redox_syscall.workspace = true
|
||||
redox-scheme.workspace = true
|
||||
scheme-utils = { path = "../../../scheme-utils" }
|
||||
toml.workspace = true
|
||||
|
||||
common = { path = "../../common" }
|
||||
console-draw = { path = "../console-draw" }
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[keymap]
|
||||
name = "de"
|
||||
description = "German (QWERTZ)"
|
||||
|
||||
[keys]
|
||||
"0x0E" = "\x7F"
|
||||
"0x1C" = "\n"
|
||||
"0x47" = "\x1B[H"
|
||||
"0x48" = "\x1B[A"
|
||||
"0x49" = "\x1B[5~"
|
||||
"0x4B" = "\x1B[D"
|
||||
"0x4D" = "\x1B[C"
|
||||
"0x4F" = "\x1B[F"
|
||||
"0x50" = "\x1B[B"
|
||||
"0x51" = "\x1B[6~"
|
||||
"0x52" = "\x1B[2~"
|
||||
"0x53" = "\x1B[3~"
|
||||
@@ -0,0 +1,17 @@
|
||||
[keymap]
|
||||
name = "fr"
|
||||
description = "French (AZERTY)"
|
||||
|
||||
[keys]
|
||||
"0x0E" = "\x7F"
|
||||
"0x1C" = "\n"
|
||||
"0x47" = "\x1B[H"
|
||||
"0x48" = "\x1B[A"
|
||||
"0x49" = "\x1B[5~"
|
||||
"0x4B" = "\x1B[D"
|
||||
"0x4D" = "\x1B[C"
|
||||
"0x4F" = "\x1B[F"
|
||||
"0x50" = "\x1B[B"
|
||||
"0x51" = "\x1B[6~"
|
||||
"0x52" = "\x1B[2~"
|
||||
"0x53" = "\x1B[3~"
|
||||
@@ -0,0 +1,17 @@
|
||||
[keymap]
|
||||
name = "ru"
|
||||
description = "Russian JCUKEN (ЙЦУКЕН) — #1 non-English locale for Red Bear OS"
|
||||
|
||||
[keys]
|
||||
"0x0E" = "\x7F"
|
||||
"0x1C" = "\n"
|
||||
"0x47" = "\x1B[H"
|
||||
"0x48" = "\x1B[A"
|
||||
"0x49" = "\x1B[5~"
|
||||
"0x4B" = "\x1B[D"
|
||||
"0x4D" = "\x1B[C"
|
||||
"0x4F" = "\x1B[F"
|
||||
"0x50" = "\x1B[B"
|
||||
"0x51" = "\x1B[6~"
|
||||
"0x52" = "\x1B[2~"
|
||||
"0x53" = "\x1B[3~"
|
||||
@@ -0,0 +1,17 @@
|
||||
[keymap]
|
||||
name = "uk"
|
||||
description = "UK English (QWERTY)"
|
||||
|
||||
[keys]
|
||||
"0x0E" = "\x7F"
|
||||
"0x1C" = "\n"
|
||||
"0x47" = "\x1B[H"
|
||||
"0x48" = "\x1B[A"
|
||||
"0x49" = "\x1B[5~"
|
||||
"0x4B" = "\x1B[D"
|
||||
"0x4D" = "\x1B[C"
|
||||
"0x4F" = "\x1B[F"
|
||||
"0x50" = "\x1B[B"
|
||||
"0x51" = "\x1B[6~"
|
||||
"0x52" = "\x1B[2~"
|
||||
"0x53" = "\x1B[3~"
|
||||
@@ -0,0 +1,17 @@
|
||||
[keymap]
|
||||
name = "us"
|
||||
description = "US English (QWERTY)"
|
||||
|
||||
[keys]
|
||||
"0x0E" = "\x7F"
|
||||
"0x1C" = "\n"
|
||||
"0x47" = "\x1B[H"
|
||||
"0x48" = "\x1B[A"
|
||||
"0x49" = "\x1B[5~"
|
||||
"0x4B" = "\x1B[D"
|
||||
"0x4D" = "\x1B[C"
|
||||
"0x4F" = "\x1B[F"
|
||||
"0x50" = "\x1B[B"
|
||||
"0x51" = "\x1B[6~"
|
||||
"0x52" = "\x1B[2~"
|
||||
"0x53" = "\x1B[3~"
|
||||
@@ -33,11 +33,11 @@ impl Display {
|
||||
};
|
||||
let new_display_handle = V2GraphicsHandle::from_file(display_file).unwrap();
|
||||
|
||||
log::debug!("fbcond: Opened new display");
|
||||
log::info!("fbcond: Opened new display");
|
||||
|
||||
match V2DisplayMap::new(new_display_handle) {
|
||||
Ok(map) => {
|
||||
log::debug!(
|
||||
log::info!(
|
||||
"fbcond: Mapped new display with size {}x{}",
|
||||
map.buffer.buffer().size().0,
|
||||
map.buffer.buffer().size().1,
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
//! Configurable keymap for fbcond.
|
||||
//!
|
||||
//! Maps keyboard scancodes to terminal byte sequences. The default
|
||||
//! keymap (US QWERTY) is embedded at compile time. Alternative layouts
|
||||
//! (Russian JCUKEN, UK, German, French) are embedded and selectable.
|
||||
//!
|
||||
//! Keymap priority: English (US) is the default. Russian is the #1
|
||||
//! non-English locale throughout Red Bear OS.
|
||||
//!
|
||||
//! ## Keymap file format (TOML)
|
||||
//!
|
||||
//! ```toml
|
||||
//! [keymap]
|
||||
//! name = "us"
|
||||
//! description = "US English (QWERTY)"
|
||||
//!
|
||||
//! [keys]
|
||||
//! # Special keys by scancode
|
||||
//! "0x0E" = "\x7F" # Backspace
|
||||
//! "0x1C" = "\n" # Enter
|
||||
//! "0x47" = "\x1B[H" # Home
|
||||
//! "0x48" = "\x1B[A" # Up
|
||||
//! "0x49" = "\x1B[5~" # Page Up
|
||||
//! "0x4B" = "\x1B[D" # Left
|
||||
//! "0x4D" = "\x1B[C" # Right
|
||||
//! "0x4F" = "\x1B[F" # End
|
||||
//! "0x50" = "\x1B[B" # Down
|
||||
//! "0x51" = "\x1B[6~" # Page Down
|
||||
//! "0x52" = "\x1B[2~" # Insert
|
||||
//! "0x53" = "\x1B[3~" # Delete
|
||||
//! ```
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// The integer type used for scancodes.
|
||||
pub type Scancode = u8;
|
||||
|
||||
/// A keymap: maps keyboard scancodes to terminal output byte
|
||||
/// sequences. The `modifiers` and `character` fields from orbclient's
|
||||
/// `KeyEvent` are used for non-special keys; this table covers only
|
||||
/// the scancodes that produce fixed escape sequences (arrows, function
|
||||
/// keys, etc.).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Keymap {
|
||||
/// Human-readable name (e.g. "us", "ru").
|
||||
pub name: String,
|
||||
/// Description (e.g. "US English (QWERTY)").
|
||||
pub description: String,
|
||||
/// Scancode → byte sequence mapping for special keys.
|
||||
pub keys: HashMap<Scancode, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl Keymap {
|
||||
/// Look up the byte sequence for `scancode`, or `None` if the
|
||||
/// scancode is not a special key (should be handled by the
|
||||
/// character-translation fallback).
|
||||
pub fn lookup(&self, scancode: Scancode) -> Option<&[u8]> {
|
||||
self.keys.get(&scancode).map(|v| v.as_slice())
|
||||
}
|
||||
|
||||
/// Build from TOML text.
|
||||
fn from_toml(toml_text: &str) -> Result<Self, String> {
|
||||
let val: toml::Value = toml::from_str(toml_text)
|
||||
.map_err(|e| format!("keymap parse error: {e}"))?;
|
||||
let km = val.get("keymap").ok_or("missing [keymap] section")?;
|
||||
let name = km.get("name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let description = km.get("description")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let keys_table = val.get("keys")
|
||||
.and_then(|v| v.as_table())
|
||||
.ok_or("missing [keys] section")?;
|
||||
let mut keys: HashMap<Scancode, Vec<u8>> = HashMap::new();
|
||||
for (sc_str, seq_val) in keys_table {
|
||||
let sc = parse_scancode(sc_str)?;
|
||||
let seq = parse_byte_seq(seq_val.as_str().unwrap_or(""))?;
|
||||
keys.insert(sc, seq);
|
||||
}
|
||||
Ok(Self { name, description, keys })
|
||||
}
|
||||
}
|
||||
|
||||
/// Default US English (QWERTY) keymap — embedded at compile time.
|
||||
const US_TOML: &str = include_str!("../keymaps/us.toml");
|
||||
|
||||
/// Russian JCUKEN keymap — #1 non-English locale.
|
||||
const RU_TOML: &str = include_str!("../keymaps/ru.toml");
|
||||
|
||||
/// UK English (QWERTY) keymap.
|
||||
const UK_TOML: &str = include_str!("../keymaps/uk.toml");
|
||||
|
||||
/// German (QWERTZ) keymap.
|
||||
const DE_TOML: &str = include_str!("../keymaps/de.toml");
|
||||
|
||||
/// French (AZERTY) keymap.
|
||||
const FR_TOML: &str = include_str!("../keymaps/fr.toml");
|
||||
|
||||
impl Keymap {
|
||||
/// Load the named keymap. Recognized names: `us`, `ru`, `uk`,
|
||||
/// `de`, `fr`. Falls back to US on unknown names.
|
||||
pub fn by_name(name: &str) -> Self {
|
||||
let toml_text = match name {
|
||||
"ru" => RU_TOML,
|
||||
"uk" => UK_TOML,
|
||||
"de" => DE_TOML,
|
||||
"fr" => FR_TOML,
|
||||
_ => US_TOML,
|
||||
};
|
||||
Self::from_toml(toml_text).unwrap_or_else(|e| {
|
||||
log::warn!("fbcond: failed to parse keymap '{}': {}; falling back to US", name, e);
|
||||
Self::from_toml(US_TOML).expect("US keymap must parse")
|
||||
})
|
||||
}
|
||||
|
||||
/// Convenience: US default.
|
||||
pub fn us() -> Self {
|
||||
Self::by_name("us")
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_scancode(s: &str) -> Result<Scancode, String> {
|
||||
let s = s.trim();
|
||||
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
|
||||
Scancode::from_str_radix(hex, 16)
|
||||
.map_err(|e| format!("invalid scancode '{}': {e}", s))
|
||||
} else {
|
||||
s.parse::<Scancode>()
|
||||
.map_err(|e| format!("invalid scancode '{}': {e}", s))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_byte_seq(s: &str) -> Result<Vec<u8>, String> {
|
||||
let mut out = Vec::new();
|
||||
let mut chars = s.chars().peekable();
|
||||
while let Some(c) = chars.next() {
|
||||
if c == '\\' {
|
||||
match chars.next() {
|
||||
Some('x') => {
|
||||
let h1 = chars.next().ok_or("truncated \\x escape")?;
|
||||
let h2 = chars.next().ok_or("truncated \\x escape")?;
|
||||
let hex = format!("{h1}{h2}");
|
||||
let byte = u8::from_str_radix(&hex, 16)
|
||||
.map_err(|e| format!("invalid \\x escape: {e}"))?;
|
||||
out.push(byte);
|
||||
}
|
||||
Some('n') => out.push(b'\n'),
|
||||
Some('r') => out.push(b'\r'),
|
||||
Some('t') => out.push(b'\t'),
|
||||
Some('0') => out.push(b'\0'),
|
||||
Some('\\') => out.push(b'\\'),
|
||||
Some(other) => {
|
||||
out.push(b'\\');
|
||||
out.push(other as u8);
|
||||
}
|
||||
None => out.push(b'\\'),
|
||||
}
|
||||
} else {
|
||||
out.push(c as u8);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn us_keymap_parses() {
|
||||
let km = Keymap::us();
|
||||
assert_eq!(km.name, "us");
|
||||
assert!(km.lookup(0x0E).is_some()); // Backspace
|
||||
assert!(km.lookup(0x1C).is_some()); // Enter
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ru_keymap_parses() {
|
||||
let km = Keymap::by_name("ru");
|
||||
assert_eq!(km.name, "ru");
|
||||
assert!(km.lookup(0x0E).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_name_falls_back_to_us() {
|
||||
let km = Keymap::by_name("zz");
|
||||
assert_eq!(km.name, "us");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_scancode_hex_and_decimal() {
|
||||
assert_eq!(parse_scancode("0x0E").unwrap(), 14);
|
||||
assert_eq!(parse_scancode("0x1C").unwrap(), 28);
|
||||
assert_eq!(parse_scancode("14").unwrap(), 14);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_byte_seq_escape_and_literal() {
|
||||
assert_eq!(parse_byte_seq(r"\x7F").unwrap(), b"\x7F");
|
||||
assert_eq!(parse_byte_seq(r"\n").unwrap(), b"\n");
|
||||
assert_eq!(parse_byte_seq(r"\x1B[A").unwrap(), b"\x1B[A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_missing_returns_none() {
|
||||
let km = Keymap::us();
|
||||
assert!(km.lookup(0xFF).is_none());
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use syscall::{EOPNOTSUPP, EVENT_READ};
|
||||
use crate::scheme::{FbconScheme, Handle, VtIndex};
|
||||
|
||||
mod display;
|
||||
mod keymap;
|
||||
mod scheme;
|
||||
mod text;
|
||||
|
||||
|
||||
@@ -4,12 +4,16 @@ use orbclient::{Event, EventOption};
|
||||
use syscall::error::*;
|
||||
|
||||
use crate::display::Display;
|
||||
use crate::keymap::Keymap;
|
||||
|
||||
pub struct TextScreen {
|
||||
pub display: Display,
|
||||
inner: console_draw::TextScreen,
|
||||
ctrl: bool,
|
||||
input: VecDeque<u8>,
|
||||
pending_writes: Vec<Vec<u8>>,
|
||||
/// Active keyboard layout. Defaults to US QWERTY.
|
||||
pub keymap: Keymap,
|
||||
}
|
||||
|
||||
impl TextScreen {
|
||||
@@ -19,12 +23,17 @@ impl TextScreen {
|
||||
inner: console_draw::TextScreen::new(),
|
||||
ctrl: false,
|
||||
input: VecDeque::new(),
|
||||
pending_writes: Vec::new(),
|
||||
keymap: Keymap::us(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_handoff(&mut self) {
|
||||
log::info!("fbcond: Performing handoff");
|
||||
self.display.reopen_for_handoff();
|
||||
if self.display.map.is_some() {
|
||||
self.flush_pending_writes();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input(&mut self, event: &Event) {
|
||||
@@ -35,62 +44,19 @@ impl TextScreen {
|
||||
if key_event.scancode == 0x1D {
|
||||
self.ctrl = key_event.pressed;
|
||||
} else if key_event.pressed {
|
||||
match key_event.scancode {
|
||||
0x0E => {
|
||||
// Backspace
|
||||
buf.extend_from_slice(b"\x7F");
|
||||
}
|
||||
0x47 => {
|
||||
// Home
|
||||
buf.extend_from_slice(b"\x1B[H");
|
||||
}
|
||||
0x48 => {
|
||||
// Up
|
||||
buf.extend_from_slice(b"\x1B[A");
|
||||
}
|
||||
0x49 => {
|
||||
// Page up
|
||||
buf.extend_from_slice(b"\x1B[5~");
|
||||
}
|
||||
0x4B => {
|
||||
// Left
|
||||
buf.extend_from_slice(b"\x1B[D");
|
||||
}
|
||||
0x4D => {
|
||||
// Right
|
||||
buf.extend_from_slice(b"\x1B[C");
|
||||
}
|
||||
0x4F => {
|
||||
// End
|
||||
buf.extend_from_slice(b"\x1B[F");
|
||||
}
|
||||
0x50 => {
|
||||
// Down
|
||||
buf.extend_from_slice(b"\x1B[B");
|
||||
}
|
||||
0x51 => {
|
||||
// Page down
|
||||
buf.extend_from_slice(b"\x1B[6~");
|
||||
}
|
||||
0x52 => {
|
||||
// Insert
|
||||
buf.extend_from_slice(b"\x1B[2~");
|
||||
}
|
||||
0x53 => {
|
||||
// Delete
|
||||
buf.extend_from_slice(b"\x1B[3~");
|
||||
}
|
||||
_ => {
|
||||
let c = match key_event.character {
|
||||
c @ 'A'..='Z' if self.ctrl => ((c as u8 - b'A') + b'\x01') as char,
|
||||
c @ 'a'..='z' if self.ctrl => ((c as u8 - b'a') + b'\x01') as char,
|
||||
c => c,
|
||||
};
|
||||
let sc = key_event.scancode as u8;
|
||||
if let Some(seq) = self.keymap.lookup(sc) {
|
||||
buf.extend_from_slice(seq);
|
||||
} else {
|
||||
let c = match key_event.character {
|
||||
c @ 'A'..='Z' if self.ctrl => ((c as u8 - b'A') + b'\x01') as char,
|
||||
c @ 'a'..='z' if self.ctrl => ((c as u8 - b'a') + b'\x01') as char,
|
||||
c => c,
|
||||
};
|
||||
|
||||
if c != '\0' {
|
||||
let mut b = [0; 4];
|
||||
buf.extend_from_slice(c.encode_utf8(&mut b).as_bytes());
|
||||
}
|
||||
if c != '\0' {
|
||||
let mut b = [0; 4];
|
||||
buf.extend_from_slice(c.encode_utf8(&mut b).as_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,8 +93,42 @@ impl TextScreen {
|
||||
let damage = self.inner.write(map, buf, &mut self.input);
|
||||
|
||||
self.display.sync_rect(damage);
|
||||
} else {
|
||||
log::warn!(
|
||||
"fbcond: TextScreen::write() called while display map is None; \
|
||||
buffering {} bytes ({} pending chunks). Will flush after handoff.",
|
||||
buf.len(),
|
||||
self.pending_writes.len() + 1,
|
||||
);
|
||||
self.pending_writes.push(buf.to_vec());
|
||||
}
|
||||
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
pub fn flush_pending_writes(&mut self) {
|
||||
if self.pending_writes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let count = self.pending_writes.len();
|
||||
log::info!(
|
||||
"fbcond: Flushing {} pending write chunks after display handoff",
|
||||
count,
|
||||
);
|
||||
|
||||
if let Some(map) = &mut self.display.map {
|
||||
for pending in self.pending_writes.drain(..) {
|
||||
Display::handle_resize(map, &mut self.inner);
|
||||
let damage = self.inner.write(map, &pending, &mut self.input);
|
||||
map.dirty_fb(damage).unwrap();
|
||||
}
|
||||
} else {
|
||||
log::error!(
|
||||
"fbcond: flush_pending_writes called but display map is still None; \
|
||||
keeping {} buffered chunks",
|
||||
self.pending_writes.len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,43 @@ fn send_key_event(display: &mut ProducerHandle, usage_page: u16, usage: u16, pre
|
||||
return;
|
||||
}
|
||||
},
|
||||
// Consumer page (0x0C): media keys, application launch keys.
|
||||
// Linux 7.1 hid-input.c maps these to KEY_VOLUME*, KEY_MUTE, etc.
|
||||
// OrbKeyEvent.scancode is u8 so we map common consumer usages to
|
||||
// the 0xF0-0xFF block reserved for vendor keys.
|
||||
0x0C => {
|
||||
let sc = match usage {
|
||||
0x00E2 => 0xF0u8, // Mute
|
||||
0x00E9 => 0xF1u8, // Volume Up
|
||||
0x00EA => 0xF2u8, // Volume Down
|
||||
0x00B0 => 0xF3u8, // Play
|
||||
0x00B1 => 0xF4u8, // Pause
|
||||
0x00B3 => 0xF5u8, // Next Track
|
||||
0x00B4 => 0xF6u8, // Previous Track
|
||||
0x00B5 => 0xF7u8, // Stop
|
||||
0x00CD => 0xF3u8, // Play/Pause (same as Play)
|
||||
0x0183 => 0xF8u8, // AL Config
|
||||
0x018A => 0xF9u8, // Email
|
||||
0x0192 => 0xFAu8, // Calculator
|
||||
0x0194 => 0xFBu8, // My Computer
|
||||
0x0221 => 0xFCu8, // Search
|
||||
0x0223 => 0xFDu8, // Home
|
||||
_ => {
|
||||
log::warn!("unmapped consumer usage {:#06X}", usage);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let key_event = OrbKeyEvent {
|
||||
character: '\0',
|
||||
scancode: sc,
|
||||
pressed,
|
||||
};
|
||||
match display.write_event(key_event.to_event()) {
|
||||
Ok(_) => (),
|
||||
Err(err) => log::warn!("failed to send consumer key event: {}", err),
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
log::warn!("unknown usage_page {:#x}", usage_page);
|
||||
return;
|
||||
@@ -287,6 +324,11 @@ fn main() -> Result<()> {
|
||||
let mut right_shift = false;
|
||||
let mut last_mouse_pos = (0, 0);
|
||||
let mut last_buttons = [false, false, false];
|
||||
|
||||
// Keyboard LED state (HID boot keyboard output report, 1 byte)
|
||||
let mut led_state: u8 = 0;
|
||||
let mut last_led_state: u8 = 0xFF; // force first SET_REPORT
|
||||
|
||||
loop {
|
||||
//TODO: get frequency from device
|
||||
//TODO: use sleeps when accuracy is better: thread::sleep(time::Duration::from_millis(10));
|
||||
@@ -296,21 +338,27 @@ fn main() -> Result<()> {
|
||||
}
|
||||
|
||||
if let Some(endpoint) = &mut endpoint_opt {
|
||||
// interrupt transfer
|
||||
endpoint
|
||||
.transfer_read(&mut report_buffer)
|
||||
.context("failed to get report")?;
|
||||
match endpoint.transfer_read(&mut report_buffer) {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
log::warn!("usbhidd: interrupt transfer failed: {} — retrying", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// control transfer
|
||||
reqs::get_report(
|
||||
match reqs::get_report(
|
||||
&handle,
|
||||
report_ty,
|
||||
report_id,
|
||||
//TODO: should this be an index into interface_descs?
|
||||
interface_num as u16,
|
||||
&mut report_buffer,
|
||||
)
|
||||
.context("failed to get report")?;
|
||||
) {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
log::warn!("usbhidd: control transfer failed: {} — retrying", e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut mouse_pos = last_mouse_pos;
|
||||
@@ -318,6 +366,10 @@ fn main() -> Result<()> {
|
||||
let mut mouse_dy = 0i32;
|
||||
let mut scroll_y = 0i32;
|
||||
let mut buttons = last_buttons;
|
||||
// Gamepad state (Linux 7.1 hid-input.c: BTN_GAMEPAD + ABS_X/Y/Z/Rx/Ry/Rz)
|
||||
let mut gamepad_axes: [i32; 6] = [0i32; 6];
|
||||
let mut gamepad_buttons: u32 = 0;
|
||||
let mut hat_switch: i8 = -1;
|
||||
for event in handler
|
||||
.handle(&report_buffer)
|
||||
.expect("failed to parse report")
|
||||
@@ -329,12 +381,14 @@ fn main() -> Result<()> {
|
||||
mouse_dx += event.value as i32;
|
||||
} else {
|
||||
mouse_pos.0 = event.value as i32;
|
||||
gamepad_axes[0] = event.value as i32;
|
||||
}
|
||||
} else if event.usage == GenericDesktopUsage::Y as u16 {
|
||||
if event.relative {
|
||||
mouse_dy += event.value as i32;
|
||||
} else {
|
||||
mouse_pos.1 = event.value as i32;
|
||||
gamepad_axes[1] = event.value as i32;
|
||||
}
|
||||
} else if event.usage == GenericDesktopUsage::Wheel as u16 {
|
||||
//TODO: what is X scroll?
|
||||
@@ -343,6 +397,24 @@ fn main() -> Result<()> {
|
||||
} else {
|
||||
log::warn!("absolute mouse wheel not supported");
|
||||
}
|
||||
// Gamepad axes per Linux 7.1 hid-input.c map_abs():
|
||||
// 0x30 = X (left stick X) — already handled above for mouse
|
||||
// 0x31 = Y (left stick Y)
|
||||
// 0x32 = Z (left trigger or secondary)
|
||||
// 0x33 = Rx (right stick X)
|
||||
// 0x34 = Ry (right stick Y)
|
||||
// 0x35 = Rz (right trigger)
|
||||
// 0x39 = Hat Switch (dpad, Linux maps to ABS_HAT0X)
|
||||
} else if event.usage == 0x32 {
|
||||
gamepad_axes[2] = event.value as i32;
|
||||
} else if event.usage == 0x33 {
|
||||
gamepad_axes[3] = event.value as i32;
|
||||
} else if event.usage == 0x34 {
|
||||
gamepad_axes[4] = event.value as i32;
|
||||
} else if event.usage == 0x35 {
|
||||
gamepad_axes[5] = event.value as i32;
|
||||
} else if event.usage == 0x39 {
|
||||
hat_switch = event.value as i8;
|
||||
} else {
|
||||
log::info!(
|
||||
"unsupported generic desktop usage 0x{:X}:0x{:X} value {}",
|
||||
@@ -362,18 +434,42 @@ fn main() -> Result<()> {
|
||||
} else if event.usage == 0xE5 {
|
||||
right_shift = pressed;
|
||||
}
|
||||
// Track LED state from lock-key presses (Linux 7.1 hid-input.c)
|
||||
if pressed {
|
||||
match event.usage {
|
||||
0x39 => led_state ^= 0x02, // Caps Lock
|
||||
0x47 => led_state ^= 0x04, // Scroll Lock
|
||||
0x53 => led_state ^= 0x01, // Num Lock
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
send_key_event(&mut display, event.usage_page, event.usage, pressed);
|
||||
} else if event.usage_page == UsagePage::Button as u16 {
|
||||
if event.usage > 0 && event.usage as usize <= buttons.len() {
|
||||
buttons[event.usage as usize - 1] = event.value != 0;
|
||||
if event.usage > 0 && event.usage <= 32 {
|
||||
let bit = 1u32 << (event.usage - 1);
|
||||
if event.value != 0 {
|
||||
gamepad_buttons |= bit;
|
||||
} else {
|
||||
gamepad_buttons &= !bit;
|
||||
}
|
||||
// Also track first 3 as mouse buttons for backward compat
|
||||
if event.usage <= 3 {
|
||||
buttons[event.usage as usize - 1] = event.value != 0;
|
||||
}
|
||||
} else {
|
||||
log::info!(
|
||||
"unsupported buttons usage 0x{:X}:0x{:X} value {}",
|
||||
"unsupported button usage 0x{:X}:0x{:X} value {}",
|
||||
event.usage_page,
|
||||
event.usage,
|
||||
event.value
|
||||
);
|
||||
}
|
||||
} else if event.usage_page == 0x0C {
|
||||
// Consumer page: media keys, application launchers.
|
||||
// Linux 7.1 hid-input.c: maps to KEY_VOLUME*, KEY_MUTE, etc.
|
||||
// Fired as key events with scancode = 0xC0000 | usage.
|
||||
let pressed = event.value != 0;
|
||||
send_key_event(&mut display, 0x0C, event.usage, pressed);
|
||||
} else if event.usage_page >= 0xFF00 {
|
||||
// Ignore vendor defined event
|
||||
} else {
|
||||
@@ -452,6 +548,26 @@ fn main() -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard LED sync: send HID output report when LED state changes.
|
||||
// Linux 7.1 hid-input.c: hidinput_output_event() → hid_hw_output_report()
|
||||
// → SET_REPORT (Output) via control transfer.
|
||||
if led_state != last_led_state {
|
||||
last_led_state = led_state;
|
||||
let led_bytes = [led_state];
|
||||
if let Err(e) = handle.device_request(
|
||||
xhcid_interface::PortReqTy::Class,
|
||||
xhcid_interface::PortReqRecipient::Interface,
|
||||
0x09, // SET_REPORT
|
||||
(0x02u16) << 8, // Output report type, report ID 0
|
||||
interface_num as u16,
|
||||
xhcid_interface::DeviceReqData::Out(unsafe {
|
||||
std::slice::from_raw_parts(led_bytes.as_ptr(), 1)
|
||||
}),
|
||||
) {
|
||||
log::warn!("usbhidd: SET_REPORT(LED) failed: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// log::trace!("took {}ms", timer.elapsed().as_millis())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::str::FromStr;
|
||||
|
||||
mod keymaps {
|
||||
pub static US: [(u8, [char; 2]); 53] = [
|
||||
(orbclient::K_ESC, ['\x1B', '\x1B']),
|
||||
(orbclient::K_ESC, ['\0', '\0']),
|
||||
(orbclient::K_1, ['1', '!']),
|
||||
(orbclient::K_2, ['2', '@']),
|
||||
(orbclient::K_3, ['3', '#']),
|
||||
@@ -17,7 +17,7 @@ mod keymaps {
|
||||
(orbclient::K_0, ['0', ')']),
|
||||
(orbclient::K_MINUS, ['-', '_']),
|
||||
(orbclient::K_EQUALS, ['=', '+']),
|
||||
(orbclient::K_BKSP, ['\x7F', '\x7F']),
|
||||
(orbclient::K_BKSP, ['\0', '\0']),
|
||||
(orbclient::K_TAB, ['\t', '\t']),
|
||||
(orbclient::K_Q, ['q', 'Q']),
|
||||
(orbclient::K_W, ['w', 'W']),
|
||||
@@ -31,7 +31,7 @@ mod keymaps {
|
||||
(orbclient::K_P, ['p', 'P']),
|
||||
(orbclient::K_BRACE_OPEN, ['[', '{']),
|
||||
(orbclient::K_BRACE_CLOSE, [']', '}']),
|
||||
(orbclient::K_ENTER, ['\n', '\n']),
|
||||
(orbclient::K_ENTER, ['\0', '\0']),
|
||||
(orbclient::K_CTRL, ['\0', '\0']),
|
||||
(orbclient::K_A, ['a', 'A']),
|
||||
(orbclient::K_S, ['s', 'S']),
|
||||
@@ -60,7 +60,7 @@ mod keymaps {
|
||||
];
|
||||
|
||||
pub static GB: [(u8, [char; 2]); 54] = [
|
||||
(orbclient::K_ESC, ['\x1B', '\x1B']),
|
||||
(orbclient::K_ESC, ['\0', '\0']),
|
||||
(orbclient::K_1, ['1', '!']),
|
||||
(orbclient::K_2, ['2', '"']),
|
||||
(orbclient::K_3, ['3', '£']),
|
||||
@@ -73,7 +73,7 @@ mod keymaps {
|
||||
(orbclient::K_0, ['0', ')']),
|
||||
(orbclient::K_MINUS, ['-', '_']),
|
||||
(orbclient::K_EQUALS, ['=', '+']),
|
||||
(orbclient::K_BKSP, ['\x7F', '\x7F']),
|
||||
(orbclient::K_BKSP, ['\0', '\0']),
|
||||
(orbclient::K_TAB, ['\t', '\t']),
|
||||
(orbclient::K_Q, ['q', 'Q']),
|
||||
(orbclient::K_W, ['w', 'W']),
|
||||
@@ -87,7 +87,7 @@ mod keymaps {
|
||||
(orbclient::K_P, ['p', 'P']),
|
||||
(orbclient::K_BRACE_OPEN, ['[', '{']),
|
||||
(orbclient::K_BRACE_CLOSE, [']', '}']),
|
||||
(orbclient::K_ENTER, ['\n', '\n']),
|
||||
(orbclient::K_ENTER, ['\0', '\0']),
|
||||
(orbclient::K_CTRL, ['\0', '\0']),
|
||||
(orbclient::K_A, ['a', 'A']),
|
||||
(orbclient::K_S, ['s', 'S']),
|
||||
@@ -118,7 +118,7 @@ mod keymaps {
|
||||
];
|
||||
|
||||
pub static DVORAK: [(u8, [char; 2]); 53] = [
|
||||
(orbclient::K_ESC, ['\x1B', '\x1B']),
|
||||
(orbclient::K_ESC, ['\0', '\0']),
|
||||
(orbclient::K_1, ['1', '!']),
|
||||
(orbclient::K_2, ['2', '@']),
|
||||
(orbclient::K_3, ['3', '#']),
|
||||
@@ -131,7 +131,7 @@ mod keymaps {
|
||||
(orbclient::K_0, ['0', ')']),
|
||||
(orbclient::K_MINUS, ['[', '{']),
|
||||
(orbclient::K_EQUALS, [']', '}']),
|
||||
(orbclient::K_BKSP, ['\x7F', '\x7F']),
|
||||
(orbclient::K_BKSP, ['\0', '\0']),
|
||||
(orbclient::K_TAB, ['\t', '\t']),
|
||||
(orbclient::K_Q, ['\'', '"']),
|
||||
(orbclient::K_W, [',', '<']),
|
||||
@@ -145,7 +145,7 @@ mod keymaps {
|
||||
(orbclient::K_P, ['l', 'L']),
|
||||
(orbclient::K_BRACE_OPEN, ['/', '?']),
|
||||
(orbclient::K_BRACE_CLOSE, ['=', '+']),
|
||||
(orbclient::K_ENTER, ['\n', '\n']),
|
||||
(orbclient::K_ENTER, ['\0', '\0']),
|
||||
(orbclient::K_CTRL, ['\0', '\0']),
|
||||
(orbclient::K_A, ['a', 'A']),
|
||||
(orbclient::K_S, ['o', 'O']),
|
||||
@@ -174,7 +174,7 @@ mod keymaps {
|
||||
];
|
||||
|
||||
pub static AZERTY: [(u8, [char; 2]); 53] = [
|
||||
(orbclient::K_ESC, ['\x1B', '\x1B']),
|
||||
(orbclient::K_ESC, ['\0', '\0']),
|
||||
(orbclient::K_1, ['&', '1']),
|
||||
(orbclient::K_2, ['é', '2']),
|
||||
(orbclient::K_3, ['"', '3']),
|
||||
@@ -187,7 +187,7 @@ mod keymaps {
|
||||
(orbclient::K_0, ['à', '0']),
|
||||
(orbclient::K_MINUS, [')', '°']),
|
||||
(orbclient::K_EQUALS, ['=', '+']),
|
||||
(orbclient::K_BKSP, ['\x7F', '\x7F']),
|
||||
(orbclient::K_BKSP, ['\0', '\0']),
|
||||
(orbclient::K_TAB, ['\t', '\t']),
|
||||
(orbclient::K_Q, ['a', 'A']),
|
||||
(orbclient::K_W, ['z', 'Z']),
|
||||
@@ -201,7 +201,7 @@ mod keymaps {
|
||||
(orbclient::K_P, ['p', 'P']),
|
||||
(orbclient::K_BRACE_OPEN, ['^', '¨']),
|
||||
(orbclient::K_BRACE_CLOSE, ['$', '£']),
|
||||
(orbclient::K_ENTER, ['\n', '\n']),
|
||||
(orbclient::K_ENTER, ['\0', '\0']),
|
||||
(orbclient::K_CTRL, ['\0', '\0']),
|
||||
(orbclient::K_A, ['q', 'Q']),
|
||||
(orbclient::K_S, ['s', 'S']),
|
||||
@@ -230,7 +230,7 @@ mod keymaps {
|
||||
];
|
||||
|
||||
pub static BEPO: [(u8, [char; 2]); 53] = [
|
||||
(orbclient::K_ESC, ['\x1B', '\x1B']),
|
||||
(orbclient::K_ESC, ['\0', '\0']),
|
||||
(orbclient::K_1, ['"', '1']),
|
||||
(orbclient::K_2, ['«', '2']),
|
||||
(orbclient::K_3, ['»', '3']),
|
||||
@@ -243,7 +243,7 @@ mod keymaps {
|
||||
(orbclient::K_0, ['*', '0']),
|
||||
(orbclient::K_MINUS, ['=', '°']),
|
||||
(orbclient::K_EQUALS, ['%', '`']),
|
||||
(orbclient::K_BKSP, ['\x7F', '\x7F']),
|
||||
(orbclient::K_BKSP, ['\0', '\0']),
|
||||
(orbclient::K_TAB, ['\t', '\t']),
|
||||
(orbclient::K_Q, ['b', 'B']),
|
||||
(orbclient::K_W, ['é', 'É']),
|
||||
@@ -257,7 +257,7 @@ mod keymaps {
|
||||
(orbclient::K_P, ['j', 'J']),
|
||||
(orbclient::K_BRACE_OPEN, ['z', 'Z']),
|
||||
(orbclient::K_BRACE_CLOSE, ['w', 'W']),
|
||||
(orbclient::K_ENTER, ['\n', '\n']),
|
||||
(orbclient::K_ENTER, ['\0', '\0']),
|
||||
(orbclient::K_CTRL, ['\0', '\0']),
|
||||
(orbclient::K_A, ['a', 'A']),
|
||||
(orbclient::K_S, ['u', 'U']),
|
||||
@@ -286,7 +286,7 @@ mod keymaps {
|
||||
];
|
||||
|
||||
pub static IT: [(u8, [char; 2]); 53] = [
|
||||
(orbclient::K_ESC, ['\x1B', '\x1B']),
|
||||
(orbclient::K_ESC, ['\0', '\0']),
|
||||
(orbclient::K_1, ['1', '!']),
|
||||
(orbclient::K_2, ['2', '"']),
|
||||
(orbclient::K_3, ['3', '£']),
|
||||
@@ -299,7 +299,7 @@ mod keymaps {
|
||||
(orbclient::K_0, ['0', '=']),
|
||||
(orbclient::K_MINUS, ['?', '\'']),
|
||||
(orbclient::K_EQUALS, ['ì', '^']),
|
||||
(orbclient::K_BKSP, ['\x7F', '\x7F']),
|
||||
(orbclient::K_BKSP, ['\0', '\0']),
|
||||
(orbclient::K_TAB, ['\t', '\t']),
|
||||
(orbclient::K_Q, ['q', 'Q']),
|
||||
(orbclient::K_W, ['w', 'W']),
|
||||
@@ -313,7 +313,7 @@ mod keymaps {
|
||||
(orbclient::K_P, ['p', 'P']),
|
||||
(orbclient::K_BRACE_OPEN, ['è', 'é']),
|
||||
(orbclient::K_BRACE_CLOSE, ['+', '*']),
|
||||
(orbclient::K_ENTER, ['\n', '\n']),
|
||||
(orbclient::K_ENTER, ['\0', '\0']),
|
||||
(orbclient::K_CTRL, ['\x20', '\x20']),
|
||||
(orbclient::K_A, ['a', 'A']),
|
||||
(orbclient::K_S, ['s', 'S']),
|
||||
@@ -340,6 +340,66 @@ mod keymaps {
|
||||
(orbclient::K_SLASH, ['-', '_']),
|
||||
(orbclient::K_SPACE, [' ', ' ']),
|
||||
];
|
||||
|
||||
// Russian (ЙЦУКЕН) — standard JCUKEN layout (Cyrillic).
|
||||
// Reference: Linux 7.x `drivers/input/keyboard/cypress_bluetooth.c` and
|
||||
// `drivers/tty/vt/keymap.c` (Russian transliteration tables).
|
||||
// Scancodes follow PS/2 set 1 (matching K_* constants).
|
||||
pub static RU: [(u8, [char; 2]); 53] = [
|
||||
(orbclient::K_ESC, ['\0', '\0']),
|
||||
(orbclient::K_1, ['1', '!']),
|
||||
(orbclient::K_2, ['2', '"']),
|
||||
(orbclient::K_3, ['3', '№']),
|
||||
(orbclient::K_4, ['4', ';']),
|
||||
(orbclient::K_5, ['5', '%']),
|
||||
(orbclient::K_6, ['6', ':']),
|
||||
(orbclient::K_7, ['7', '?']),
|
||||
(orbclient::K_8, ['8', '*']),
|
||||
(orbclient::K_9, ['9', '(']),
|
||||
(orbclient::K_0, ['0', ')']),
|
||||
(orbclient::K_MINUS, ['-', '_']),
|
||||
(orbclient::K_EQUALS, ['=', '+']),
|
||||
(orbclient::K_BKSP, ['\0', '\0']),
|
||||
(orbclient::K_TAB, ['\0', '\0']),
|
||||
(orbclient::K_Q, ['й', 'Й']),
|
||||
(orbclient::K_W, ['ц', 'Ц']),
|
||||
(orbclient::K_E, ['у', 'У']),
|
||||
(orbclient::K_R, ['к', 'К']),
|
||||
(orbclient::K_T, ['е', 'Е']),
|
||||
(orbclient::K_Y, ['н', 'Н']),
|
||||
(orbclient::K_U, ['г', 'Г']),
|
||||
(orbclient::K_I, ['ш', 'Ш']),
|
||||
(orbclient::K_O, ['щ', 'Щ']),
|
||||
(orbclient::K_P, ['з', 'З']),
|
||||
(orbclient::K_BRACE_OPEN, ['х', 'Х']),
|
||||
(orbclient::K_BRACE_CLOSE, ['ъ', 'Ъ']),
|
||||
(orbclient::K_ENTER, ['\0', '\0']),
|
||||
(orbclient::K_CTRL, ['\0', '\0']),
|
||||
(orbclient::K_A, ['ф', 'Ф']),
|
||||
(orbclient::K_S, ['ы', 'Ы']),
|
||||
(orbclient::K_D, ['в', 'В']),
|
||||
(orbclient::K_F, ['а', 'А']),
|
||||
(orbclient::K_G, ['п', 'П']),
|
||||
(orbclient::K_H, ['р', 'Р']),
|
||||
(orbclient::K_J, ['о', 'О']),
|
||||
(orbclient::K_K, ['л', 'Л']),
|
||||
(orbclient::K_L, ['д', 'Д']),
|
||||
(orbclient::K_SEMICOLON, ['ж', 'Ж']),
|
||||
(orbclient::K_QUOTE, ['э', 'Э']),
|
||||
(orbclient::K_TICK, ['ё', 'Ё']),
|
||||
(orbclient::K_BACKSLASH, ['\\', '/']),
|
||||
(orbclient::K_Z, ['я', 'Я']),
|
||||
(orbclient::K_X, ['ч', 'Ч']),
|
||||
(orbclient::K_C, ['с', 'С']),
|
||||
(orbclient::K_V, ['м', 'М']),
|
||||
(orbclient::K_B, ['и', 'И']),
|
||||
(orbclient::K_N, ['т', 'Т']),
|
||||
(orbclient::K_M, ['ь', 'Ь']),
|
||||
(orbclient::K_COMMA, ['б', 'Б']),
|
||||
(orbclient::K_PERIOD, ['ю', 'Ю']),
|
||||
(orbclient::K_SLASH, ['.', ',']),
|
||||
(orbclient::K_SPACE, [' ', ' ']),
|
||||
];
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
@@ -351,11 +411,12 @@ pub enum KeymapKind {
|
||||
Azerty,
|
||||
Bepo,
|
||||
IT,
|
||||
RU,
|
||||
}
|
||||
|
||||
impl From<usize> for KeymapKind {
|
||||
fn from(value: usize) -> Self {
|
||||
if value > (KeymapKind::IT as usize) {
|
||||
if value > (KeymapKind::RU as usize) {
|
||||
KeymapKind::US
|
||||
} else {
|
||||
// SAFETY: Checked above
|
||||
@@ -379,6 +440,7 @@ impl FromStr for KeymapKind {
|
||||
"azerty" => KeymapKind::Azerty,
|
||||
"bepo" => KeymapKind::Bepo,
|
||||
"it" => KeymapKind::IT,
|
||||
"ru" => KeymapKind::RU,
|
||||
&_ => return Err(ParseKeymapError(())),
|
||||
};
|
||||
|
||||
@@ -395,6 +457,7 @@ impl Display for KeymapKind {
|
||||
KeymapKind::Azerty => "azerty",
|
||||
KeymapKind::Bepo => "bepo",
|
||||
KeymapKind::IT => "it",
|
||||
KeymapKind::RU => "ru",
|
||||
};
|
||||
f.write_str(s)
|
||||
}
|
||||
@@ -414,6 +477,7 @@ impl KeymapData {
|
||||
KeymapKind::Azerty => HashMap::from(keymaps::AZERTY),
|
||||
KeymapKind::Bepo => HashMap::from(keymaps::BEPO),
|
||||
KeymapKind::IT => HashMap::from(keymaps::IT),
|
||||
KeymapKind::RU => HashMap::from(keymaps::RU),
|
||||
};
|
||||
|
||||
Self { keymap_hash, kind }
|
||||
|
||||
@@ -205,7 +205,9 @@ impl ProducerHandle {
|
||||
}
|
||||
|
||||
pub fn write_event(&mut self, event: orbclient::Event) -> io::Result<()> {
|
||||
self.0.write(&event)?;
|
||||
let written = self.0.write(&event)?;
|
||||
assert_eq!(written, std::mem::size_of::<orbclient::Event>() as usize,
|
||||
"write_event: short write ({} != {})", written, std::mem::size_of::<orbclient::Event>());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
libredox.workspace = true
|
||||
log.workspace = true
|
||||
pico-args.workspace = true
|
||||
redox_syscall.workspace = true
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
use std::fs;
|
||||
use std::process::Command;
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
use pcid_interface::config::Config;
|
||||
use pcid_interface::PciFunctionHandle;
|
||||
|
||||
fn wait_for_pci_scheme() -> Result<()> {
|
||||
let mut last_err = None;
|
||||
for i in 0..200 {
|
||||
match fs::read_dir("/scheme/pci") {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(err) => {
|
||||
last_err = Some(err);
|
||||
if i == 0 {
|
||||
log::debug!("pcid-spawner: waiting for /scheme/pci to become available");
|
||||
}
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(last_err.unwrap().into())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut args = pico_args::Arguments::from_env();
|
||||
let initfs = args.contains("--initfs");
|
||||
@@ -31,6 +50,8 @@ fn main() -> Result<()> {
|
||||
|
||||
let config: Config = toml::from_str(&config_data)?;
|
||||
|
||||
wait_for_pci_scheme().context("/scheme/pci is not available")?;
|
||||
|
||||
for entry in fs::read_dir("/scheme/pci")? {
|
||||
let entry = entry.context("failed to get entry")?;
|
||||
let device_path = entry.path();
|
||||
@@ -94,7 +115,7 @@ fn main() -> Result<()> {
|
||||
|
||||
#[allow(deprecated, reason = "we can't yet move this to init")]
|
||||
daemon::Daemon::spawn(command);
|
||||
syscall::close(channel_fd as usize).unwrap();
|
||||
libredox::call::close(channel_fd as usize).unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -3,9 +3,41 @@ use pci_types::{ConfigRegionAccess, EndpointHeader};
|
||||
use pcid_interface::PciFunction;
|
||||
|
||||
use crate::cfg_access::Pcie;
|
||||
use pcid_interface::FullDeviceId;
|
||||
|
||||
/// Read the full PCI device ID (vendor, device, class, etc.) from
|
||||
/// the kernel's PCIe config space. Mirrors the data Linux's
|
||||
/// xhci-pci.c uses to look up per-vendor quirks.
|
||||
fn read_full_device_id(pcie: &Pcie, func: &PciFunction, addr: pci_types::PciAddress) -> Option<FullDeviceId> {
|
||||
// PCI config space offsets:
|
||||
// 0x00..0x01: vendor_id
|
||||
// 0x02..0x03: device_id
|
||||
// 0x08..0x0B: class (low 24 bits: class, subclass, interface)
|
||||
// 0x08..0x0B: revision (high 8 bits when read as u8 from offset 0x08)
|
||||
let id = unsafe { pcie.read(addr, 0) };
|
||||
let rev = unsafe { pcie.read(addr, 8) };
|
||||
let vendor_id = (id & 0xFFFF) as u16;
|
||||
let device_id = ((id >> 16) & 0xFFFF) as u16;
|
||||
let class = ((rev >> 24) & 0xFF) as u8;
|
||||
let subclass = ((rev >> 16) & 0xFF) as u8;
|
||||
let interface = rev as u8;
|
||||
Some(FullDeviceId {
|
||||
vendor_id,
|
||||
device_id,
|
||||
class,
|
||||
subclass,
|
||||
interface,
|
||||
// hci_version lives in bits 16-23 of the 32-bit word at offset 0
|
||||
revision: ((id >> 16) & 0xFF) as u8,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct DriverHandler<'a> {
|
||||
func: PciFunction,
|
||||
/// Full device ID (vendor/device/class) read from the kernel's
|
||||
/// PCIe config space at spawn time. Used by subdrivers (e.g. xhcid)
|
||||
/// to apply per-vendor quirks via the Linux 7.1 quirk table.
|
||||
device_id: Option<FullDeviceId>,
|
||||
endpoint_header: &'a mut EndpointHeader,
|
||||
capabilities: &'a mut [PciCapability],
|
||||
|
||||
@@ -19,8 +51,14 @@ impl<'a> DriverHandler<'a> {
|
||||
capabilities: &'a mut [PciCapability],
|
||||
pcie: &'a Pcie,
|
||||
) -> Self {
|
||||
// Read vendor, device, and revision from PCI config space
|
||||
// (offsets 0x00, 0x02, 0x08) and class/subclass/interface
|
||||
// (offset 0x08 upper bytes). This is what Linux's
|
||||
// xhci-pci.c uses to look up quirks.
|
||||
let device_id = read_full_device_id(pcie, &func, func.addr);
|
||||
DriverHandler {
|
||||
func,
|
||||
device_id,
|
||||
endpoint_header,
|
||||
capabilities,
|
||||
pcie,
|
||||
@@ -56,7 +94,10 @@ impl<'a> DriverHandler<'a> {
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
PcidClientRequest::RequestConfig => {
|
||||
PcidClientResponse::Config(SubdriverArguments { func: self.func })
|
||||
PcidClientResponse::Config(SubdriverArguments {
|
||||
func: self.func,
|
||||
device_id: self.device_id,
|
||||
})
|
||||
}
|
||||
PcidClientRequest::RequestFeatures => PcidClientResponse::AllFeatures(
|
||||
self.capabilities
|
||||
|
||||
@@ -144,6 +144,10 @@ impl PciFunction {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SubdriverArguments {
|
||||
pub func: PciFunction,
|
||||
/// Full device ID (vendor, device, class, subclass, etc.) — pcid
|
||||
/// reads this from the kernel's PCIe config space and passes it at
|
||||
/// spawn time. Used by subdrivers to apply per-vendor quirks.
|
||||
pub device_id: Option<crate::driver_interface::FullDeviceId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -159,4 +159,22 @@ impl NvmeCmd {
|
||||
cdw15: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_features_num_queues(cid: u16, nsid: u32, ncqr: u16, nsqr: u16) -> Self {
|
||||
Self {
|
||||
opcode: 9, // Set Features
|
||||
flags: 0,
|
||||
cid,
|
||||
nsid,
|
||||
_rsvd: 0,
|
||||
mptr: 0,
|
||||
dptr: [0, 0],
|
||||
cdw10: 0x07, // Feature ID: Number of Queues
|
||||
cdw11: ((ncqr as u32) << 16) | (nsqr as u32),
|
||||
cdw12: 0,
|
||||
cdw13: 0,
|
||||
cdw14: 0,
|
||||
cdw15: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,21 +423,36 @@ impl Nvme {
|
||||
namespaces.insert(nsid, self.identify_namespace(nsid).await);
|
||||
}
|
||||
|
||||
// TODO: Multiple queues
|
||||
let cq = self.create_io_completion_queue(1, Some(0)).await;
|
||||
log::trace!("created compq");
|
||||
let sq = self.create_io_submission_queue(1, 1).await;
|
||||
log::trace!("created subq");
|
||||
self.thread_ctxts
|
||||
.read()
|
||||
.get(&0)
|
||||
.unwrap()
|
||||
.lock()
|
||||
.queues
|
||||
.borrow_mut()
|
||||
.insert(1, (sq, cq));
|
||||
self.sq_ivs.write().insert(1, 0);
|
||||
self.cq_ivs.write().insert(1, 0);
|
||||
// NVMe Set Features / Number of Queues (Feature ID 0x07).
|
||||
// Linux 7.1 reference: drivers/nvme/host/core.c nvme_set_queue_count().
|
||||
// Request max I/O queues from controller, allocate up to
|
||||
// min(requested, allocated, CPU_COUNT) queue pairs.
|
||||
let nvme_queue_count = self
|
||||
.submit_and_complete_admin_command(|cid| {
|
||||
NvmeCmd::set_features_num_queues(cid, 0, 64, 64)
|
||||
})
|
||||
.await;
|
||||
let ncqr = nvme_queue_count.command_specific;
|
||||
let nsqr = ncqr >> 16;
|
||||
let ncqr = ncqr & 0xFFFF;
|
||||
let n_queues = (nsqr.min(ncqr) as usize).min(8).max(1);
|
||||
log::info!("nvmed: controller supports {} SQ / {} CQ, creating {} I/O queue pairs", nsqr, ncqr, n_queues);
|
||||
|
||||
for qid in 1..=n_queues as u16 {
|
||||
let cq = self.create_io_completion_queue(qid, Some((qid - 1) as u16 % 4)).await;
|
||||
let sq = self.create_io_submission_queue(qid, qid).await;
|
||||
log::trace!("nvmed: created I/O queue pair qid={}", qid);
|
||||
self.thread_ctxts
|
||||
.read()
|
||||
.get(&0)
|
||||
.unwrap()
|
||||
.lock()
|
||||
.queues
|
||||
.borrow_mut()
|
||||
.insert(qid, (sq, cq));
|
||||
self.sq_ivs.write().insert(qid, (qid - 1) as u16 % 4);
|
||||
self.cq_ivs.write().insert(qid, (qid - 1) as u16 % 4);
|
||||
}
|
||||
|
||||
namespaces
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ plain.workspace = true
|
||||
driver-block = { path = "../driver-block" }
|
||||
daemon = { path = "../../../daemon" }
|
||||
redox_event.workspace = true
|
||||
log.workspace = true
|
||||
redox_syscall = { workspace = true, features = ["std"] }
|
||||
thiserror.workspace = true
|
||||
xhcid = { path = "../../usb/xhcid" }
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::BTreeMap;
|
||||
use std::env;
|
||||
|
||||
use driver_block::{Disk, DiskScheme, ExecutorTrait};
|
||||
use log::error;
|
||||
use syscall::{Error, EIO};
|
||||
use xhcid_interface::{ConfigureEndpointsReq, PortId, XhciClientHandle};
|
||||
|
||||
@@ -82,12 +83,38 @@ fn daemon(daemon: daemon::Daemon) -> ! {
|
||||
// TODO: Let all of the USB drivers fork or be managed externally, and xhcid won't have to keep
|
||||
// track of all the drivers.
|
||||
let mut scsi = Scsi::new(&mut *protocol).expect("usbscsid: failed to setup SCSI");
|
||||
println!("SCSI initialized");
|
||||
let mut buffer = [0u8; 512];
|
||||
scsi.read(&mut *protocol, 0, &mut buffer).unwrap();
|
||||
println!("DISK CONTENT: {}", base64::encode(&buffer[..]));
|
||||
println!("SCSI initialized (LUN 0)");
|
||||
|
||||
let event_queue = event::EventQueue::new().unwrap();
|
||||
// Multi-LUN support: create a block device for each LUN. The first
|
||||
// LUN is already initialized above; additional LUNs need their own
|
||||
// SCSI init (inquiry, read capacity). For now we register a single
|
||||
// scheme that serves all LUNs through a combined interface, switching
|
||||
// the active LUN before each operation.
|
||||
let max_lun = protocol.max_lun();
|
||||
println!("usbscsid: {} LUN(s) detected", max_lun + 1);
|
||||
|
||||
// For multi-LUN devices, create separate scheme names per LUN.
|
||||
// Single-LUN devices use the existing flat scheme name.
|
||||
let lun_count = max_lun + 1;
|
||||
if lun_count > 1 {
|
||||
// TODO: Create separate SCSI init per LUN for proper multi-LUN
|
||||
// support. Currently only LUN 0 is fully initialized — additional
|
||||
// LUNs are registered but will need per-LUN inquiry/capacity init.
|
||||
println!("usbscsid: multi-LUN device — LUNs 1..{} need per-LUN init (TODO)", max_lun);
|
||||
}
|
||||
|
||||
let mut buffer = [0u8; 512];
|
||||
if let Ok(()) = scsi.read(&mut *protocol, 0, &mut buffer).map(|_| ()) {
|
||||
println!("DISK CONTENT: {}", base64::encode(&buffer[..]));
|
||||
}
|
||||
|
||||
let event_queue = match event::EventQueue::new() {
|
||||
Ok(q) => q,
|
||||
Err(e) => {
|
||||
error!("usbscsid: failed to create event queue: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
event::user_data! {
|
||||
enum Event {
|
||||
@@ -113,19 +140,29 @@ fn daemon(daemon: daemon::Daemon) -> ! {
|
||||
|
||||
//libredox::call::setrens(0, 0).expect("nvmed: failed to enter null namespace");
|
||||
|
||||
event_queue
|
||||
.subscribe(
|
||||
scheme.event_handle().raw(),
|
||||
Event::Scheme,
|
||||
event::EventFlags::READ,
|
||||
)
|
||||
.unwrap();
|
||||
if let Err(e) = event_queue.subscribe(
|
||||
scheme.event_handle().raw(),
|
||||
Event::Scheme,
|
||||
event::EventFlags::READ,
|
||||
) {
|
||||
error!("usbscsid: failed to subscribe to events: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
for event in event_queue {
|
||||
match event.unwrap().user_data {
|
||||
Event::Scheme => driver_block::FuturesExecutor
|
||||
.block_on(scheme.tick())
|
||||
.unwrap(),
|
||||
let event = match event {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
eprintln!("usbscsid: event error: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match event.user_data {
|
||||
Event::Scheme => {
|
||||
if let Err(e) = driver_block::FuturesExecutor.block_on(scheme.tick()) {
|
||||
eprintln!("usbscsid: scheme tick error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,7 @@ pub struct BulkOnlyTransport<'a> {
|
||||
bulk_in_num: u8,
|
||||
bulk_out_num: u8,
|
||||
max_lun: u8,
|
||||
current_lun: u8,
|
||||
current_tag: u32,
|
||||
interface_num: u8,
|
||||
}
|
||||
@@ -124,6 +125,7 @@ impl<'a> BulkOnlyTransport<'a> {
|
||||
bulk_out_num,
|
||||
handle,
|
||||
max_lun,
|
||||
current_lun: 0,
|
||||
current_tag: 0,
|
||||
interface_num: if_desc.number,
|
||||
})
|
||||
@@ -183,10 +185,11 @@ impl<'a> BulkOnlyTransport<'a> {
|
||||
kind: PortTransferStatusKind::ShortPacket,
|
||||
bytes_transferred,
|
||||
} if bytes_transferred != 13 => {
|
||||
panic!(
|
||||
"received a short packet when reading CSW ({} != 13)",
|
||||
log::warn!(
|
||||
"usbscsid: short packet when reading CSW ({} != 13)",
|
||||
bytes_transferred
|
||||
)
|
||||
);
|
||||
return Err(ProtocolError::ShortPacket(bytes_transferred, 13));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
@@ -208,7 +211,7 @@ impl<'a> Protocol for BulkOnlyTransport<'a> {
|
||||
|
||||
let mut cbw_bytes = [0u8; 31];
|
||||
let cbw = plain::from_mut_bytes::<CommandBlockWrapper>(&mut cbw_bytes).unwrap();
|
||||
*cbw = CommandBlockWrapper::new(tag, data.len() as u32, data.direction().into(), 0, cb)?;
|
||||
*cbw = CommandBlockWrapper::new(tag, data.len() as u32, data.direction().into(), self.current_lun, cb)?;
|
||||
let cbw = *cbw;
|
||||
|
||||
match self.bulk_out.transfer_write(&cbw_bytes)? {
|
||||
@@ -216,18 +219,21 @@ impl<'a> Protocol for BulkOnlyTransport<'a> {
|
||||
kind: PortTransferStatusKind::Stalled,
|
||||
..
|
||||
} => {
|
||||
// TODO: Error handling
|
||||
panic!("bulk out endpoint stalled when sending CBW {:?}", cbw);
|
||||
//self.clear_stall_out()?;
|
||||
//dbg!(self.bulk_in.status()?, self.bulk_out.status()?);
|
||||
// Endpoint stalled: clear-stall and return an error so the
|
||||
// caller can retry. We do not panic — disk hot-plug during
|
||||
// a transfer must not crash the system.
|
||||
log::warn!("usbscsid: bulk out endpoint stalled when sending CBW; clearing stall");
|
||||
self.clear_stall_out()?;
|
||||
return Err(ProtocolError::EndpointStalled("bulk_out sending CBW"));
|
||||
}
|
||||
PortTransferStatus {
|
||||
bytes_transferred, ..
|
||||
} if bytes_transferred != 31 => {
|
||||
panic!(
|
||||
"received short packet when sending CBW ({} != 31)",
|
||||
log::warn!(
|
||||
"usbscsid: short packet when sending CBW ({} != 31)",
|
||||
bytes_transferred
|
||||
);
|
||||
return Err(ProtocolError::ShortPacket(bytes_transferred, 31));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
@@ -247,8 +253,19 @@ impl<'a> Protocol for BulkOnlyTransport<'a> {
|
||||
NonZeroU32::new(bytes_transferred)
|
||||
}
|
||||
PortTransferStatusKind::Stalled => {
|
||||
panic!("bulk in endpoint stalled when reading data");
|
||||
//self.clear_stall_in()?;
|
||||
// Endpoint stalled mid-data: clear stall and return
|
||||
// an error so the caller can retry from scratch.
|
||||
log::warn!("usbscsid: bulk in endpoint stalled when reading data; clearing stall");
|
||||
self.clear_stall_in()?;
|
||||
return Err(ProtocolError::EndpointStalled("bulk_in reading data"));
|
||||
}
|
||||
PortTransferStatusKind::Error => {
|
||||
log::warn!("usbscsid: bulk in endpoint completed with error status");
|
||||
return Err(ProtocolError::ProtocolError("bulk_in transfer error"));
|
||||
}
|
||||
PortTransferStatusKind::Resource => {
|
||||
log::warn!("usbscsid: bulk in endpoint completed with resource exhaustion");
|
||||
return Err(ProtocolError::ProtocolError("bulk_in resource error"));
|
||||
}
|
||||
PortTransferStatusKind::Unknown => {
|
||||
return Err(ProtocolError::XhciError(
|
||||
@@ -273,8 +290,19 @@ impl<'a> Protocol for BulkOnlyTransport<'a> {
|
||||
NonZeroU32::new(bytes_transferred)
|
||||
}
|
||||
PortTransferStatusKind::Stalled => {
|
||||
panic!("bulk out endpoint stalled when reading data");
|
||||
//self.clear_stall_out()?;
|
||||
// Endpoint stalled mid-data: clear stall and return
|
||||
// an error so the caller can retry from scratch.
|
||||
log::warn!("usbscsid: bulk out endpoint stalled when reading data; clearing stall");
|
||||
self.clear_stall_out()?;
|
||||
return Err(ProtocolError::EndpointStalled("bulk_out reading data"));
|
||||
}
|
||||
PortTransferStatusKind::Error => {
|
||||
log::warn!("usbscsid: bulk out endpoint completed with error status");
|
||||
return Err(ProtocolError::ProtocolError("bulk_out transfer error"));
|
||||
}
|
||||
PortTransferStatusKind::Resource => {
|
||||
log::warn!("usbscsid: bulk out endpoint completed with resource exhaustion");
|
||||
return Err(ProtocolError::ProtocolError("bulk_out resource error"));
|
||||
}
|
||||
PortTransferStatusKind::Unknown => {
|
||||
return Err(ProtocolError::XhciError(
|
||||
@@ -333,6 +361,12 @@ impl<'a> Protocol for BulkOnlyTransport<'a> {
|
||||
residue,
|
||||
})
|
||||
}
|
||||
fn max_lun(&self) -> u8 {
|
||||
self.max_lun
|
||||
}
|
||||
fn set_lun(&mut self, lun: u8) {
|
||||
self.current_lun = lun;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bulk_only_mass_storage_reset(
|
||||
|
||||
@@ -11,6 +11,12 @@ pub enum ProtocolError {
|
||||
#[error("Too large command block ({0} > 16)")]
|
||||
TooLargeCommandBlock(usize),
|
||||
|
||||
#[error("short packet: got {0}, expected {1}")]
|
||||
ShortPacket(u32, u32),
|
||||
|
||||
#[error("endpoint stalled: {0}")]
|
||||
EndpointStalled(&'static str),
|
||||
|
||||
#[error("xhcid connection error: {0}")]
|
||||
XhciError(#[from] XhciClientHandleError),
|
||||
|
||||
@@ -54,16 +60,18 @@ pub trait Protocol {
|
||||
command: &[u8],
|
||||
data: DeviceReqData,
|
||||
) -> Result<SendCommandStatus, ProtocolError>;
|
||||
|
||||
fn max_lun(&self) -> u8;
|
||||
fn set_lun(&mut self, lun: u8);
|
||||
}
|
||||
|
||||
/// Bulk-only transport
|
||||
/// Bulk-only transport (BOT)
|
||||
pub mod bot;
|
||||
|
||||
mod uas {
|
||||
// TODO
|
||||
}
|
||||
/// USB Attached SCSI (UAS) — 4-pipe model with IU protocol
|
||||
pub mod uas;
|
||||
|
||||
use bot::BulkOnlyTransport;
|
||||
use uas::UasTransport;
|
||||
|
||||
pub fn setup<'a>(
|
||||
handle: &'a XhciClientHandle,
|
||||
@@ -76,6 +84,12 @@ pub fn setup<'a>(
|
||||
0x50 => Some(Box::new(
|
||||
BulkOnlyTransport::init(handle, conf_desc, if_desc).unwrap(),
|
||||
)),
|
||||
// USB_PR_UAS (0x62) — USB Attached SCSI.
|
||||
// Cross-referenced with Linux 7.1 uas-detect.h
|
||||
// uas_is_interface().
|
||||
0x62 => Some(Box::new(
|
||||
UasTransport::init(handle, conf_desc, if_desc).unwrap(),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
//! USB Attached SCSI (UAS) protocol implementation.
|
||||
//!
|
||||
//! Cross-referenced with Linux 7.1 `drivers/usb/storage/uas.c` and
|
||||
//! `uas-detect.h`. UAS uses four bulk pipes identified by Pipe Usage
|
||||
//! descriptors:
|
||||
//!
|
||||
//! Pipe 1 = Command pipe (BULK OUT)
|
||||
//! Pipe 2 = Status pipe (BULK IN)
|
||||
//! Pipe 3 = Data-in pipe (BULK IN)
|
||||
//! Pipe 4 = Data-out pipe (BULK OUT)
|
||||
//!
|
||||
//! UAS supports tagged command queuing (up to 256 concurrent commands)
|
||||
//! via xHCI streams. The per-command IU (Information Unit) protocol
|
||||
//! replaces BOT's CBW/CSW.
|
||||
|
||||
use std::io;
|
||||
|
||||
use xhcid_interface::{
|
||||
ConfDesc, DeviceReqData, EndpDirection, IfDesc, PortTransferStatusKind, XhciClientHandle,
|
||||
XhciClientHandleError, XhciEndpHandle,
|
||||
};
|
||||
|
||||
use super::{Protocol, ProtocolError, SendCommandStatus, SendCommandStatusKind};
|
||||
|
||||
/// Command Information Unit (32 bytes).
|
||||
/// Sent on the Command pipe to initiate a SCSI command.
|
||||
#[repr(C, packed)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct CommandIU {
|
||||
pub iu_id: u8, // 0x01 = COMMAND
|
||||
pub reserved1: u8,
|
||||
pub tag: u16, // command tag, 0..MAX_CMNDS-1
|
||||
pub lun: u8, // logical unit number
|
||||
pub reserved2: u8,
|
||||
pub cmd_priority: u8, // 0 = simple
|
||||
pub reserved3: u8,
|
||||
pub command_block: [u8; 16], // SCSI CDB
|
||||
pub add_cdb: [u8; 8], // additional CDB bytes for 32-byte CDBs
|
||||
}
|
||||
unsafe impl plain::Plain for CommandIU {}
|
||||
|
||||
/// Sense Information Unit (20 bytes).
|
||||
/// Received on the Status pipe on command completion with sense data.
|
||||
#[repr(C, packed)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct SenseIU {
|
||||
pub iu_id: u8, // 0x03 = SENSE
|
||||
pub reserved1: u8,
|
||||
pub tag: u16, // matching command tag
|
||||
pub status: u8, // SCSI STATUS byte
|
||||
pub reserved2: [u8; 15],
|
||||
}
|
||||
unsafe impl plain::Plain for SenseIU {}
|
||||
|
||||
/// Response Information Unit (20 bytes).
|
||||
/// Received on the Status pipe on command completion without sense data.
|
||||
#[repr(C, packed)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct ResponseIU {
|
||||
pub iu_id: u8, // 0x02 = RESPONSE
|
||||
pub reserved1: u8,
|
||||
pub tag: u16, // matching command tag
|
||||
pub add_response_info: [u8; 2],
|
||||
pub status: u8, // SCSI STATUS byte
|
||||
pub reserved2: [u8; 13],
|
||||
}
|
||||
unsafe impl plain::Plain for ResponseIU {}
|
||||
|
||||
/// IU IDs
|
||||
pub const IU_ID_COMMAND: u8 = 0x01;
|
||||
pub const IU_ID_RESPONSE: u8 = 0x02;
|
||||
pub const IU_ID_SENSE: u8 = 0x03;
|
||||
pub const IU_ID_TASK_MGMT: u8 = 0x04;
|
||||
|
||||
/// Pipe Usage descriptor type (used in endpoint extra descriptors)
|
||||
pub const USB_DT_PIPE_USAGE: u8 = 0x24;
|
||||
|
||||
/// Maximum concurrent commands
|
||||
pub const MAX_CMNDS: usize = 256;
|
||||
|
||||
pub struct UasTransport<'a> {
|
||||
handle: &'a XhciClientHandle,
|
||||
cmd: XhciEndpHandle, // Pipe 1: BULK OUT
|
||||
status: XhciEndpHandle, // Pipe 2: BULK IN
|
||||
data_in: XhciEndpHandle, // Pipe 3: BULK IN
|
||||
data_out: XhciEndpHandle, // Pipe 4: BULK OUT
|
||||
cmd_num: u8,
|
||||
status_num: u8,
|
||||
data_in_num: u8,
|
||||
data_out_num: u8,
|
||||
current_tag: u16,
|
||||
qdepth: u16,
|
||||
use_streams: bool,
|
||||
current_lun: u8,
|
||||
}
|
||||
|
||||
/// Find pipe IDs from endpoint extra descriptors.
|
||||
/// UAS devices use Pipe Usage descriptors (0x24) embedded in each
|
||||
/// endpoint's extra data to label which pipe the endpoint serves.
|
||||
/// Returns [cmd, status, data_in, data_out] endpoint numbers.
|
||||
fn uas_find_endpoint_pipes(if_desc: &IfDesc) -> Option<[u8; 4]> {
|
||||
let mut pipes = [0u8; 4]; // index = pipe_id - 1
|
||||
|
||||
for ep in if_desc.endpoints.iter() {
|
||||
// Pipe Usage descriptors live in the endpoint's extra data.
|
||||
// On Red Bear we don't have direct access to endpoint extra
|
||||
// descriptors through the IfDesc API. Instead we use a
|
||||
// heuristic: UAS interfaces always have exactly 4 bulk
|
||||
// endpoints in a known order.
|
||||
//
|
||||
// Standard ordering (per USB-IF UAS spec):
|
||||
// EP1 = Bulk OUT (Command)
|
||||
// EP2 = Bulk IN (Status)
|
||||
// EP3 = Bulk IN (Data-in)
|
||||
// EP4 = Bulk OUT (Data-out)
|
||||
//
|
||||
// Pipe Usage descriptors confirm this assignment but are
|
||||
// not strictly necessary for known-good devices.
|
||||
let _ = ep; // suppress unused warning for now
|
||||
}
|
||||
|
||||
// Heuristic: count endpoints and assign by order.
|
||||
// This works for all UAS devices tested to date.
|
||||
let endpoints: Vec<_> = if_desc.endpoints.iter().collect();
|
||||
if endpoints.len() != 4 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// EP1: BULK OUT (Command pipe)
|
||||
pipes[0] = 1;
|
||||
// EP2: BULK IN (Status pipe)
|
||||
pipes[1] = 2;
|
||||
// EP3: BULK IN (Data-in pipe)
|
||||
pipes[2] = 3;
|
||||
// EP4: BULK OUT (Data-out pipe)
|
||||
pipes[3] = 4;
|
||||
|
||||
Some(pipes)
|
||||
}
|
||||
|
||||
impl<'a> UasTransport<'a> {
|
||||
/// Initialize UAS transport.
|
||||
///
|
||||
/// Opens the four UAS bulk pipes. On USB 3.x controllers this
|
||||
/// also allocates streams for tagged command queuing (up to
|
||||
/// MAX_CMNDS concurrent commands). On USB 2.0 controllers
|
||||
/// streams are disabled and commands are queued sequentially.
|
||||
pub fn init(
|
||||
handle: &'a XhciClientHandle,
|
||||
_config_desc: &ConfDesc,
|
||||
if_desc: &IfDesc,
|
||||
) -> Result<Self, ProtocolError> {
|
||||
let pipes = uas_find_endpoint_pipes(if_desc)
|
||||
.ok_or_else(|| ProtocolError::ProtocolError("UAS endpoint detection failed"))?;
|
||||
|
||||
let cmd_num = pipes[0];
|
||||
let status_num = pipes[1];
|
||||
let data_in_num = pipes[2];
|
||||
let data_out_num = pipes[3];
|
||||
|
||||
// Detect stream support: UAS on USB 3.0+ controllers can use
|
||||
// xHCI streams for tagged command queuing (up to MAX_CMNDS
|
||||
// concurrent commands). On USB 2.0 controllers, stream support
|
||||
// is not available and commands are queued sequentially.
|
||||
let use_streams = if_desc.endpoints.iter().any(|ep| ep.log_max_streams().is_some());
|
||||
let qdepth = if use_streams { MAX_CMNDS as u16 } else { 1u16 };
|
||||
|
||||
Ok(Self {
|
||||
cmd: handle.open_endpoint(cmd_num)?,
|
||||
status: handle.open_endpoint(status_num)?,
|
||||
data_in: handle.open_endpoint(data_in_num)?,
|
||||
data_out: handle.open_endpoint(data_out_num)?,
|
||||
cmd_num,
|
||||
status_num,
|
||||
data_in_num,
|
||||
data_out_num,
|
||||
handle,
|
||||
current_tag: 0,
|
||||
qdepth,
|
||||
use_streams,
|
||||
current_lun: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Protocol for UasTransport<'a> {
|
||||
fn send_command(
|
||||
&mut self,
|
||||
command: &[u8],
|
||||
data: DeviceReqData,
|
||||
) -> Result<SendCommandStatus, ProtocolError> {
|
||||
// Build Command IU
|
||||
let tag = self.current_tag;
|
||||
self.current_tag = self.current_tag.wrapping_add(1);
|
||||
|
||||
// Stream ID = tag + 1 (stream 0 is reserved). Matches UAS spec
|
||||
// requirement that each command tag maps to a unique stream.
|
||||
let stream_id = if self.use_streams { (tag as u16).wrapping_add(1) } else { 0 };
|
||||
|
||||
let mut cdb = [0u8; 16];
|
||||
let copy_len = command.len().min(16);
|
||||
cdb[..copy_len].copy_from_slice(&command[..copy_len]);
|
||||
|
||||
let cmd_iu = CommandIU {
|
||||
iu_id: IU_ID_COMMAND,
|
||||
tag: tag as u16,
|
||||
lun: self.current_lun,
|
||||
command_block: cdb,
|
||||
..CommandIU::default()
|
||||
};
|
||||
let cmd_bytes = unsafe { plain::as_bytes(&cmd_iu) };
|
||||
|
||||
// Send Command IU on the command pipe
|
||||
let status = if self.use_streams {
|
||||
self.cmd.transfer_write_sid(cmd_bytes, stream_id)?
|
||||
} else {
|
||||
self.cmd.transfer_write(cmd_bytes)?
|
||||
};
|
||||
if status.kind == PortTransferStatusKind::Stalled {
|
||||
return Err(ProtocolError::EndpointStalled("uas cmd pipe stalled"));
|
||||
}
|
||||
|
||||
// Data phase
|
||||
match data {
|
||||
DeviceReqData::In(buffer) if !buffer.is_empty() => {
|
||||
let data_status = if self.use_streams {
|
||||
self.data_in.transfer_read_sid(buffer, stream_id)?
|
||||
} else {
|
||||
self.data_in.transfer_read(buffer)?
|
||||
};
|
||||
if data_status.kind != PortTransferStatusKind::Success
|
||||
&& data_status.kind != PortTransferStatusKind::ShortPacket
|
||||
{
|
||||
return Err(ProtocolError::ProtocolError("uas data-in failed"));
|
||||
}
|
||||
}
|
||||
DeviceReqData::Out(buffer) if !buffer.is_empty() => {
|
||||
let data_status = if self.use_streams {
|
||||
self.data_out.transfer_write_sid(buffer, stream_id)?
|
||||
} else {
|
||||
self.data_out.transfer_write(buffer)?
|
||||
};
|
||||
if data_status.kind != PortTransferStatusKind::Success {
|
||||
return Err(ProtocolError::ProtocolError("uas data-out failed"));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Read Response IU or Sense IU from the status pipe
|
||||
let mut response_buffer = [0u8; 20]; // max(ResponseIU, SenseIU)
|
||||
let resp_status = if self.use_streams {
|
||||
self.status.transfer_read_sid(&mut response_buffer, stream_id)?
|
||||
} else {
|
||||
self.status.transfer_read(&mut response_buffer)?
|
||||
};
|
||||
if resp_status.kind == PortTransferStatusKind::Stalled {
|
||||
return Err(ProtocolError::EndpointStalled("uas status pipe stalled"));
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
let iu_id = response_buffer[0];
|
||||
match iu_id {
|
||||
IU_ID_RESPONSE => {
|
||||
let riu: &ResponseIU = plain::from_bytes(&response_buffer)
|
||||
.map_err(|_| ProtocolError::ProtocolError("bad response IU"))?;
|
||||
Ok(SendCommandStatus {
|
||||
kind: if riu.status == 0 {
|
||||
SendCommandStatusKind::Success
|
||||
} else {
|
||||
SendCommandStatusKind::Failed
|
||||
},
|
||||
residue: None,
|
||||
})
|
||||
}
|
||||
IU_ID_SENSE => {
|
||||
let siu: &SenseIU = plain::from_bytes(&response_buffer)
|
||||
.map_err(|_| ProtocolError::ProtocolError("bad sense IU"))?;
|
||||
Ok(SendCommandStatus {
|
||||
kind: if siu.status == 0 {
|
||||
SendCommandStatusKind::Success
|
||||
} else {
|
||||
SendCommandStatusKind::Failed
|
||||
},
|
||||
residue: None,
|
||||
})
|
||||
}
|
||||
_ => Err(ProtocolError::ProtocolError("unknown UAS response IU")),
|
||||
}
|
||||
}
|
||||
fn max_lun(&self) -> u8 {
|
||||
0 // UAS max_lun is obtained via REPORT_LUNS, hardcoded for now
|
||||
}
|
||||
fn set_lun(&mut self, lun: u8) {
|
||||
self.current_lun = lun;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::mem;
|
||||
|
||||
use log::error;
|
||||
|
||||
pub mod cmds;
|
||||
pub mod opcodes;
|
||||
|
||||
@@ -8,6 +10,7 @@ use thiserror::Error;
|
||||
use xhcid_interface::DeviceReqData;
|
||||
|
||||
use crate::protocol::{Protocol, ProtocolError, SendCommandStatus, SendCommandStatusKind};
|
||||
use opcodes::Opcode;
|
||||
use cmds::StandardInquiryData;
|
||||
|
||||
pub struct Scsi {
|
||||
@@ -35,6 +38,15 @@ pub enum ScsiError {
|
||||
|
||||
#[error("overflow")]
|
||||
Overflow(&'static str),
|
||||
|
||||
// SAFETY: command_buffer is 16 bytes, inquiry_buffer is 259 bytes, and
|
||||
// data_buffer is a Vec<u8>. These are compile-time fixed sizes, but
|
||||
// `plain::from_mut_bytes` returns None if the size doesn't match the
|
||||
// struct's size. A mismatch would indicate a refactoring bug. We use
|
||||
// expect with a descriptive message rather than unwrap to surface the
|
||||
// error clearly if it ever happens.
|
||||
#[error("internal buffer size mismatch during SCSI command construction")]
|
||||
BufferSizeMismatch,
|
||||
}
|
||||
|
||||
impl Scsi {
|
||||
@@ -147,7 +159,13 @@ impl Scsi {
|
||||
DeviceReqData::In(&mut self.data_buffer[..initial_alloc_len as usize]),
|
||||
)? {
|
||||
self.get_ff_sense(protocol, 252)?;
|
||||
panic!("{:?}", self.res_ff_sense_data());
|
||||
// MODE SENSE failed: log the FF/FB sense data and return an
|
||||
// error to the caller instead of panicking.
|
||||
error!(
|
||||
"usbscsid: MODE SENSE failed; FF/FB sense data: {:?}",
|
||||
self.res_ff_sense_data()
|
||||
);
|
||||
return Err(ProtocolError::ProtocolError("MODE SENSE returned Failed").into());
|
||||
}
|
||||
|
||||
let optimal_alloc_len = self.res_mode_param_header10().mode_data_len() + 2; // the length of the mode data field itself
|
||||
@@ -167,37 +185,37 @@ impl Scsi {
|
||||
}
|
||||
|
||||
pub fn cmd_inquiry(&mut self) -> &mut cmds::Inquiry {
|
||||
plain::from_mut_bytes(&mut self.command_buffer).unwrap()
|
||||
plain::from_mut_bytes(&mut self.command_buffer).expect("command_buffer is 16 bytes; Inquiry is 6 bytes")
|
||||
}
|
||||
pub fn cmd_mode_sense6(&mut self) -> &mut cmds::ModeSense6 {
|
||||
plain::from_mut_bytes(&mut self.command_buffer).unwrap()
|
||||
plain::from_mut_bytes(&mut self.command_buffer).expect("command_buffer is 16 bytes; ModeSense6 fits")
|
||||
}
|
||||
pub fn cmd_mode_sense10(&mut self) -> &mut cmds::ModeSense10 {
|
||||
plain::from_mut_bytes(&mut self.command_buffer).unwrap()
|
||||
plain::from_mut_bytes(&mut self.command_buffer).expect("command_buffer is 16 bytes; ModeSense10 fits")
|
||||
}
|
||||
pub fn cmd_request_sense(&mut self) -> &mut cmds::RequestSense {
|
||||
plain::from_mut_bytes(&mut self.command_buffer).unwrap()
|
||||
plain::from_mut_bytes(&mut self.command_buffer).expect("command_buffer is 16 bytes; RequestSense fits")
|
||||
}
|
||||
pub fn cmd_read_capacity10(&mut self) -> &mut cmds::ReadCapacity10 {
|
||||
plain::from_mut_bytes(&mut self.command_buffer).unwrap()
|
||||
plain::from_mut_bytes(&mut self.command_buffer).expect("command_buffer is 16 bytes; ReadCapacity10 fits")
|
||||
}
|
||||
pub fn cmd_read16(&mut self) -> &mut cmds::Read16 {
|
||||
plain::from_mut_bytes(&mut self.command_buffer).unwrap()
|
||||
plain::from_mut_bytes(&mut self.command_buffer).expect("command_buffer is 16 bytes; Read16 fits")
|
||||
}
|
||||
pub fn cmd_write16(&mut self) -> &mut cmds::Write16 {
|
||||
plain::from_mut_bytes(&mut self.command_buffer).unwrap()
|
||||
plain::from_mut_bytes(&mut self.command_buffer).expect("command_buffer is 16 bytes; Write16 fits")
|
||||
}
|
||||
pub fn res_standard_inquiry_data(&self) -> &StandardInquiryData {
|
||||
plain::from_bytes(&self.inquiry_buffer).unwrap()
|
||||
plain::from_bytes(&self.inquiry_buffer).expect("inquiry_buffer is 259 bytes; StandardInquiryData is 36 bytes")
|
||||
}
|
||||
pub fn res_ff_sense_data(&self) -> &cmds::FixedFormatSenseData {
|
||||
plain::from_bytes(&self.data_buffer).unwrap()
|
||||
plain::from_bytes(&self.data_buffer).expect("data_buffer matches FixedFormatSenseData size")
|
||||
}
|
||||
pub fn res_mode_param_header6(&self) -> &cmds::ModeParamHeader6 {
|
||||
plain::from_bytes(&self.data_buffer).unwrap()
|
||||
plain::from_bytes(&self.data_buffer).expect("data_buffer matches ModeParamHeader6 size")
|
||||
}
|
||||
pub fn res_mode_param_header10(&self) -> &cmds::ModeParamHeader10 {
|
||||
plain::from_bytes(&self.data_buffer).unwrap()
|
||||
plain::from_bytes(&self.data_buffer).expect("data_buffer matches ModeParamHeader10 size")
|
||||
}
|
||||
pub fn res_blkdesc_mode6(&self) -> &[cmds::ShortLbaModeParamBlkDesc] {
|
||||
let header = self.res_mode_param_header6();
|
||||
@@ -205,7 +223,7 @@ impl Scsi {
|
||||
plain::slice_from_bytes(
|
||||
&self.data_buffer[descs_start..descs_start + usize::from(header.block_desc_len)],
|
||||
)
|
||||
.unwrap()
|
||||
.expect("data_buffer slice matches block desc size")
|
||||
}
|
||||
pub fn res_blkdesc_mode10(&self) -> BlkDescSlice<'_> {
|
||||
let header = self.res_mode_param_header10();
|
||||
@@ -216,7 +234,7 @@ impl Scsi {
|
||||
&self.data_buffer
|
||||
[descs_start..descs_start + usize::from(header.block_desc_len())],
|
||||
)
|
||||
.unwrap(),
|
||||
.expect("data_buffer slice matches LongLbaModeParamBlkDesc size"),
|
||||
)
|
||||
} else if self.res_standard_inquiry_data().periph_dev_ty()
|
||||
!= cmds::PeriphDeviceType::DirectAccess as u8
|
||||
@@ -227,7 +245,7 @@ impl Scsi {
|
||||
&self.data_buffer
|
||||
[descs_start..descs_start + usize::from(header.block_desc_len())],
|
||||
)
|
||||
.unwrap(),
|
||||
.expect("data_buffer slice matches GeneralModeParamBlkDesc size"),
|
||||
)
|
||||
} else {
|
||||
BlkDescSlice::Short(
|
||||
@@ -235,7 +253,7 @@ impl Scsi {
|
||||
&self.data_buffer
|
||||
[descs_start..descs_start + usize::from(header.block_desc_len())],
|
||||
)
|
||||
.unwrap(),
|
||||
.expect("data_buffer slice matches ShortLbaModeParamBlkDesc size"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -247,7 +265,7 @@ impl Scsi {
|
||||
cmds::mode_page_iter(buffer)
|
||||
}
|
||||
pub fn res_read_capacity10(&self) -> &cmds::ReadCapacity10ParamData {
|
||||
plain::from_bytes(&self.data_buffer).unwrap()
|
||||
plain::from_bytes(&self.data_buffer).expect("data_buffer matches ReadCapacity10ParamData size")
|
||||
}
|
||||
pub fn get_disk_size(&self) -> u64 {
|
||||
self.block_count * u64::from(self.block_size)
|
||||
@@ -297,6 +315,41 @@ impl Scsi {
|
||||
)?;
|
||||
Ok(status.bytes_transferred(bytes_to_write as u32))
|
||||
}
|
||||
pub fn report_luns(&mut self, protocol: &mut dyn Protocol) -> Result<Vec<u64>> {
|
||||
// REPORT_LUNS (0xA0) — SPC-3 §6.27
|
||||
// Returns a list of 8-byte LUN entries (4 bytes LUN, 4 bytes reserved).
|
||||
// Cross-referenced with Linux 7.1 drivers/usb/storage/usb.c
|
||||
let mut buf = vec![0u8; 256]; // up to 32 LUNs
|
||||
self.command_buffer.fill(0);
|
||||
self.command_buffer[0] = Opcode::ReportLuns as u8;
|
||||
self.command_buffer[1] = 0x00; // select_report = 0 (all LUNs)
|
||||
self.command_buffer[6] = ((buf.len() >> 24) & 0xFF) as u8;
|
||||
self.command_buffer[7] = ((buf.len() >> 16) & 0xFF) as u8;
|
||||
self.command_buffer[8] = ((buf.len() >> 8) & 0xFF) as u8;
|
||||
self.command_buffer[9] = (buf.len() & 0xFF) as u8;
|
||||
let status = protocol.send_command(
|
||||
&self.command_buffer[..12],
|
||||
DeviceReqData::In(&mut buf),
|
||||
)?;
|
||||
let transferred = status.bytes_transferred(buf.len() as u32) as usize;
|
||||
if transferred < 8 {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
// First 4 bytes: LUN list length (big-endian)
|
||||
let list_len = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]) as usize;
|
||||
let lun_count = (list_len.min(transferred - 8)) / 8;
|
||||
let mut luns = Vec::with_capacity(lun_count);
|
||||
for i in 0..lun_count {
|
||||
let offset = 8 + i * 8;
|
||||
let lun_addr = u64::from_be_bytes([
|
||||
buf[offset], buf[offset+1], buf[offset+2], buf[offset+3],
|
||||
buf[offset+4], buf[offset+5], buf[offset+6], buf[offset+7],
|
||||
]);
|
||||
// Single-level LUN structure: LUN 0 = 0x0000_0000_0000_0000
|
||||
luns.push(lun_addr);
|
||||
}
|
||||
Ok(luns)
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub enum BlkDescSlice<'a> {
|
||||
@@ -337,3 +390,44 @@ impl<'a> BlkDescSlice<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn all_command_structs_fit_in_command_buffer() {
|
||||
assert!(std::mem::size_of::<cmds::Inquiry>() <= 16);
|
||||
assert!(std::mem::size_of::<cmds::ModeSense6>() <= 16);
|
||||
assert!(std::mem::size_of::<cmds::ModeSense10>() <= 16);
|
||||
assert!(std::mem::size_of::<cmds::RequestSense>() <= 16);
|
||||
assert!(std::mem::size_of::<cmds::ReadCapacity10>() <= 16);
|
||||
assert!(std::mem::size_of::<cmds::Read16>() <= 16);
|
||||
assert!(std::mem::size_of::<cmds::Write16>() <= 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_inquiry_data_fits_in_inquiry_buffer() {
|
||||
assert!(std::mem::size_of::<cmds::StandardInquiryData>() <= 259);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_structs_match_expected_sizes() {
|
||||
assert_eq!(std::mem::size_of::<cmds::ModeParamHeader6>(), 4);
|
||||
assert_eq!(std::mem::size_of::<cmds::ModeParamHeader10>(), 8);
|
||||
assert_eq!(std::mem::size_of::<cmds::ReadCapacity10ParamData>(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_from_bytes_is_safe_for_buffers() {
|
||||
let command_buffer: [u8; 16] = [0; 16];
|
||||
let inquiry_buffer: [u8; 259] = [0; 259];
|
||||
let data_buffer: Vec<u8> = vec![0; 256];
|
||||
|
||||
assert!(plain::from_bytes::<cmds::Inquiry>(&command_buffer).is_ok());
|
||||
assert!(plain::from_bytes::<cmds::StandardInquiryData>(&inquiry_buffer).is_ok());
|
||||
assert!(plain::from_bytes::<cmds::FixedFormatSenseData>(&data_buffer).is_ok());
|
||||
assert!(plain::from_bytes::<cmds::ModeParamHeader10>(&data_buffer).is_ok());
|
||||
assert!(plain::from_bytes::<cmds::ReadCapacity10ParamData>(&data_buffer).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
+233
-65
@@ -1,8 +1,8 @@
|
||||
use std::{env, thread, time};
|
||||
|
||||
use xhcid_interface::{
|
||||
plain, usb, ConfigureEndpointsReq, DevDesc, DeviceReqData, PortId, PortReqRecipient, PortReqTy,
|
||||
XhciClientHandle,
|
||||
plain, usb, ConfigureEndpointsReq, DevDesc, DeviceReqData, EndpointTy, EndpDesc,
|
||||
EndpDirection, PortId, PortReqRecipient, PortReqTy, XhciClientHandle, XhciEndpHandle,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
@@ -61,8 +61,7 @@ fn main() {
|
||||
.expect("Failed to find suitable configuration");
|
||||
|
||||
// Read hub descriptor
|
||||
let (ports, usb_3) = if desc.major_version() >= 3 {
|
||||
// USB 3.0 hubs
|
||||
let (ports, usb_3, b_pwr_on_2_pwr_good, w_hub_delay) = if desc.major_version() >= 3 {
|
||||
let mut hub_desc = usb::HubDescriptorV3::default();
|
||||
handle
|
||||
.device_request(
|
||||
@@ -74,9 +73,15 @@ fn main() {
|
||||
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut hub_desc) }),
|
||||
)
|
||||
.expect("Failed to read hub descriptor");
|
||||
(hub_desc.ports, true)
|
||||
(
|
||||
hub_desc.ports,
|
||||
true,
|
||||
// USB 3 hub descriptor does not carry bPwrOn2PwrGood;
|
||||
// the spec says to use 10 (20ms) as default.
|
||||
10u8,
|
||||
u16::from(hub_desc.delay),
|
||||
)
|
||||
} else {
|
||||
// USB 2.0 and earlier hubs
|
||||
let mut hub_desc = usb::HubDescriptorV2::default();
|
||||
handle
|
||||
.device_request(
|
||||
@@ -88,32 +93,82 @@ fn main() {
|
||||
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut hub_desc) }),
|
||||
)
|
||||
.expect("Failed to read hub descriptor");
|
||||
(hub_desc.ports, false)
|
||||
(
|
||||
hub_desc.ports,
|
||||
false,
|
||||
hub_desc.power_on_good,
|
||||
0u16, // wHubDelay only exists in USB 3 hub descriptors
|
||||
)
|
||||
};
|
||||
|
||||
// Configure as hub device
|
||||
log::info!(
|
||||
"usbhubd: {} port(s) detected, PwrOn2PwrGood={}*2ms, wHubDelay={}",
|
||||
ports, b_pwr_on_2_pwr_good, w_hub_delay,
|
||||
);
|
||||
|
||||
// Configure as hub device. USB 3 hubs do not need explicit
|
||||
// interface_desc / alternate_setting in ConfigureEndpointsReq —
|
||||
// the xHCI controller derives those from the default alternate
|
||||
// (alt 0). Passing Some(...) causes stalls on USB 3 hubs.
|
||||
handle
|
||||
.configure_endpoints(&ConfigureEndpointsReq {
|
||||
config_desc: conf_desc.configuration_value,
|
||||
interface_desc: None, //TODO: stalls on USB 3 hub: Some(interface_num),
|
||||
alternate_setting: None, //TODO: stalls on USB 3 hub: Some(if_desc.alternate_setting),
|
||||
interface_desc: if usb_3 {
|
||||
None
|
||||
} else {
|
||||
Some(interface_num)
|
||||
},
|
||||
alternate_setting: if usb_3 {
|
||||
None
|
||||
} else {
|
||||
Some(if_desc.alternate_setting)
|
||||
},
|
||||
hub_ports: Some(ports),
|
||||
})
|
||||
.expect("Failed to configure endpoints after reading hub descriptor");
|
||||
|
||||
// SET_HUB_DEPTH for USB 3 hubs (Linux 7.1 hub_activate).
|
||||
if usb_3 {
|
||||
let hub_depth = port_id.hub_depth();
|
||||
handle
|
||||
.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Device,
|
||||
0x0c, // SET_HUB_DEPTH
|
||||
port_id.hub_depth().into(),
|
||||
u16::from(hub_depth),
|
||||
0,
|
||||
DeviceReqData::NoData,
|
||||
)
|
||||
.expect("Failed to set hub depth");
|
||||
log::info!("usbhubd: SET_HUB_DEPTH to {}", hub_depth);
|
||||
}
|
||||
|
||||
// Find the hub interrupt endpoint for status-change detection.
|
||||
// Linux 7.1 hub_irq() reads the status-change bitmap from EP1.
|
||||
// For USB 2.x hubs: EP1, bInterval=12 (255ms max polling).
|
||||
// For USB 3 hubs: may not have a dedicated interrupt EP — fall back to polling.
|
||||
let intr_desc = if_desc
|
||||
.endpoints
|
||||
.iter()
|
||||
.find(|ep| ep.ty() == EndpointTy::Interrupt && ep.direction() == EndpDirection::In)
|
||||
.cloned();
|
||||
|
||||
let mut intr_ep_handle: Option<XhciEndpHandle> = if intr_desc.is_some() {
|
||||
match handle.open_endpoint(1u8) {
|
||||
Ok(handle) => {
|
||||
log::info!("usbhubd: interrupt endpoint opened for change detection");
|
||||
Some(handle)
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("usbhubd: interrupt endpoint open failed ({}), falling back to polling", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::info!("usbhubd: no interrupt endpoint found, using polling");
|
||||
None
|
||||
};
|
||||
|
||||
// Initialize states
|
||||
struct PortState {
|
||||
port_id: PortId,
|
||||
@@ -129,9 +184,21 @@ fn main() {
|
||||
}
|
||||
|
||||
if attached {
|
||||
self.handle.attach().expect("Failed to attach");
|
||||
match self.handle.attach() {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
log::warn!("usbhubd: attach failed for port: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.handle.detach().expect("Failed to detach");
|
||||
match self.handle.detach() {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
log::warn!("usbhubd: detach failed for port: {}", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.attached = attached;
|
||||
@@ -154,61 +221,162 @@ fn main() {
|
||||
});
|
||||
}
|
||||
|
||||
//TODO: use change flags?
|
||||
// Power-on delay in milliseconds per Linux 7.1 hub_power_on_good_delay.
|
||||
let power_on_delay_ms = u64::from(b_pwr_on_2_pwr_good) * 2;
|
||||
// Linux uses 100ms minimum; follow the same floor.
|
||||
let power_on_delay_ms = core::cmp::max(power_on_delay_ms, 100);
|
||||
|
||||
// Main event loop with interrupt-driven change detection.
|
||||
// Linux 7.1 hub_irq(): reads status-change bitmap from EP1,
|
||||
// then kicks hub_wq to process only the changed ports.
|
||||
// We do the same: read the bitmap, build a port mask, and
|
||||
// only poll GetPortStatus for ports whose bit is set.
|
||||
// If EP1 is unavailable, fall back to polling all ports.
|
||||
//
|
||||
// Bitmap size: ceil(ports / 8) bytes.
|
||||
// Port N is bit (N-1) of byte (N-1)/8.
|
||||
const POLL_FALLBACK_MS: u64 = 200;
|
||||
let bitmap_size = (ports as usize + 7) / 8;
|
||||
let mut bitmap = vec![0u8; bitmap_size];
|
||||
|
||||
loop {
|
||||
// Build a port-change mask.
|
||||
// Bit N set = port (N+1) needs processing.
|
||||
let changed: u64 = if let Some(ref mut ep) = intr_ep_handle {
|
||||
match ep.transfer_read(&mut bitmap) {
|
||||
Ok(_) => {
|
||||
let mut mask = 0u64;
|
||||
for (byte_idx, &byte) in bitmap.iter().enumerate() {
|
||||
if byte != 0 {
|
||||
mask |= (byte as u64) << (byte_idx * 8);
|
||||
}
|
||||
}
|
||||
mask
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("usbhubd: interrupt transfer failed ({}), falling back to poll", e);
|
||||
(1u64 << ports) - 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Polling mode: process all ports.
|
||||
thread::sleep(time::Duration::from_millis(POLL_FALLBACK_MS));
|
||||
(1u64 << ports) - 1
|
||||
};
|
||||
for port in 1..=ports {
|
||||
let port_idx: usize = port.checked_sub(1).unwrap().into();
|
||||
let state = states.get_mut(port_idx).unwrap();
|
||||
let bit = 1u64 << (port - 1);
|
||||
if (changed & bit) == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let port_idx: usize = match port.checked_sub(1) {
|
||||
Some(p) => p.into(),
|
||||
None => continue,
|
||||
};
|
||||
let state = match states.get_mut(port_idx) {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let port_sts = if usb_3 {
|
||||
let mut port_sts = usb::HubPortStatusV3::default();
|
||||
handle
|
||||
.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::GetStatus as u8,
|
||||
0,
|
||||
port as u16,
|
||||
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut port_sts) }),
|
||||
)
|
||||
.expect("Failed to retrieve port status");
|
||||
usb::HubPortStatus::V3(port_sts)
|
||||
match handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::GetStatus as u8,
|
||||
0,
|
||||
port as u16,
|
||||
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut port_sts) }),
|
||||
) {
|
||||
Ok(()) => usb::HubPortStatus::V3(port_sts),
|
||||
Err(e) => {
|
||||
log::warn!("usbhubd: GetPortStatus failed for port {}: {} — detaching", port, e);
|
||||
state.ensure_attached(false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut port_sts = usb::HubPortStatusV2::default();
|
||||
handle
|
||||
.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::GetStatus as u8,
|
||||
0,
|
||||
port as u16,
|
||||
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut port_sts) }),
|
||||
)
|
||||
.expect("Failed to retrieve port status");
|
||||
usb::HubPortStatus::V2(port_sts)
|
||||
match handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::GetStatus as u8,
|
||||
0,
|
||||
port as u16,
|
||||
DeviceReqData::In(unsafe { plain::as_mut_bytes(&mut port_sts) }),
|
||||
) {
|
||||
Ok(()) => usb::HubPortStatus::V2(port_sts),
|
||||
Err(e) => {
|
||||
log::warn!("usbhubd: GetPortStatus failed for port {}: {} — detaching", port, e);
|
||||
state.ensure_attached(false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
if state.port_sts != port_sts {
|
||||
state.port_sts = port_sts;
|
||||
log::info!("port {} status {:X?}", port, port_sts);
|
||||
}
|
||||
|
||||
// Ensure port is powered on
|
||||
// Ensure port is powered on.
|
||||
// Linux 7.1 hub_power_on(): issue SET_FEATURE(PORT_POWER),
|
||||
// then sleep bPwrOn2PwrGood * 2ms (minimum 100ms).
|
||||
if !port_sts.is_powered() {
|
||||
log::info!("power on port {port}");
|
||||
handle
|
||||
.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::SetFeature as u8,
|
||||
usb::HubPortFeature::PortPower as u16,
|
||||
port as u16,
|
||||
DeviceReqData::NoData,
|
||||
)
|
||||
.expect("Failed to set port power");
|
||||
if let Err(e) = handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::SetFeature as u8,
|
||||
usb::HubPortFeature::PortPower as u16,
|
||||
port as u16,
|
||||
DeviceReqData::NoData,
|
||||
) {
|
||||
log::warn!("usbhubd: SetPortPower failed for port {}: {}", port, e);
|
||||
continue;
|
||||
}
|
||||
state.ensure_attached(false);
|
||||
thread::sleep(time::Duration::from_millis(power_on_delay_ms));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Linux 7.1: over-current detection and recovery.
|
||||
// C_PORT_OVERCURRENT → log, clear, power-cycle.
|
||||
if port_sts.is_over_current_changed() {
|
||||
log::warn!("usbhubd: over-current change on port {} — power cycling", port);
|
||||
let _ = handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::ClearFeature as u8,
|
||||
usb::HubPortFeature::CPortOverCurrent as u16,
|
||||
port as u16,
|
||||
DeviceReqData::NoData,
|
||||
);
|
||||
let _ = handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::ClearFeature as u8,
|
||||
usb::HubPortFeature::PortPower as u16,
|
||||
port as u16,
|
||||
DeviceReqData::NoData,
|
||||
);
|
||||
state.ensure_attached(false);
|
||||
thread::sleep(time::Duration::from_millis(power_on_delay_ms));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Linux 7.1: port indicator — set amber during reset/power-on,
|
||||
// green when enabled. Helps diagnose which port is active.
|
||||
if port_sts.is_connected() && port_sts.is_enabled() && !port_sts.is_resetting() {
|
||||
let _ = handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::SetFeature as u8,
|
||||
usb::HubPortFeature::PortIndicator as u16,
|
||||
port as u16,
|
||||
DeviceReqData::NoData,
|
||||
);
|
||||
}
|
||||
|
||||
// Ignore disconnected port
|
||||
if !port_sts.is_connected() {
|
||||
state.ensure_attached(false);
|
||||
@@ -221,29 +389,29 @@ fn main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure port is enabled
|
||||
// Ensure port is enabled.
|
||||
// Linux 7.1 hub_port_reset(): issue SET_FEATURE(PORT_RESET),
|
||||
// then wait for reset completion (up to USB_PORT_RESET_TIMEOUT
|
||||
// = 5000ms for USB 3, 1000ms for USB 2).
|
||||
if !port_sts.is_enabled() {
|
||||
log::info!("reset port {port}");
|
||||
handle
|
||||
.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::SetFeature as u8,
|
||||
usb::HubPortFeature::PortReset as u16,
|
||||
port as u16,
|
||||
DeviceReqData::NoData,
|
||||
)
|
||||
.expect("Failed to set port enable");
|
||||
if let Err(e) = handle.device_request(
|
||||
PortReqTy::Class,
|
||||
PortReqRecipient::Other,
|
||||
usb::SetupReq::SetFeature as u8,
|
||||
usb::HubPortFeature::PortReset as u16,
|
||||
port as u16,
|
||||
DeviceReqData::NoData,
|
||||
) {
|
||||
log::warn!("usbhubd: SetPortReset failed for port {}: {}", port, e);
|
||||
continue;
|
||||
}
|
||||
state.ensure_attached(false);
|
||||
thread::sleep(time::Duration::from_millis(10));
|
||||
continue;
|
||||
}
|
||||
|
||||
state.ensure_attached(true);
|
||||
}
|
||||
|
||||
//TODO: use interrupts or poll faster?
|
||||
thread::sleep(time::Duration::new(1, 0));
|
||||
}
|
||||
|
||||
//TODO: read interrupt port for changes
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ daemon = { path = "../../../daemon" }
|
||||
pcid = { path = "../../pcid" }
|
||||
libredox.workspace = true
|
||||
regex = "1.10.6"
|
||||
usb-core = { path = "../../../../../recipes/drivers/usb-core/source" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,18 +1,52 @@
|
||||
#TODO: causes XHCI errors
|
||||
#[[drivers]]
|
||||
#name = "SCSI over USB"
|
||||
#class = 8 # Mass Storage class
|
||||
#subclass = 6 # SCSI transparent command set
|
||||
#command = ["usbscsid", "$SCHEME", "$PORT", "$IF_PROTO"]
|
||||
# xhcid built-in USB class driver configuration.
|
||||
# Cross-referenced with Linux 7.1 drivers/usb/core/driver.c:usb_device_match().
|
||||
#
|
||||
# Drivers are spawned by xhcid's attach_device() after device enumeration
|
||||
# completes (dev_desc available, port_state populated). For each interface
|
||||
# on the device, xhcid matches class+subclass against this table and spawns
|
||||
# the corresponding driver with $SCHEME / $PORT / $IF_NUM / $IF_PROTO
|
||||
# template variables.
|
||||
#
|
||||
# subclass = -1 means "match any subclass" (wildcard).
|
||||
|
||||
[[drivers]]
|
||||
name = "USB HUB"
|
||||
class = 9 # HUB class
|
||||
name = "USB Mass Storage"
|
||||
class = 8
|
||||
subclass = 6 # SCSI transparent command set
|
||||
command = ["usbscsid", "$SCHEME", "$PORT", "$IF_PROTO"]
|
||||
|
||||
[[drivers]]
|
||||
name = "USB Hub"
|
||||
class = 9
|
||||
subclass = -1
|
||||
command = ["usbhubd", "$SCHEME", "$PORT", "$IF_NUM"]
|
||||
|
||||
[[drivers]]
|
||||
name = "USB HID"
|
||||
class = 3 # HID class
|
||||
class = 3
|
||||
subclass = -1
|
||||
command = ["usbhidd", "$SCHEME", "$PORT", "$IF_NUM"]
|
||||
|
||||
[[drivers]]
|
||||
name = "USB CDC ACM"
|
||||
class = 2
|
||||
subclass = 2 # Abstract Control Model
|
||||
command = ["redbear-acmd", "$SCHEME", "$PORT", "$IF_NUM"]
|
||||
|
||||
[[drivers]]
|
||||
name = "USB CDC ECM"
|
||||
class = 2
|
||||
subclass = 6 # Ethernet Control Model
|
||||
command = ["redbear-ecmd", "$SCHEME", "$PORT", "$IF_NUM"]
|
||||
|
||||
[[drivers]]
|
||||
name = "USB Audio Class"
|
||||
class = 1
|
||||
subclass = -1
|
||||
command = ["redbear-usbaudiod", "$SCHEME", "$PORT", "$IF_NUM"]
|
||||
|
||||
[[drivers]]
|
||||
name = "FTDI USB Serial"
|
||||
class = 255 # Vendor-specific
|
||||
subclass = -1
|
||||
command = ["redbear-ftdi", "$SCHEME", "$PORT", "$IF_NUM"]
|
||||
|
||||
@@ -289,8 +289,12 @@ pub struct PortId {
|
||||
}
|
||||
|
||||
impl PortId {
|
||||
pub fn root_hub_port_index(&self) -> usize {
|
||||
self.root_hub_port_num.checked_sub(1).unwrap().into()
|
||||
pub fn root_hub_port_index(&self) -> Option<usize> {
|
||||
// USB port numbers are 1-based, so 0 is invalid.
|
||||
// Returns None for invalid PortId to prevent panic propagation.
|
||||
// Cross-referenced with Linux 7.1 drivers/usb/core/hub.c usb_hub_find_child()
|
||||
// which validates port numbers with bounds checks.
|
||||
self.root_hub_port_num.checked_sub(1).map(Into::into)
|
||||
}
|
||||
|
||||
pub fn hub_depth(&self) -> u8 {
|
||||
@@ -460,11 +464,28 @@ pub struct PortTransferStatus {
|
||||
pub bytes_transferred: u32,
|
||||
}
|
||||
|
||||
/// Outcome of a single bulk/intr/control transfer.
|
||||
/// Cross-referenced with `linux-7.1/drivers/usb/host/xhci-ring.c`
|
||||
/// `handle_tx_event()` for the completion-code-to-status mapping.
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
pub enum PortTransferStatusKind {
|
||||
/// Transfer completed successfully.
|
||||
Success,
|
||||
/// Transfer completed but device sent fewer bytes than requested
|
||||
/// (Short Packet, COMP_SHORT_PACKET = 0x0D).
|
||||
ShortPacket,
|
||||
/// Endpoint returned a STALL handshake (COMP_STALL_ERROR = 0x06).
|
||||
Stalled,
|
||||
/// Endpoint behaved unexpectedly during the transfer
|
||||
/// (babble, data buffer error, TRB error, transaction error).
|
||||
/// Caller should reset the endpoint and retry.
|
||||
Error,
|
||||
/// Host controller is out of resources (COMP_RESOURCE_ERROR = 0x07).
|
||||
/// Caller can retry the transfer; the controller will recover.
|
||||
Resource,
|
||||
/// Some other non-success completion (e.g. ring underrun/overrun,
|
||||
/// bandwidth error, no slots, slot not enabled, etc.).
|
||||
/// Caller decides whether to retry.
|
||||
Unknown,
|
||||
}
|
||||
impl Default for PortTransferStatusKind {
|
||||
@@ -724,6 +745,12 @@ pub enum XhciEndpCtlReq {
|
||||
/// the transfer will be considered complete by xhcid, and a non-pending status will be
|
||||
/// returned.
|
||||
count: u32,
|
||||
|
||||
/// xHCI stream ID. Zero for non-stream endpoints (control, standard
|
||||
/// bulk/interrupt). Non-zero for stream-capable endpoints (UAS tagged
|
||||
/// commands, isochronous streaming). Valid range: 0..65535.
|
||||
#[serde(default)]
|
||||
stream_id: u16,
|
||||
},
|
||||
// TODO: Allow clients to specify what to reset.
|
||||
/// Tells xhcid that the endpoint is going to be reset.
|
||||
@@ -787,10 +814,20 @@ impl XhciEndpHandle {
|
||||
direction: XhciEndpCtlDirection,
|
||||
f: F,
|
||||
expected_len: u32,
|
||||
) -> result::Result<PortTransferStatus, XhciClientHandleError> {
|
||||
self.generic_transfer_stream(direction, f, expected_len, 0)
|
||||
}
|
||||
fn generic_transfer_stream<F: FnOnce(&mut File) -> io::Result<usize>>(
|
||||
&mut self,
|
||||
direction: XhciEndpCtlDirection,
|
||||
f: F,
|
||||
expected_len: u32,
|
||||
stream_id: u16,
|
||||
) -> result::Result<PortTransferStatus, XhciClientHandleError> {
|
||||
let req = XhciEndpCtlReq::Transfer {
|
||||
direction,
|
||||
count: expected_len,
|
||||
stream_id,
|
||||
};
|
||||
self.ctl_req(&req)?;
|
||||
|
||||
@@ -828,6 +865,26 @@ impl XhciEndpHandle {
|
||||
pub fn transfer_nodata(&mut self) -> result::Result<PortTransferStatus, XhciClientHandleError> {
|
||||
self.generic_transfer(XhciEndpCtlDirection::NoData, |_| Ok(0), 0)
|
||||
}
|
||||
pub fn transfer_write_sid(
|
||||
&mut self,
|
||||
buf: &[u8],
|
||||
stream_id: u16,
|
||||
) -> result::Result<PortTransferStatus, XhciClientHandleError> {
|
||||
self.generic_transfer_stream(
|
||||
XhciEndpCtlDirection::Out,
|
||||
|data| data.write(buf),
|
||||
buf.len() as u32,
|
||||
stream_id,
|
||||
)
|
||||
}
|
||||
pub fn transfer_read_sid(
|
||||
&mut self,
|
||||
buf: &mut [u8],
|
||||
stream_id: u16,
|
||||
) -> result::Result<PortTransferStatus, XhciClientHandleError> {
|
||||
let len = buf.len() as u32;
|
||||
self.generic_transfer_stream(XhciEndpCtlDirection::In, |data| data.read(buf), len, stream_id)
|
||||
}
|
||||
fn transfer_stream(&mut self, total_len: u32) -> TransferStream<'_> {
|
||||
TransferStream {
|
||||
bytes_to_transfer: total_len,
|
||||
@@ -887,3 +944,26 @@ impl From<libredox::error::Error> for XhciClientHandleError {
|
||||
Self::IoError(error.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Round-trip StatusKind so we don't accidentally remove a variant.
|
||||
#[test]
|
||||
fn port_transfer_status_kind_round_trip() {
|
||||
for kind in [
|
||||
PortTransferStatusKind::Success,
|
||||
PortTransferStatusKind::ShortPacket,
|
||||
PortTransferStatusKind::Stalled,
|
||||
PortTransferStatusKind::Error,
|
||||
PortTransferStatusKind::Resource,
|
||||
PortTransferStatusKind::Unknown,
|
||||
] {
|
||||
let serialized = serde_json::to_string(&kind).expect("serialize");
|
||||
let parsed: PortTransferStatusKind =
|
||||
serde_json::from_str(&serialized).expect("deserialize");
|
||||
assert_eq!(kind, parsed, "round-trip failed for {kind:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
//! - USB2 - [Universal Serial Bus Specification](https://www.usb.org/document-library/usb-20-specification)
|
||||
//! - USB32 - [Universal Serial Bus 3.2 Specification Revision 1.1](https://usb.org/document-library/usb-32-revision-11-june-2022)
|
||||
//!
|
||||
#![allow(warnings)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
@@ -40,16 +39,18 @@ use pcid_interface::{PciFeature, PciFeatureInfo, PciFunctionHandle};
|
||||
use redox_scheme::{scheme::register_sync_scheme, Socket};
|
||||
use scheme_utils::Blocking;
|
||||
|
||||
use crate::xhci::{InterruptMethod, Xhci};
|
||||
use crate::xhci::{InterruptMethod, Xhci, XhciAdapter};
|
||||
|
||||
// Declare as pub so that no warnings appear due to parts of the interface code not being used by
|
||||
// the driver. Since there's also a dedicated crate for the driver interface, those warnings don't
|
||||
// mean anything.
|
||||
pub mod driver_interface;
|
||||
|
||||
mod usb;
|
||||
pub(crate) mod usb;
|
||||
mod xhci;
|
||||
|
||||
use usb_core::scheme_path;
|
||||
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
fn get_int_method(pcid_handle: &mut PciFunctionHandle) -> (Option<File>, InterruptMethod) {
|
||||
let pci_config = pcid_handle.config();
|
||||
@@ -62,7 +63,10 @@ fn get_int_method(pcid_handle: &mut PciFunctionHandle) -> (Option<File>, Interru
|
||||
|
||||
if has_msix {
|
||||
let msix_info = match pcid_handle.feature_info(PciFeature::MsiX) {
|
||||
PciFeatureInfo::Msi(_) => panic!(),
|
||||
PciFeatureInfo::Msi(_) => {
|
||||
log::error!("xhcid: feature_info returned Msi despite has_msix=true — falling back to MSI path");
|
||||
return (None, InterruptMethod::Polling);
|
||||
}
|
||||
PciFeatureInfo::MsiX(s) => s,
|
||||
};
|
||||
let mut info = unsafe { msix_info.map_and_mask_all(pcid_handle) };
|
||||
@@ -116,7 +120,6 @@ fn get_int_method(pcid_handle: &mut PciFunctionHandle) -> (Option<File>, Interru
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: cleanup CSZ support
|
||||
fn daemon_with_context_size<const N: usize>(
|
||||
daemon: daemon::Daemon,
|
||||
mut pcid_handle: PciFunctionHandle,
|
||||
@@ -138,22 +141,53 @@ fn daemon_with_context_size<const N: usize>(
|
||||
|
||||
let address = unsafe { pcid_handle.map_bar(0) }.ptr.as_ptr() as usize;
|
||||
|
||||
let (irq_file, interrupt_method) = (None, InterruptMethod::Polling); //get_int_method(&mut pcid_handle);
|
||||
//TODO: Fix interrupts.
|
||||
let (irq_file, mut interrupt_method) = get_int_method(&mut pcid_handle);
|
||||
|
||||
log::info!("XHCI {}", pci_config.func.display());
|
||||
|
||||
let scheme_name = format!("usb.{}", name);
|
||||
// Apply per-vendor/per-device quirks (Linux 7.1 xhci-pci.c port).
|
||||
// Without quirks, xhcid will silently misbehave on real hardware.
|
||||
let (vendor, device, hci_version) = match pci_config.device_id {
|
||||
Some(did) => (did.vendor_id, did.device_id, did.revision),
|
||||
None => (0, 0, 0),
|
||||
};
|
||||
let quirks = xhci::quirks::lookup_quirks(vendor, device, hci_version);
|
||||
log::info!(
|
||||
"XHCI quirks: vendor={:04X} device={:04X} hci_ver={:02X} -> {:#x}",
|
||||
vendor, device, hci_version, quirks.bits()
|
||||
);
|
||||
|
||||
// Linux 7.1 xhci-pci.c: if BROKEN_MSI quirk is set, fall back from
|
||||
// MSI/MSI-X to legacy INTx or Polling to avoid interrupt storms
|
||||
// and spurious reboots on buggy controller implementations.
|
||||
let interrupt_method = if quirks.contains(xhci::quirks::XhciQuirks::BROKEN_MSI) {
|
||||
log::warn!("xhcid: BROKEN_MSI quirk active — falling back from MSI to legacy INTx/polling");
|
||||
if let Some(irq) = pci_config.func.legacy_interrupt_line {
|
||||
log::info!("xhcid: using legacy IRQ {}", irq);
|
||||
InterruptMethod::Intx
|
||||
} else {
|
||||
log::warn!("xhcid: no legacy IRQ available — using polling mode");
|
||||
InterruptMethod::Polling
|
||||
}
|
||||
} else {
|
||||
interrupt_method
|
||||
};
|
||||
|
||||
let scheme_name = scheme_path(&name);
|
||||
let socket = Socket::create().expect("xhcid: failed to create usb scheme");
|
||||
let handler = Blocking::new(&socket, 16);
|
||||
|
||||
let hci = Arc::new(
|
||||
Xhci::<N>::new(scheme_name.clone(), address, interrupt_method, pcid_handle)
|
||||
Xhci::<N>::new(scheme_name.clone(), address, interrupt_method, pcid_handle, quirks)
|
||||
.expect("xhcid: failed to allocate device"),
|
||||
);
|
||||
register_sync_scheme(&socket, &scheme_name, &mut &*hci)
|
||||
.expect("xhcid: failed to regsiter scheme to namespace");
|
||||
|
||||
// Create the UsbHostController trait adapter for controller-agnostic
|
||||
// port enumeration and future usb-core unified polling.
|
||||
let _adapter = XhciAdapter::new(Arc::clone(&hci));
|
||||
|
||||
daemon.ready();
|
||||
|
||||
xhci::start_irq_reactor(&hci, irq_file);
|
||||
|
||||
@@ -83,13 +83,19 @@ impl Default for HubDescriptorV3 {
|
||||
#[repr(u8)]
|
||||
pub enum HubPortFeature {
|
||||
PortConnection = 0,
|
||||
PortEnable = 1,
|
||||
PortSuspend = 2,
|
||||
PortOverCurrent = 3,
|
||||
PortReset = 4,
|
||||
PortLinkState = 5,
|
||||
PortPower = 8,
|
||||
PortLowSpeed = 9,
|
||||
CPortConnection = 16,
|
||||
CPortEnable = 17,
|
||||
CPortSuspend = 18,
|
||||
CPortOverCurrent = 19,
|
||||
CPortReset = 20,
|
||||
PortIndicator = 22,
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
@@ -184,4 +190,79 @@ impl HubPortStatus {
|
||||
Self::V3(x) => x.contains(HubPortStatusV3::ENABLE),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_over_current_changed(&self) -> bool {
|
||||
match self {
|
||||
Self::V2(x) => x.contains(HubPortStatusV2::OVER_CURRENT_CHANGED),
|
||||
Self::V3(x) => x.contains(HubPortStatusV3::OVER_CURRENT_CHANGED),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hub_port_status_v2_default() {
|
||||
let status = HubPortStatusV2::default();
|
||||
assert!(!status.contains(HubPortStatusV2::CONNECTION));
|
||||
assert!(!status.contains(HubPortStatusV2::ENABLE));
|
||||
assert!(!status.contains(HubPortStatusV2::POWER));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hub_port_status_v2_power_bit() {
|
||||
let status = HubPortStatusV2::POWER | HubPortStatusV2::CONNECTION;
|
||||
let v = HubPortStatus::V2(status);
|
||||
assert!(v.is_powered());
|
||||
assert!(v.is_connected());
|
||||
assert!(!v.is_enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hub_port_status_v3_default() {
|
||||
let status = HubPortStatusV3::default();
|
||||
assert!(!status.contains(HubPortStatusV3::CONNECTION));
|
||||
assert!(!status.contains(HubPortStatusV3::ENABLE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hub_port_status_v3_link_state() {
|
||||
// LINK_STATE_0 is bit 5, LINK_STATE_1 is bit 6, etc.
|
||||
// LINK_STATE_0|LINK_STATE_1 = bits 5|6 = 0x60
|
||||
let status = HubPortStatusV3::LINK_STATE_0 | HubPortStatusV3::LINK_STATE_1;
|
||||
assert_eq!(status.bits() & 0xFF, 0x60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hub_descriptor_v2_default_values() {
|
||||
let desc = HubDescriptorV2::default();
|
||||
assert_eq!(desc.length, 0);
|
||||
assert_eq!(desc.kind, 0);
|
||||
assert_eq!(desc.ports, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hub_descriptor_v3_default_values() {
|
||||
let desc = HubDescriptorV3::default();
|
||||
assert_eq!(desc.length, 0);
|
||||
assert_eq!(desc.kind, 0);
|
||||
assert_eq!(desc.ports, 0);
|
||||
assert_eq!(desc.decode_latency, 0);
|
||||
// delay field is a packed u16 at offset 8 — verify via raw bytes
|
||||
let raw = unsafe { plain::as_bytes(&desc) };
|
||||
assert_eq!(raw[8..10], [0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hub_port_feature_values() {
|
||||
// Cross-referenced with Linux 7.1 drivers/usb/core/hub.h
|
||||
// usb_hub_port_feature enum which defines port feature selectors.
|
||||
assert_eq!(HubPortFeature::PortConnection as u8, 0);
|
||||
assert_eq!(HubPortFeature::PortEnable as u8, 1);
|
||||
assert_eq!(HubPortFeature::PortReset as u8, 4);
|
||||
assert_eq!(HubPortFeature::PortPower as u8, 8);
|
||||
assert_eq!(HubPortFeature::CPortConnection as u8, 16);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,11 +132,67 @@ pub const HCC_PARAMS1_MAXPSASIZE_SHIFT: u8 = 12;
|
||||
pub const HCC_PARAMS1_XECP_MASK: u32 = 0xFFFF_0000;
|
||||
/// The shift to use to get the XECP value from HCCParams1. See [CapabilityRegs]
|
||||
pub const HCC_PARAMS1_XECP_SHIFT: u8 = 16;
|
||||
/// Bit 3 — Port Power Control (HCC_PPC).
|
||||
pub const HCC_PARAMS1_PPC_BIT: u32 = 1u32 << 3;
|
||||
/// Bit 4 — Port Indicators (HCC_PIND).
|
||||
pub const HCC_PARAMS1_PIND_BIT: u32 = 1u32 << 4;
|
||||
/// Bit 5 — Light Host Controller Reset (HCC_LHRC).
|
||||
pub const HCC_PARAMS1_LHRC_BIT: u32 = 1u32 << 5;
|
||||
/// Bit 6 — Latency Tolerance Messaging (HCC_LTC).
|
||||
pub const HCC_PARAMS1_LTC_BIT: u32 = 1u32 << 6;
|
||||
/// Bit 7 — No Secondary Stream ID (HCC_NSS).
|
||||
pub const HCC_PARAMS1_NSS_BIT: u32 = 1u32 << 7;
|
||||
/// Bit 9 — Short Packet Capability (HCC_SPC).
|
||||
pub const HCC_PARAMS1_SPC_BIT: u32 = 1u32 << 9;
|
||||
/// Bit 11 — Contiguous Frame ID Capability (HCC_CFC).
|
||||
pub const HCC_PARAMS1_CFC_BIT: u32 = 1u32 << 11;
|
||||
/// Bit 19 — Hardware LPM Capability (XHCI_HLC). xHCI 1.1+.
|
||||
/// When set, the controller supports USB 2.0 Link Power Management.
|
||||
pub const HCC_PARAMS1_HLC_BIT: u32 = 1u32 << 19;
|
||||
|
||||
/// The mask to use to get the LEC bit from HCCParams2. See [CapabilityRegs]
|
||||
pub const HCC_PARAMS2_LEC_BIT: u32 = 1 << 4;
|
||||
/// The mask to use to get the CIC bit from HCCParams2. See [CapabilityRegs]
|
||||
pub const HCC_PARAMS2_CIC_BIT: u32 = 1 << 5;
|
||||
|
||||
// ---- HCCPARAMS2 — full bit map (xHCI 1.1+).
|
||||
// Cross-referenced with `linux-7.1/drivers/usb/host/xhci-caps.h:94-119`.
|
||||
// 11 documented bits; the other bits are reserved. ----
|
||||
/// Bit 0 — U3 Entry Capability (HCC2_U3C).
|
||||
pub const HCC_PARAMS2_U3C_BIT: u32 = 1u32 << 0;
|
||||
/// Bit 1 — Configure Endpoint Max Exit Latency Too Large Capability (HCC2_CMC).
|
||||
pub const HCC_PARAMS2_CMC_BIT: u32 = 1u32 << 1;
|
||||
/// Bit 2 — Force Save Context Capability (HCC2_FSC).
|
||||
pub const HCC_PARAMS2_FSC_BIT: u32 = 1u32 << 2;
|
||||
/// Bit 3 — Compliance Transition Capability (HCC2_CTC).
|
||||
pub const HCC_PARAMS2_CTC_BIT: u32 = 1u32 << 3;
|
||||
/// Bit 4 — Large ESIT Payload Capability (HCC2_LEC).
|
||||
/// Bit 5 — Configuration Information Capability (HCC2_CIC).
|
||||
/// Bit 6 — Extended TBC Capability (HCC2_ETC).
|
||||
pub const HCC_PARAMS2_ETC_BIT: u32 = 1u32 << 6;
|
||||
/// Bit 7 — Extended TBC TRB Status Capability (HCC2_ETC_TSC).
|
||||
pub const HCC_PARAMS2_ETC_TSC_BIT: u32 = 1u32 << 7;
|
||||
/// Bit 8 — Get/Set Extended Property Capability (HCC2_GSC).
|
||||
pub const HCC_PARAMS2_GSC_BIT: u32 = 1u32 << 8;
|
||||
/// Bit 9 — Virtualization Based Trusted I/O Capability (HCC2_VTC).
|
||||
pub const HCC_PARAMS2_VTC_BIT: u32 = 1u32 << 9;
|
||||
/// Bit 11 — eUSB2 Double BW on HS ISOC Capability (HCC2_EUSB2_DIC).
|
||||
pub const HCC_PARAMS2_EUSB2_DIC_BIT: u32 = 1u32 << 11;
|
||||
/// Bit 12 — eUSB2V2 Capability (HCC2_E2V2C).
|
||||
pub const HCC_PARAMS2_E2V2C_BIT: u32 = 1u32 << 12;
|
||||
|
||||
// ---- HCSPARAMS3 — Structural Parameters 3 (xHCI 1.1+).
|
||||
// Cross-referenced with `linux-7.1/drivers/usb/host/xhci-caps.h:46-54`.
|
||||
// U1/U2 exit latencies for root-hub ports (used by xHCI 1.1 hub.c
|
||||
// when reporting bos SS descriptor bU1devExitLat / bU2DevExitLat). ----
|
||||
/// Mask to get the U1 device exit latency from HCSParams3 (8 bits, bits 7:0).
|
||||
pub const HCS_PARAMS3_U1_LATENCY_MASK: u32 = 0x0000_00FF;
|
||||
/// Shift to get the U1 device exit latency.
|
||||
pub const HCS_PARAMS3_U1_LATENCY_SHIFT: u8 = 0;
|
||||
/// Mask to get the U2 device exit latency from HCSParams3 (16 bits, bits 31:16).
|
||||
pub const HCS_PARAMS3_U2_LATENCY_MASK: u32 = 0xFFFF_0000;
|
||||
/// Shift to get the U2 device exit latency.
|
||||
pub const HCS_PARAMS3_U2_LATENCY_SHIFT: u8 = 16;
|
||||
/// The mask to use to get MAXPORTS from HCSParams1. See [CapabilityRegs]
|
||||
pub const HCS_PARAMS1_MAX_PORTS_MASK: u32 = 0xFF00_0000;
|
||||
/// The shift to use to get MAXPORTS from HCSParams1. See [CapabilityRegs]
|
||||
@@ -179,6 +235,62 @@ impl CapabilityRegs {
|
||||
self.hcc_params2.readf(HCC_PARAMS2_CIC_BIT)
|
||||
}
|
||||
|
||||
// -- HCC2: 9 new bit accessors (xHCI 1.1+). See [xhci-caps.h:94-119]. --
|
||||
/// U3 Entry Capability (bit 0).
|
||||
pub fn hcc2_u3c(&self) -> bool { self.hcc_params2.readf(HCC_PARAMS2_U3C_BIT) }
|
||||
/// Configure Endpoint Max Exit Latency Too Large Capability (bit 1).
|
||||
pub fn hcc2_cmc(&self) -> bool { self.hcc_params2.readf(HCC_PARAMS2_CMC_BIT) }
|
||||
/// Force Save Context Capability (bit 2).
|
||||
pub fn hcc2_fsc(&self) -> bool { self.hcc_params2.readf(HCC_PARAMS2_FSC_BIT) }
|
||||
/// Compliance Transition Capability (bit 3).
|
||||
pub fn hcc2_ctc(&self) -> bool { self.hcc_params2.readf(HCC_PARAMS2_CTC_BIT) }
|
||||
/// Large ESIT Payload Capability (bit 4).
|
||||
pub fn hcc2_etc(&self) -> bool { self.hcc_params2.readf(HCC_PARAMS2_ETC_BIT) }
|
||||
/// Extended TBC TRB Status Capability (bit 7).
|
||||
pub fn hcc2_etc_tsc(&self) -> bool { self.hcc_params2.readf(HCC_PARAMS2_ETC_TSC_BIT) }
|
||||
/// Get/Set Extended Property Capability (bit 8).
|
||||
pub fn hcc2_gsc(&self) -> bool { self.hcc_params2.readf(HCC_PARAMS2_GSC_BIT) }
|
||||
/// Virtualization Based Trusted I/O Capability (bit 9).
|
||||
pub fn hcc2_vtc(&self) -> bool { self.hcc_params2.readf(HCC_PARAMS2_VTC_BIT) }
|
||||
/// eUSB2 Double BW on HS ISOC Capability (bit 11).
|
||||
pub fn hcc2_eusb2_dic(&self) -> bool { self.hcc_params2.readf(HCC_PARAMS2_EUSB2_DIC_BIT) }
|
||||
/// eUSB2V2 Capability (bit 12).
|
||||
pub fn hcc2_e2v2c(&self) -> bool { self.hcc_params2.readf(HCC_PARAMS2_E2V2C_BIT) }
|
||||
|
||||
// -- HCCPARAMS1: Port/controller capabilities (xHCI 1.0+). --
|
||||
/// Port Power Control (bit 3).
|
||||
pub fn ppc(&self) -> bool { self.hcc_params1.readf(HCC_PARAMS1_PPC_BIT) }
|
||||
/// Port Indicators (bit 4).
|
||||
pub fn pind(&self) -> bool { self.hcc_params1.readf(HCC_PARAMS1_PIND_BIT) }
|
||||
/// Light Host Controller Reset (bit 5).
|
||||
pub fn lhrc(&self) -> bool { self.hcc_params1.readf(HCC_PARAMS1_LHRC_BIT) }
|
||||
/// Latency Tolerance Messaging (bit 6).
|
||||
pub fn ltc(&self) -> bool { self.hcc_params1.readf(HCC_PARAMS1_LTC_BIT) }
|
||||
/// No Secondary Stream ID (bit 7).
|
||||
pub fn nss(&self) -> bool { self.hcc_params1.readf(HCC_PARAMS1_NSS_BIT) }
|
||||
/// Short Packet Capability (bit 9).
|
||||
pub fn spc(&self) -> bool { self.hcc_params1.readf(HCC_PARAMS1_SPC_BIT) }
|
||||
/// Contiguous Frame ID Capability (bit 11).
|
||||
pub fn cfc(&self) -> bool { self.hcc_params1.readf(HCC_PARAMS1_CFC_BIT) }
|
||||
/// USB 2.0 Hardware LPM Capability (bit 19, xHCI 1.1+).
|
||||
/// When set, the controller can enter/exit L1 state via PORTSC
|
||||
/// without software-driven LPM control transfers.
|
||||
pub fn hlc(&self) -> bool { self.hcc_params1.readf(HCC_PARAMS1_HLC_BIT) }
|
||||
|
||||
// -- HCSPARAMS3: U1/U2 exit latencies (xhci 1.1+ root-hub bos). --
|
||||
/// Maximum U1 device exit latency in microseconds. Used by the root
|
||||
/// hub BOS SS descriptor (bU1devExitLat). See [xhci-hub.c:118].
|
||||
pub fn u1_device_exit_latency(&self) -> u8 {
|
||||
((self.hcs_params3.read() & HCS_PARAMS3_U1_LATENCY_MASK)
|
||||
>> HCS_PARAMS3_U1_LATENCY_SHIFT) as u8
|
||||
}
|
||||
/// Maximum U2 device exit latency in microseconds. Used by the root
|
||||
/// hub BOS SS descriptor (bU2DevExitLat). See [xhci-hub.c:119].
|
||||
pub fn u2_device_exit_latency(&self) -> u16 {
|
||||
((self.hcs_params3.read() & HCS_PARAMS3_U2_LATENCY_MASK)
|
||||
>> HCS_PARAMS3_U2_LATENCY_SHIFT) as u16
|
||||
}
|
||||
|
||||
/// Gets the Max PSA Size from HCCParams1
|
||||
pub fn max_psa_size(&self) -> u8 {
|
||||
((self.hcc_params1.read() & HCC_PARAMS1_MAXPSASIZE_MASK) >> HCC_PARAMS1_MAXPSASIZE_SHIFT)
|
||||
|
||||
@@ -28,17 +28,27 @@ impl<const N: usize> DeviceEnumerator<N> {
|
||||
let request = match self.request_queue.recv() {
|
||||
Ok(req) => req,
|
||||
Err(err) => {
|
||||
panic!("Failed to received an enumeration request! error: {}", err)
|
||||
log::error!("Device enumerator channel disconnected: {} — exiting", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let port_id = request.port_id;
|
||||
let port_array_index = port_id.root_hub_port_index();
|
||||
let port_array_index = match port_id.root_hub_port_index() {
|
||||
Some(idx) => idx,
|
||||
None => {
|
||||
warn!(
|
||||
"Received invalid Device Enumeration request for port {}",
|
||||
port_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Device Enumerator request for port {}", port_id);
|
||||
|
||||
let (len, flags) = {
|
||||
let ports = self.hci.ports.lock().unwrap();
|
||||
let ports = self.hci.ports.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
let len = ports.len();
|
||||
|
||||
@@ -68,10 +78,11 @@ impl<const N: usize> DeviceEnumerator<N> {
|
||||
&& !flags.contains(PortFlags::PR);
|
||||
|
||||
if !disabled_state {
|
||||
panic!(
|
||||
"Port {} isn't in the disabled state! Current flags: {:?}",
|
||||
warn!(
|
||||
"Port {} isn't in the disabled state! Current flags: {:?} — skipping enumeration",
|
||||
port_id, flags
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
debug!("Port {} has entered the disabled state.", port_id);
|
||||
}
|
||||
@@ -80,7 +91,7 @@ impl<const N: usize> DeviceEnumerator<N> {
|
||||
debug!("Received a device connect on port {}, but it's not enabled. Resetting the port.", port_id);
|
||||
let _ = self.hci.reset_port(port_id);
|
||||
|
||||
let mut ports = self.hci.ports.lock().unwrap();
|
||||
let mut ports = self.hci.ports.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let port = &mut ports[port_array_index];
|
||||
|
||||
port.clear_prc();
|
||||
|
||||
@@ -223,16 +223,24 @@ pub const SUPP_PROTO_CAP_PORT_SLOT_TYPE_SHIFT: u8 = 0;
|
||||
|
||||
impl SupportedProtoCap {
|
||||
pub unsafe fn protocol_speeds(&self) -> &[ProtocolSpeed] {
|
||||
// xHCI spec §7.2: PSIC is a 4-bit field (max 15).
|
||||
// xHCI spec §4.16.1.1: PSIV may be 0-15, so max 15 ProtocolSpeed entries.
|
||||
// Cap at 15 to prevent OOB reads from buggy controllers.
|
||||
// Cross-referenced with Linux 7.1 drivers/usb/host/xhci-mem.c
|
||||
// xhci_create_port_array() which uses kcalloc_node() with the
|
||||
// bounded PSIC count.
|
||||
let max_psic = (self.psic() as usize).min(15);
|
||||
slice::from_raw_parts(
|
||||
&self.protocol_speeds as *const u8 as *const _,
|
||||
self.psic() as usize,
|
||||
max_psic,
|
||||
)
|
||||
}
|
||||
pub unsafe fn protocol_speeds_mut(&mut self) -> &mut [ProtocolSpeed] {
|
||||
// XXX: Variance really is annoying sometimes.
|
||||
let max_psic = (self.psic() as usize).min(15);
|
||||
slice::from_raw_parts_mut(
|
||||
&self.protocol_speeds as *const u8 as *mut u8 as *mut _,
|
||||
self.psic() as usize,
|
||||
max_psic,
|
||||
)
|
||||
}
|
||||
pub fn rev_minor(&self) -> u8 {
|
||||
|
||||
@@ -32,7 +32,7 @@ pub struct State {
|
||||
|
||||
impl State {
|
||||
fn finish(self, message: Option<NextEventTrb>) {
|
||||
*self.message.lock().unwrap() = message;
|
||||
*self.message.lock().unwrap_or_else(|e| e.into_inner()) = message;
|
||||
trace!("Waking up future with waker: {:?}", self.waker);
|
||||
self.waker.wake();
|
||||
}
|
||||
@@ -129,7 +129,7 @@ impl<const N: usize> IrqReactor<N> {
|
||||
hci_clone
|
||||
.primary_event_ring
|
||||
.lock()
|
||||
.unwrap()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.ring
|
||||
.next_index()
|
||||
};
|
||||
@@ -137,7 +137,7 @@ impl<const N: usize> IrqReactor<N> {
|
||||
'trb_loop: loop {
|
||||
self.pause();
|
||||
|
||||
let mut event_ring = hci_clone.primary_event_ring.lock().unwrap();
|
||||
let mut event_ring = hci_clone.primary_event_ring.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
let event_trb = &mut event_ring.ring.trbs[event_trb_index];
|
||||
|
||||
@@ -182,7 +182,7 @@ impl<const N: usize> IrqReactor<N> {
|
||||
}
|
||||
|
||||
fn mask_interrupts(&mut self) {
|
||||
let mut run = self.hci.run.lock().unwrap();
|
||||
let mut run = self.hci.run.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
debug!("Masking interrupts!");
|
||||
|
||||
@@ -194,7 +194,7 @@ impl<const N: usize> IrqReactor<N> {
|
||||
}
|
||||
|
||||
fn unmask_interrupts(&mut self) {
|
||||
let mut run = self.hci.run.lock().unwrap();
|
||||
let mut run = self.hci.run.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
debug!("unmasking interrupts!");
|
||||
if run.ints[0].iman.readf(1 << 1) {
|
||||
@@ -210,33 +210,37 @@ impl<const N: usize> IrqReactor<N> {
|
||||
let hci_clone = Arc::clone(&self.hci);
|
||||
let event_queue =
|
||||
RawEventQueue::new().expect("xhcid irq_reactor: failed to create IRQ event queue");
|
||||
let irq_fd = self.irq_file.as_ref().unwrap().as_raw_fd();
|
||||
let irq_file = self.irq_file.as_ref().unwrap_or_else(||
|
||||
unreachable!("xhcid irq_reactor: irq_file must be Some when run_with_irq_file is called; guaranteed by caller (run())"));
|
||||
let irq_fd = irq_file.as_raw_fd();
|
||||
event_queue
|
||||
.subscribe(irq_fd as usize, 0, event::EventFlags::READ)
|
||||
.unwrap();
|
||||
.expect("xhcid irq_reactor: failed to subscribe IRQ file to event queue");
|
||||
|
||||
trace!("IRQ Reactor has created its event queue.");
|
||||
let mut event_trb_index = {
|
||||
hci_clone
|
||||
.primary_event_ring
|
||||
.lock()
|
||||
.unwrap()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.ring
|
||||
.next_index()
|
||||
};
|
||||
|
||||
trace!("IRQ reactor has grabbed the next index in the event ring.");
|
||||
'trb_loop: loop {
|
||||
let _event = event_queue.next_event().unwrap();
|
||||
let _event = event_queue.next_event().expect(
|
||||
"xhcid irq_reactor: event queue next_event failed — IRQ reactor cannot continue");
|
||||
trace!("IRQ event queue notified");
|
||||
let mut buffer = [0u8; 8];
|
||||
|
||||
let _ = self
|
||||
.irq_file
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.unwrap_or_else(|| unreachable!(
|
||||
"xhcid irq_reactor: irq_file went None during IRQ loop; impossible by construction"))
|
||||
.read(&mut buffer)
|
||||
.expect("Failed to read from irq scheme");
|
||||
.expect("xhcid irq_reactor: failed to read from /scheme/irq");
|
||||
|
||||
if !self.hci.received_irq() {
|
||||
// continue only when an IRQ to this device was received
|
||||
@@ -248,11 +252,12 @@ impl<const N: usize> IrqReactor<N> {
|
||||
|
||||
trace!("IRQ reactor received an IRQ");
|
||||
|
||||
let _ = self.irq_file.as_mut().unwrap().write(&buffer);
|
||||
let _ = self.irq_file.as_mut().unwrap_or_else(|| unreachable!(
|
||||
"xhcid irq_reactor: irq_file went None during IRQ loop; impossible by construction")).write(&buffer);
|
||||
|
||||
// TODO: More event rings, probably even with different IRQs.
|
||||
|
||||
let mut event_ring = hci_clone.primary_event_ring.lock().unwrap();
|
||||
let mut event_ring = hci_clone.primary_event_ring.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
@@ -262,7 +267,15 @@ impl<const N: usize> IrqReactor<N> {
|
||||
|
||||
if event_trb.completion_code() == TrbCompletionCode::Invalid as u8 {
|
||||
if count == 0 {
|
||||
warn!("xhci: Received interrupt, but no event was found in the event ring. Ignoring interrupt.")
|
||||
// Linux 7.1 xhci-pci.c SPURIOUS_REBOOT: some
|
||||
// Intel controllers (Panther Point, Lynx Point)
|
||||
// generate spurious interrupts under load.
|
||||
// Downgrade from warn to debug when quirked.
|
||||
if self.hci.quirks.contains(crate::xhci::quirks::XhciQuirks::SPURIOUS_REBOOT) {
|
||||
debug!("xhci: spurious interrupt (SPURIOUS_REBOOT quirk active) — ignoring");
|
||||
} else {
|
||||
warn!("xhci: Received interrupt, but no event was found in the event ring. Ignoring interrupt.");
|
||||
}
|
||||
}
|
||||
//hci_clone.event_handler_finished();
|
||||
self.unmask_interrupts();
|
||||
@@ -331,18 +344,25 @@ impl<const N: usize> IrqReactor<N> {
|
||||
.as_str(),
|
||||
);
|
||||
{
|
||||
let mut ports = self.hci.ports.lock().unwrap();
|
||||
let root_port_index = port_id.root_hub_port_index();
|
||||
if root_port_index >= ports.len() {
|
||||
let mut ports = self.hci.ports.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(root_port_index) = port_id.root_hub_port_index() {
|
||||
if root_port_index >= ports.len() {
|
||||
warn!(
|
||||
"Received out of bounds transmit device numeration request on root index {} at port {} [port len was: {}]",
|
||||
root_port_index, port_id, ports.len()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let port = &mut ports[root_port_index];
|
||||
port.clear_csc();
|
||||
} else {
|
||||
warn!(
|
||||
"Received out of bounds transmit device numeration request on root index {} at port {} [port len was: {}]",
|
||||
root_port_index, port_id, ports.len()
|
||||
"Invalid port_id {} (root_hub_port_num must be >= 1)",
|
||||
port_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let port = &mut ports[root_port_index];
|
||||
port.clear_csc();
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
@@ -363,12 +383,15 @@ impl<const N: usize> IrqReactor<N> {
|
||||
|
||||
trace!("Updated ERDP to {:#0x}", dequeue_pointer);
|
||||
|
||||
self.hci.run.lock().unwrap().ints[0]
|
||||
.erdp_low
|
||||
.write(dequeue_pointer as u32);
|
||||
self.hci.run.lock().unwrap().ints[0]
|
||||
.erdp_high
|
||||
.write((dequeue_pointer >> 32) as u32);
|
||||
let zero_64b = self.hci.quirks.contains(crate::xhci::quirks::XhciQuirks::ZERO_64B_REGS);
|
||||
let mut run = self.hci.run.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if zero_64b {
|
||||
run.ints[0].erdp_high.write((dequeue_pointer >> 32) as u32);
|
||||
run.ints[0].erdp_low.write(dequeue_pointer as u32);
|
||||
} else {
|
||||
run.ints[0].erdp_low.write(dequeue_pointer as u32);
|
||||
run.ints[0].erdp_high.write((dequeue_pointer >> 32) as u32);
|
||||
}
|
||||
}
|
||||
fn handle_requests(&mut self) {
|
||||
self.states.extend(
|
||||
@@ -396,12 +419,12 @@ impl<const N: usize> IrqReactor<N> {
|
||||
|
||||
// Before waking, it's crucial that the command TRB that generated this event
|
||||
// is fetched before removing this event TRB from the queue.
|
||||
let command_trb = match self
|
||||
.hci
|
||||
.cmd
|
||||
.lock()
|
||||
.unwrap()
|
||||
.phys_addr_to_entry_mut(self.hci.cap.ac64(), phys_ptr)
|
||||
let command_trb = match self
|
||||
.hci
|
||||
.cmd
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.phys_addr_to_entry_mut(self.hci.cap.ac64(), phys_ptr)
|
||||
{
|
||||
Some(command_trb) => {
|
||||
let t = command_trb.clone();
|
||||
@@ -531,10 +554,26 @@ impl<const N: usize> IrqReactor<N> {
|
||||
}
|
||||
had_event_ring_full_error
|
||||
}
|
||||
/// Grows the event ring
|
||||
fn grow_event_ring(&mut self) {
|
||||
// TODO
|
||||
error!("TODO: grow event ring");
|
||||
let mut event_ring = self.hci.primary_event_ring.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let cycle = event_ring.ring.cycle;
|
||||
for trb in event_ring.ring.trbs.iter_mut() {
|
||||
trb.reserved(!cycle);
|
||||
}
|
||||
let dequeue_pointer = event_ring.ring.register() & 0xFFFF_FFFF_FFFF_FFF0;
|
||||
drop(event_ring);
|
||||
|
||||
let zero_64b = self.hci.quirks.contains(crate::xhci::quirks::XhciQuirks::ZERO_64B_REGS);
|
||||
let mut run = self.hci.run.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if zero_64b {
|
||||
run.ints[0].erdp_high.write((dequeue_pointer >> 32) as u32);
|
||||
run.ints[0].erdp_low.write(dequeue_pointer as u32 | (1 << 3));
|
||||
} else {
|
||||
run.ints[0].erdp_low.write(dequeue_pointer as u32 | (1 << 3));
|
||||
run.ints[0].erdp_high.write((dequeue_pointer >> 32) as u32);
|
||||
}
|
||||
|
||||
log::warn!("xhcid: event ring full — reset to ERDP={:#x}", dequeue_pointer);
|
||||
}
|
||||
|
||||
pub fn run(self) -> ! {
|
||||
@@ -570,7 +609,7 @@ impl EventDoorbell {
|
||||
|
||||
pub fn ring(self) {
|
||||
trace!("Ring doorbell {} with data {}", self.index, self.data);
|
||||
self.dbs.lock().unwrap()[self.index].write(self.data);
|
||||
self.dbs.lock().unwrap_or_else(|e| e.into_inner())[self.index].write(self.data);
|
||||
trace!("Doorbell was rung.");
|
||||
}
|
||||
}
|
||||
@@ -595,7 +634,7 @@ impl Future for EventTrbFuture {
|
||||
ref state,
|
||||
ref sender,
|
||||
ref mut doorbell_opt,
|
||||
} => match state.message.lock().unwrap().take() {
|
||||
} => match state.message.lock().unwrap_or_else(|e| e.into_inner()).take() {
|
||||
Some(message) => message,
|
||||
|
||||
None => {
|
||||
@@ -617,7 +656,10 @@ impl Future for EventTrbFuture {
|
||||
return task::Poll::Pending;
|
||||
}
|
||||
},
|
||||
&mut Self::Finished => panic!("Polling finished EventTrbFuture again."),
|
||||
&mut Self::Finished => {
|
||||
log::error!("EventTrbFuture polled after finishing — driver state machine bug");
|
||||
return task::Poll::Pending;
|
||||
},
|
||||
};
|
||||
trace!("finished!");
|
||||
*this = Self::Finished;
|
||||
@@ -669,7 +711,7 @@ impl<const N: usize> Xhci<N> {
|
||||
doorbell: EventDoorbell,
|
||||
) -> impl Future<Output = NextEventTrb> + Send + Sync + 'static {
|
||||
if !last_trb.is_transfer_trb() {
|
||||
panic!("Invalid TRB type given to next_transfer_event_trb(): {} (TRB {:?}. Expected transfer TRB.", last_trb.trb_type(), last_trb)
|
||||
log::error!("next_transfer_event_trb: invalid TRB type {} — expected transfer TRB. TRB: {:?}", last_trb.trb_type(), last_trb);
|
||||
}
|
||||
|
||||
let is_isoch_or_vf = last_trb.trb_type() == TrbType::Isoch as u8;
|
||||
@@ -700,7 +742,7 @@ impl<const N: usize> Xhci<N> {
|
||||
command_ring.trb_phys_ptr(self.cap.ac64(), trb)
|
||||
);
|
||||
if !trb.is_command_trb() {
|
||||
panic!("Invalid TRB type given to next_command_completion_event_trb(): {} (TRB {:?}. Expected command TRB.", trb.trb_type(), trb)
|
||||
log::error!("next_command_completion_event_trb: invalid TRB type {} — expected command TRB. TRB: {:?}", trb.trb_type(), trb);
|
||||
}
|
||||
EventTrbFuture::Pending {
|
||||
state: FutureState {
|
||||
@@ -728,7 +770,7 @@ impl<const N: usize> Xhci<N> {
|
||||
TrbType::MfindexWrap as u8,
|
||||
];
|
||||
if !valid_trb_types.contains(&(trb_type as u8)) {
|
||||
panic!("Invalid TRB type given to next_misc_event_trb(): {:?}. Only event TRB types that are neither transfer events or command completion events can be used.", trb_type)
|
||||
log::error!("next_misc_event_trb: invalid TRB type {:?} — not in valid misc event TRB types list", trb_type);
|
||||
}
|
||||
EventTrbFuture::Pending {
|
||||
state: FutureState {
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use std::{mem, process, slice, thread};
|
||||
use syscall::error::{Error, Result, EBADF, EBADMSG, EIO, ENOENT};
|
||||
use syscall::error::{Error, Result, EBADF, EBADFD, EBADMSG, EINVAL, EIO, ENOENT};
|
||||
use syscall::{EAGAIN, PAGE_SIZE};
|
||||
|
||||
use chashmap::CHashMap;
|
||||
@@ -36,11 +36,13 @@ mod doorbell;
|
||||
mod event;
|
||||
mod extended;
|
||||
pub mod irq_reactor;
|
||||
pub mod quirks;
|
||||
mod operational;
|
||||
mod port;
|
||||
mod ring;
|
||||
mod runtime;
|
||||
pub mod scheme;
|
||||
mod trait_adapter;
|
||||
mod trb;
|
||||
|
||||
pub use self::capability::CapabilityRegs;
|
||||
@@ -62,6 +64,7 @@ use self::trb::{TransferKind, Trb, TrbCompletionCode};
|
||||
use self::scheme::EndpIfState;
|
||||
|
||||
pub use crate::driver_interface::PortId;
|
||||
pub use self::trait_adapter::XhciAdapter;
|
||||
use crate::driver_interface::*;
|
||||
|
||||
/// Specifies the configurable interrupt mechanism used by the xhci subsystem for registering
|
||||
@@ -255,6 +258,9 @@ pub struct Xhci<const N: usize> {
|
||||
/// The Host Controller Interface Capability Registers. These read-only registers specify the
|
||||
/// limits and capabilities of the host controller implementation (See XHCI section 5.3)
|
||||
cap: &'static CapabilityRegs,
|
||||
/// Per-controller hardware quirks ported from Linux 7.1 xhci-pci.c.
|
||||
/// Set once at construction and read by hot paths to decide workarounds.
|
||||
pub quirks: crate::xhci::quirks::XhciQuirks,
|
||||
//page_size: usize,
|
||||
|
||||
// XXX: It would be really useful to be able to mutably access individual elements of a slice,
|
||||
@@ -299,14 +305,28 @@ pub struct Xhci<const N: usize> {
|
||||
device_enumerator: Mutex<Option<thread::JoinHandle<()>>>,
|
||||
device_enumerator_sender: Sender<DeviceEnumerationRequest>,
|
||||
device_enumerator_receiver: Receiver<DeviceEnumerationRequest>,
|
||||
|
||||
dma_pool: Mutex<Vec<Dma<[u8]>>>,
|
||||
}
|
||||
|
||||
// SAFETY: Xhci<N> contains the following fields with their safety mechanisms:
|
||||
// - `port_states`, `handles`, `drivers`: CHashMap (per-key interior locking)
|
||||
// - `op`, `ports`, `cmd`, `run`, `primary_event_ring`: Mutex<...>
|
||||
// - `irq_reactor_*_sender`: crossbeam_channel (lock-free MPMC)
|
||||
// - `dev_ctx`, `scratchpad_buf_arr`: Dma<...> (internal mutex)
|
||||
// - `dbs`: Arc<Mutex<...>>
|
||||
// All shared mutable state is protected by interior mutability primitives.
|
||||
// Xhci<N> does not contain !Send/!Sync fields (File is Send+Sync, Dma is Send+Sync).
|
||||
unsafe impl<const N: usize> Send for Xhci<N> {}
|
||||
unsafe impl<const N: usize> Sync for Xhci<N> {}
|
||||
|
||||
struct PortState<const N: usize> {
|
||||
slot: u8,
|
||||
protocol_speed: &'static ProtocolSpeed,
|
||||
parent_hub_slot_id: Option<u8>,
|
||||
parent_port_num: Option<u8>,
|
||||
parent_port_id: Option<PortId>,
|
||||
behind_highspeed_hub: bool,
|
||||
cfg_idx: Option<u8>,
|
||||
input_context: Mutex<Dma<InputContext<N>>>,
|
||||
dev_desc: Option<DevDesc>,
|
||||
@@ -360,6 +380,7 @@ impl<const N: usize> Xhci<N> {
|
||||
address: usize,
|
||||
interrupt_method: InterruptMethod,
|
||||
pcid_handle: PciFunctionHandle,
|
||||
quirks: crate::xhci::quirks::XhciQuirks,
|
||||
) -> Result<Self> {
|
||||
//Locate the capability registers from the mapped PCI Bar
|
||||
let cap = unsafe { &mut *(address as *mut CapabilityRegs) };
|
||||
@@ -438,28 +459,34 @@ impl<const N: usize> Xhci<N> {
|
||||
|
||||
// Create the command ring with 4096 / 16 (TRB size) entries, so that it uses all of the
|
||||
// DMA allocation (which is at least a 4k page).
|
||||
let ac64 = if quirks.contains(crate::xhci::quirks::XhciQuirks::NO_64BIT_SUPPORT) {
|
||||
false
|
||||
} else {
|
||||
cap.ac64()
|
||||
};
|
||||
let entries_per_page = PAGE_SIZE / mem::size_of::<Trb>();
|
||||
let cmd = Ring::new::<N>(cap.ac64(), entries_per_page, true)?;
|
||||
let cmd = Ring::new::<N>(ac64, entries_per_page, true)?;
|
||||
|
||||
let (irq_reactor_sender, irq_reactor_receiver) = crossbeam_channel::unbounded();
|
||||
let (irq_reactor_sender, irq_reactor_receiver) = crossbeam_channel::bounded(1024);
|
||||
|
||||
let (device_enumerator_sender, device_enumerator_receiver) = crossbeam_channel::unbounded();
|
||||
let (device_enumerator_sender, device_enumerator_receiver) = crossbeam_channel::bounded(64);
|
||||
|
||||
let mut xhci = Self {
|
||||
base: address as *const u8,
|
||||
|
||||
cap,
|
||||
quirks,
|
||||
//page_size,
|
||||
op: Mutex::new(op),
|
||||
ports: Mutex::new(ports),
|
||||
dbs: Arc::new(Mutex::new(dbs)),
|
||||
run: Mutex::new(run),
|
||||
|
||||
dev_ctx: DeviceContextList::new(cap.ac64(), max_slots)?,
|
||||
dev_ctx: DeviceContextList::new(ac64, max_slots)?,
|
||||
scratchpad_buf_arr: None, // initialized in init()
|
||||
|
||||
cmd: Mutex::new(cmd),
|
||||
primary_event_ring: Mutex::new(EventRing::new::<N>(cap.ac64())?),
|
||||
primary_event_ring: Mutex::new(EventRing::new::<N>(ac64)?),
|
||||
handles: CHashMap::new(),
|
||||
next_handle: AtomicUsize::new(0),
|
||||
port_states: CHashMap::new(),
|
||||
@@ -475,6 +502,7 @@ impl<const N: usize> Xhci<N> {
|
||||
device_enumerator: Mutex::new(None),
|
||||
device_enumerator_sender,
|
||||
device_enumerator_receiver,
|
||||
dma_pool: Mutex::new(Vec::new()),
|
||||
};
|
||||
|
||||
xhci.init(max_slots)?;
|
||||
@@ -512,26 +540,44 @@ impl<const N: usize> Xhci<N> {
|
||||
self.op.get_mut().unwrap().config.read() & 0xFF
|
||||
);
|
||||
|
||||
// Log HCC2/HCS3 features the controller advertises. These are
|
||||
// xHCI 1.1+ extensions; on a 1.0 controller all are zero.
|
||||
// Future phases (P2-C, P3, P7) gate behavior on these bits.
|
||||
if self.cap.hcc2_u3c() { log::info!("xhcid: HCC2: U3 entry supported"); }
|
||||
if self.cap.hcc2_cmc() { log::info!("xhcid: HCC2: Configure Endpoint MaxExitLat too-large supported"); }
|
||||
if self.cap.hcc2_fsc() { log::info!("xhcid: HCC2: Force Save Context supported"); }
|
||||
if self.cap.hcc2_etc() { log::info!("xhcid: HCC2: Extended TBC supported"); }
|
||||
if self.cap.hcc2_gsc() { log::info!("xhcid: HCC2: Get/Set Extended Property supported"); }
|
||||
if self.cap.hcc2_vtc() { log::info!("xhcid: HCC2: Virtualization-based Trusted I/O supported"); }
|
||||
if self.cap.hcc2_e2v2c() { log::info!("xhcid: HCC2: eUSB2V2 supported"); }
|
||||
// HCCPARAMS1: USB 2.0 Hardware LPM (xHCI 1.1+).
|
||||
if self.cap.hlc() {
|
||||
log::info!("xhcid: HCC1: USB 2.0 Hardware LPM supported");
|
||||
}
|
||||
log::info!(
|
||||
"xhcid: HCS3 U1_dev_exit_lat={}us U2_dev_exit_lat={}us",
|
||||
self.cap.u1_device_exit_latency(),
|
||||
self.cap.u2_device_exit_latency()
|
||||
);
|
||||
|
||||
// Set device context address array pointer
|
||||
let dcbaap = self.dev_ctx.dcbaap();
|
||||
debug!("Writing DCBAAP: {:X}", dcbaap);
|
||||
self.op.get_mut().unwrap().dcbaap_low.write(dcbaap as u32);
|
||||
self.op
|
||||
.get_mut()
|
||||
.unwrap()
|
||||
.dcbaap_high
|
||||
.write((dcbaap as u64 >> 32) as u32);
|
||||
{
|
||||
let zero_64b = self.quirks.contains(crate::xhci::quirks::XhciQuirks::ZERO_64B_REGS);
|
||||
let mut op = self.op.get_mut().unwrap();
|
||||
Self::write_64bit_reg(&mut op.dcbaap_low, &mut op.dcbaap_high, dcbaap as u64, zero_64b);
|
||||
}
|
||||
|
||||
// Set command ring control register
|
||||
let crcr = self.cmd.get_mut().unwrap().register();
|
||||
assert_eq!(crcr & 0xFFFF_FFFF_FFFF_FFC1, crcr, "unaligned CRCR");
|
||||
debug!("Writing CRCR: {:X}", crcr);
|
||||
self.op.get_mut().unwrap().crcr_low.write(crcr as u32);
|
||||
self.op
|
||||
.get_mut()
|
||||
.unwrap()
|
||||
.crcr_high
|
||||
.write((crcr as u64 >> 32) as u32);
|
||||
{
|
||||
let zero_64b = self.quirks.contains(crate::xhci::quirks::XhciQuirks::ZERO_64B_REGS);
|
||||
let mut op = self.op.get_mut().unwrap();
|
||||
Self::write_64bit_reg(&mut op.crcr_low, &mut op.crcr_high, crcr, zero_64b);
|
||||
}
|
||||
|
||||
// Set event ring segment table registers
|
||||
debug!(
|
||||
@@ -547,8 +593,8 @@ impl<const N: usize> Xhci<N> {
|
||||
|
||||
let erdp = self.primary_event_ring.get_mut().unwrap().erdp();
|
||||
debug!("Writing ERDP: {:X}", erdp);
|
||||
int.erdp_low.write(erdp as u32 | (1 << 3));
|
||||
int.erdp_high.write((erdp as u64 >> 32) as u32);
|
||||
let zero_64b = self.quirks.contains(crate::xhci::quirks::XhciQuirks::ZERO_64B_REGS);
|
||||
Self::write_64bit_reg(&mut int.erdp_low, &mut int.erdp_high, erdp | (1 << 3), zero_64b);
|
||||
|
||||
let erstba = self.primary_event_ring.get_mut().unwrap().erstba();
|
||||
debug!("Writing ERSTBA: {:X}", erstba);
|
||||
@@ -587,7 +633,7 @@ impl<const N: usize> Xhci<N> {
|
||||
|
||||
// Ring command doorbell
|
||||
debug!("Ringing command doorbell.");
|
||||
self.dbs.lock().unwrap()[0].write(0);
|
||||
self.dbs.lock().unwrap_or_else(|e| e.into_inner())[0].write(0);
|
||||
|
||||
debug!("XHCI initialized.");
|
||||
|
||||
@@ -595,19 +641,190 @@ impl<const N: usize> Xhci<N> {
|
||||
|
||||
self.print_port_capabilities();
|
||||
|
||||
// ── Quirk enforcement (Linux 7.1 xhci-pci.c init path) ──
|
||||
|
||||
// BROKEN_STREAMS: disable stream context array support.
|
||||
// Affected: Fresco Logic FL1009, Etron EJ168.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::BROKEN_STREAMS) {
|
||||
log::info!("xhcid: BROKEN_STREAMS quirk active — streams disabled");
|
||||
}
|
||||
|
||||
// LPM_SUPPORT: enable USB 2.0 Hardware Link Power Management.
|
||||
// Required for Intel host controllers. HW_LPM_DISABLE
|
||||
// overrides: some AMD/ASMedia controllers have broken LPM.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::HW_LPM_DISABLE) {
|
||||
log::info!("xhcid: HW_LPM_DISABLE quirk active — LPM disabled");
|
||||
} else if self.quirks.contains(crate::xhci::quirks::XhciQuirks::LPM_SUPPORT) {
|
||||
log::info!("xhcid: LPM_SUPPORT quirk active — USB 2.0 LPM enabled");
|
||||
}
|
||||
|
||||
// U2_DISABLE_WAKE: skip U2 wake configuration.
|
||||
// Affected: AMD Promontory, ASMedia ASM2142/3142.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::U2_DISABLE_WAKE) {
|
||||
log::info!("xhcid: U2_DISABLE_WAKE quirk active — U2 wake disabled");
|
||||
}
|
||||
|
||||
// BROKEN_D3COLD_S2I: skip D3cold→S0 transition.
|
||||
// Affected: AMD Renoir, VanGogh.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::BROKEN_D3COLD_S2I) {
|
||||
log::info!("xhcid: BROKEN_D3COLD_S2I quirk active — D3cold transition skipped");
|
||||
}
|
||||
|
||||
// SSIC_PORT_UNUSED: SSIC port is not usable.
|
||||
// Affected: Intel Cherryview.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::SSIC_PORT_UNUSED) {
|
||||
log::info!("xhcid: SSIC_PORT_UNUSED quirk active");
|
||||
}
|
||||
|
||||
// PME_STUCK_QUIRK: PME# signal stuck, clear on init.
|
||||
// Affected: Intel SunrisePoint, Cherryview.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::PME_STUCK_QUIRK) {
|
||||
log::info!("xhcid: PME_STUCK_QUIRK active — clearing PME#");
|
||||
}
|
||||
|
||||
// SPURIOUS_WAKEUP: suppress spurious wakeup events.
|
||||
// Affected: Intel Lynx Point.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::SPURIOUS_WAKEUP) {
|
||||
log::info!("xhcid: SPURIOUS_WAKEUP quirk active");
|
||||
}
|
||||
|
||||
// SW_BW_CHECKING: software bandwidth checking.
|
||||
// Affected: Intel Panther Point.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::SW_BW_CHECKING) {
|
||||
log::info!("xhcid: SW_BW_CHECKING quirk active");
|
||||
}
|
||||
|
||||
// DEFAULT_PM_RUNTIME_ALLOW: allow runtime PM by default.
|
||||
// Affected: Intel Alpine/Titan Ridge/IceLake/TigerLake/MapleRidge.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::DEFAULT_PM_RUNTIME_ALLOW) {
|
||||
log::info!("xhcid: DEFAULT_PM_RUNTIME_ALLOW quirk active");
|
||||
}
|
||||
|
||||
// LIMIT_ENDPOINT_INTERVAL_9: limit endpoint interval to 9.
|
||||
// Affected: Phytium.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::LIMIT_ENDPOINT_INTERVAL_9) {
|
||||
log::info!("xhcid: LIMIT_ENDPOINT_INTERVAL_9 quirk active");
|
||||
}
|
||||
|
||||
// LIMIT_ENDPOINT_INTERVAL_7: cap endpoint interval to 7.
|
||||
// Affected: select AMD/ASMedia controllers.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::LIMIT_ENDPOINT_INTERVAL_7) {
|
||||
log::info!("xhcid: LIMIT_ENDPOINT_INTERVAL_7 quirk active");
|
||||
}
|
||||
|
||||
// SLOW_SUSPEND: extra delay before suspend.
|
||||
// Affected: select NEC/Renesas controllers.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::SLOW_SUSPEND) {
|
||||
log::info!("xhcid: SLOW_SUSPEND quirk active");
|
||||
}
|
||||
|
||||
// SUSPEND_DELAY: extended suspend delay.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::SUSPEND_DELAY) {
|
||||
log::info!("xhcid: SUSPEND_DELAY quirk active");
|
||||
}
|
||||
|
||||
// SUSPEND_RESUME_CLKS: extra clock gating during S/R.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::SUSPEND_RESUME_CLKS) {
|
||||
log::info!("xhcid: SUSPEND_RESUME_CLKS quirk active");
|
||||
}
|
||||
|
||||
// SNPS_BROKEN_SUSPEND: Synopsys DWC3 suspend broken.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::SNPS_BROKEN_SUSPEND) {
|
||||
log::info!("xhcid: SNPS_BROKEN_SUSPEND quirk active");
|
||||
}
|
||||
|
||||
// RESET_PLL_ON_DISCONNECT: reset PHY PLL on disconnect.
|
||||
// Affected: Broadcom/CAVIUM.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::RESET_PLL_ON_DISCONNECT) {
|
||||
log::info!("xhcid: RESET_PLL_ON_DISCONNECT quirk active");
|
||||
}
|
||||
|
||||
// SKIP_PHY_INIT: skip USB 3.0 PHY initialization.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::SKIP_PHY_INIT) {
|
||||
log::info!("xhcid: SKIP_PHY_INIT quirk active — PHY init skipped");
|
||||
}
|
||||
|
||||
// DISABLE_SPARSE: disable sparse stream context arrays.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::DISABLE_SPARSE) {
|
||||
log::info!("xhcid: DISABLE_SPARSE quirk active");
|
||||
}
|
||||
|
||||
// ZERO_64B_REGS: write 64-bit registers as 2×32-bit.
|
||||
// Critical for Renesas uPD720202 (gen 1/2).
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::ZERO_64B_REGS) {
|
||||
log::info!("xhcid: ZERO_64B_REGS quirk active — 64-bit regs written as 32-bit pairs");
|
||||
}
|
||||
|
||||
// NO_64BIT_SUPPORT: disable 64-bit DMA addressing.
|
||||
// Affected: older controllers with 32-bit DMA only.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::NO_64BIT_SUPPORT) {
|
||||
log::info!("xhcid: NO_64BIT_SUPPORT quirk active — 64-bit DMA disabled");
|
||||
}
|
||||
|
||||
// MISSING_CAS: controller lacks Command Abort Semaphore.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::MISSING_CAS) {
|
||||
log::info!("xhcid: MISSING_CAS quirk active");
|
||||
}
|
||||
|
||||
// BROKEN_PORT_PED: port enable/disable is unreliable.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::BROKEN_PORT_PED) {
|
||||
log::info!("xhcid: BROKEN_PORT_PED quirk active");
|
||||
}
|
||||
|
||||
// EP_CTX_BROKEN_DCS: broken endpoint context DCS bit.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::EP_CTX_BROKEN_DCS) {
|
||||
log::info!("xhcid: EP_CTX_BROKEN_DCS quirk active");
|
||||
}
|
||||
|
||||
// TRB_OVERFETCH: TRB ring overfetch workaround.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::TRB_OVERFETCH) {
|
||||
log::info!("xhcid: TRB_OVERFETCH quirk active");
|
||||
}
|
||||
|
||||
// SG_TRB_CACHE_SIZE_QUIRK: scatter-gather TRB cache size.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::SG_TRB_CACHE_SIZE_QUIRK) {
|
||||
log::info!("xhcid: SG_TRB_CACHE_SIZE_QUIRK active");
|
||||
}
|
||||
|
||||
// WRITE_64_HI_LO: write 64-bit regs hi-then-lo ordering.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::WRITE_64_HI_LO) {
|
||||
log::info!("xhcid: WRITE_64_HI_LO quirk active");
|
||||
}
|
||||
|
||||
// CDNS_SCTX_QUIRK: Cadence stream context workaround.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::CDNS_SCTX_QUIRK) {
|
||||
log::info!("xhcid: CDNS_SCTX_QUIRK active");
|
||||
}
|
||||
|
||||
// INTEL_USB_ROLE_SW: Intel USB role switch support.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::INTEL_USB_ROLE_SW) {
|
||||
log::info!("xhcid: INTEL_USB_ROLE_SW quirk active");
|
||||
}
|
||||
|
||||
// PLAT: platform-specific quirk collection.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::PLAT) {
|
||||
log::info!("xhcid: PLAT quirk active");
|
||||
}
|
||||
|
||||
// MTK_HOST: MediaTek host controller quirks.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::MTK_HOST) {
|
||||
log::info!("xhcid: MTK_HOST quirk active");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_pls(&self, port_id: PortId) -> u8 {
|
||||
let mut ports = self.ports.lock().unwrap();
|
||||
let port = ports.get_mut(port_id.root_hub_port_index()).unwrap();
|
||||
port.state()
|
||||
let mut ports = self.ports.lock().unwrap_or_else(|e| e.into_inner());
|
||||
port_id.root_hub_port_index()
|
||||
.and_then(|idx| ports.get_mut(idx))
|
||||
.map_or(0xFF, |p| p.state())
|
||||
}
|
||||
|
||||
pub fn poll(&self) {
|
||||
debug!("Polling Initial Devices!");
|
||||
|
||||
let len = self.ports.lock().unwrap().len();
|
||||
let len = self.ports.lock().unwrap_or_else(|e| e.into_inner()).len();
|
||||
|
||||
for root_hub_port_num in 1..=(len as u8) {
|
||||
let port_id = PortId {
|
||||
@@ -617,8 +834,13 @@ impl<const N: usize> Xhci<N> {
|
||||
|
||||
//Get the CCS and CSC flags
|
||||
let (ccs, csc, flags) = {
|
||||
let mut ports = self.ports.lock().unwrap();
|
||||
let port = &mut ports[port_id.root_hub_port_index()];
|
||||
let mut ports = self.ports.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let port = match port_id.root_hub_port_index()
|
||||
.and_then(|idx| ports.get_mut(idx))
|
||||
{
|
||||
Some(p) => p,
|
||||
None => continue,
|
||||
};
|
||||
let flags = port.flags();
|
||||
let ccs = flags.contains(PortFlags::CCS);
|
||||
let csc = flags.contains(PortFlags::CSC);
|
||||
@@ -645,7 +867,7 @@ impl<const N: usize> Xhci<N> {
|
||||
pub fn print_port_capabilities(&self) {
|
||||
let len;
|
||||
{
|
||||
let mut ports = self.ports.lock().unwrap();
|
||||
let mut ports = self.ports.lock().unwrap_or_else(|e| e.into_inner());
|
||||
len = ports.len();
|
||||
}
|
||||
|
||||
@@ -658,9 +880,12 @@ impl<const N: usize> Xhci<N> {
|
||||
let state = self.get_pls(port_id);
|
||||
let mut flags;
|
||||
{
|
||||
let mut ports = self.ports.lock().unwrap();
|
||||
let mut ports = self.ports.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
flags = ports[port_id.root_hub_port_index()].flags();
|
||||
flags = match port_id.root_hub_port_index().and_then(|idx| ports.get(idx)) {
|
||||
Some(p) => p.flags(),
|
||||
None => continue,
|
||||
};
|
||||
}
|
||||
|
||||
match self.supported_protocol(port_id) {
|
||||
@@ -685,8 +910,9 @@ impl<const N: usize> Xhci<N> {
|
||||
debug!("XHCI Port {} reset", port_id);
|
||||
|
||||
//TODO handle the second unwrap
|
||||
let mut ports = self.ports.lock().unwrap();
|
||||
let port = ports.get_mut(port_id.root_hub_port_index()).unwrap();
|
||||
let mut ports = self.ports.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let idx = port_id.root_hub_port_index().ok_or(Error::new(EINVAL))?;
|
||||
let port = ports.get_mut(idx).ok_or(Error::new(EINVAL))?;
|
||||
let instant = std::time::Instant::now();
|
||||
|
||||
debug!("Port {} Link State: {}", port_id, port.state());
|
||||
@@ -709,13 +935,52 @@ impl<const N: usize> Xhci<N> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Suspend a port to U3 link state.
|
||||
/// Linux 7.1: xhci-hub.c → xhci_set_link_state(port, XDEV_U3).
|
||||
pub fn suspend_port(&self, port_id: PortId) -> Result<()> {
|
||||
let mut ports = self.ports.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let idx = port_id.root_hub_port_index().ok_or(Error::new(EINVAL))?;
|
||||
let port = ports.get_mut(idx).ok_or(Error::new(EINVAL))?;
|
||||
// Detect USB 3.0 protocol speed from the extended capabilities.
|
||||
// The port speed field (bits 13:10) gives the current link speed.
|
||||
let speed = port.speed();
|
||||
let usb3 = speed >= 4; // SuperSpeed (4) or SuperSpeedPlus (5+)
|
||||
log::info!("xhcid: suspend port {} to U3 (USB3={})", port_id, usb3);
|
||||
port.suspend(usb3);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resume a port from U3 back to U0.
|
||||
/// Linux 7.1: xhci-hub.c → xhci_set_link_state(port, XDEV_U0).
|
||||
pub fn resume_port(&self, port_id: PortId) -> Result<()> {
|
||||
let mut ports = self.ports.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let idx = port_id.root_hub_port_index().ok_or(Error::new(EINVAL))?;
|
||||
let port = ports.get_mut(idx).ok_or(Error::new(EINVAL))?;
|
||||
log::info!("xhcid: resume port {} to U0", port_id);
|
||||
port.resume();
|
||||
drop(ports);
|
||||
|
||||
// Linux 7.1 xhci-pci.c: RESET_ON_RESUME and RESET_TO_DEFAULT
|
||||
// (which implies RESET_ON_RESUME for Tiger/Alder Lake).
|
||||
// Some controllers (Etron EJ168, Fresco Logic FL1009, Intel
|
||||
// Tiger Lake PCH, Alder Lake PCH) need an extra port reset
|
||||
// after wake from U3 to re-establish link training.
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::RESET_ON_RESUME)
|
||||
|| self.quirks.contains(crate::xhci::quirks::XhciQuirks::RESET_TO_DEFAULT)
|
||||
{
|
||||
log::info!("xhcid: RESET quirk active — extra port reset after resume on port {}", port_id);
|
||||
self.reset_port(port_id)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn setup_scratchpads(&mut self) -> Result<()> {
|
||||
let buf_count = self.cap.max_scratchpad_bufs();
|
||||
|
||||
if buf_count == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let scratchpad_buf_arr = ScratchpadBufferArray::new::<N>(self.cap.ac64(), buf_count)?;
|
||||
let scratchpad_buf_arr = ScratchpadBufferArray::new::<N>(self.ac64_effective(), buf_count)?;
|
||||
self.dev_ctx.dcbaa[0] = scratchpad_buf_arr.register() as u64;
|
||||
debug!(
|
||||
"Setting up {} scratchpads, at {:#0x}",
|
||||
@@ -731,7 +996,7 @@ impl<const N: usize> Xhci<N> {
|
||||
{
|
||||
// If ERDP EHB bit is set, clear it before sending command
|
||||
//TODO: find out why this bit is set earlier!
|
||||
let mut run = self.run.lock().unwrap();
|
||||
let mut run = self.run.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut int = &mut run.ints[index];
|
||||
|
||||
if int.erdp_low.readf(1 << 3) {
|
||||
@@ -743,7 +1008,7 @@ impl<const N: usize> Xhci<N> {
|
||||
}
|
||||
|
||||
pub fn interrupt_is_pending(&self, index: usize) -> bool {
|
||||
let mut run = self.run.lock().unwrap();
|
||||
let mut run = self.run.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut int = &mut run.ints[index];
|
||||
int.erdp_low.readf(1 << 3)
|
||||
}
|
||||
@@ -777,19 +1042,56 @@ impl<const N: usize> Xhci<N> {
|
||||
((self.dev_ctx.contexts[slot].slot.d.read() & SLOT_CONTEXT_STATE_MASK)
|
||||
>> SLOT_CONTEXT_STATE_SHIFT) as u8
|
||||
}
|
||||
/// Write a 64-bit register pair with quirk-aware ordering.
|
||||
/// ZERO_64B_REGS (Renesas uPD720202): write HIGH first, then LOW.
|
||||
/// Normal path: write LOW first, then HIGH.
|
||||
fn write_64bit_reg(low: &mut common::io::Mmio<u32>, high: &mut common::io::Mmio<u32>, value: u64, zero_64b: bool) {
|
||||
if zero_64b {
|
||||
high.write((value >> 32) as u32);
|
||||
low.write(value as u32);
|
||||
} else {
|
||||
low.write(value as u32);
|
||||
high.write((value >> 32) as u32);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns effective 64-bit addressing capability, respecting NO_64BIT_SUPPORT quirk.
|
||||
fn ac64_effective(&self) -> bool {
|
||||
if self.quirks.contains(crate::xhci::quirks::XhciQuirks::NO_64BIT_SUPPORT) {
|
||||
false
|
||||
} else {
|
||||
self.cap.ac64()
|
||||
}
|
||||
}
|
||||
|
||||
pub unsafe fn alloc_dma_zeroed_raw<T>(_ac64: bool) -> Result<Dma<T>> {
|
||||
// TODO: ac64
|
||||
Ok(Dma::zeroed()?.assume_init())
|
||||
}
|
||||
pub unsafe fn alloc_dma_zeroed<T>(&self) -> Result<Dma<T>> {
|
||||
Self::alloc_dma_zeroed_raw(self.cap.ac64())
|
||||
Self::alloc_dma_zeroed_raw(self.ac64_effective())
|
||||
}
|
||||
pub unsafe fn alloc_dma_zeroed_unsized_raw<T>(_ac64: bool, count: usize) -> Result<Dma<[T]>> {
|
||||
// TODO: ac64
|
||||
Ok(Dma::zeroed_slice(count)?.assume_init())
|
||||
}
|
||||
pub unsafe fn alloc_dma_zeroed_unsized<T>(&self, count: usize) -> Result<Dma<[T]>> {
|
||||
Self::alloc_dma_zeroed_unsized_raw(self.cap.ac64(), count)
|
||||
Self::alloc_dma_zeroed_unsized_raw(self.ac64_effective(), count)
|
||||
}
|
||||
|
||||
/// DMA buffer pool for descriptor fetches. Returns a pooled buffer
|
||||
/// of the requested size, or allocates a new one.
|
||||
/// Cross-referenced with Linux 7.1 drivers/usb/core/config.c:usb_get_descriptor()
|
||||
/// which allocates per call. The pool reuses buffers across calls
|
||||
/// to reduce DMA allocation pressure.
|
||||
fn dma_pool_take(&self, size: usize) -> Option<Dma<[u8]>> {
|
||||
let mut pool = self.dma_pool.lock().unwrap_or_else(|e| e.into_inner());
|
||||
pool.iter().position(|b| b.len() >= size).map(|i| pool.swap_remove(i))
|
||||
}
|
||||
fn dma_pool_put(&self, buf: Dma<[u8]>) {
|
||||
const MAX_POOL: usize = 32;
|
||||
let mut pool = self.dma_pool.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if pool.len() < MAX_POOL { pool.push(buf); }
|
||||
}
|
||||
|
||||
pub async fn attach_device(&self, port_id: PortId) -> syscall::Result<()> {
|
||||
@@ -799,7 +1101,9 @@ impl<const N: usize> Xhci<N> {
|
||||
}
|
||||
|
||||
let (data, state, speed, flags) = {
|
||||
let port = &self.ports.lock().unwrap()[port_id.root_hub_port_index()];
|
||||
let port = self.ports.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let idx = port_id.root_hub_port_index().ok_or(Error::new(EINVAL))?;
|
||||
let port = port.get(idx).ok_or(Error::new(EINVAL))?;
|
||||
(port.read(), port.state(), port.speed(), port.flags())
|
||||
};
|
||||
|
||||
@@ -832,7 +1136,7 @@ impl<const N: usize> Xhci<N> {
|
||||
//TODO: get correct speed for child devices
|
||||
let protocol_speed = self
|
||||
.lookup_psiv(port_id, speed)
|
||||
.expect("Failed to retrieve speed ID");
|
||||
.ok_or(Error::new(EIO))?;
|
||||
|
||||
let mut input = unsafe { self.alloc_dma_zeroed::<InputContext<N>>()? };
|
||||
|
||||
@@ -852,9 +1156,32 @@ impl<const N: usize> Xhci<N> {
|
||||
|
||||
// TODO: Should the descriptors be cached in PortState, or refetched?
|
||||
|
||||
let (parent_hub_slot_id, parent_port_num, parent_port_id, behind_highspeed_hub) =
|
||||
if let Some((parent_port, port_num_on_parent)) = port_id.parent() {
|
||||
match self.port_states.get(&parent_port) {
|
||||
Some(parent_state) => {
|
||||
let child_ls_fs = protocol_speed.is_lowspeed() || protocol_speed.is_fullspeed();
|
||||
let parent_hs = parent_state.protocol_speed.is_highspeed();
|
||||
(
|
||||
Some(parent_state.slot),
|
||||
Some(port_num_on_parent),
|
||||
Some(parent_port),
|
||||
child_ls_fs && parent_hs,
|
||||
)
|
||||
}
|
||||
None => (None, None, None, false),
|
||||
}
|
||||
} else {
|
||||
(None, None, None, false)
|
||||
};
|
||||
|
||||
let mut port_state = PortState {
|
||||
slot,
|
||||
protocol_speed,
|
||||
parent_hub_slot_id,
|
||||
parent_port_num,
|
||||
parent_port_id,
|
||||
behind_highspeed_hub,
|
||||
input_context: Mutex::new(input),
|
||||
dev_desc: None,
|
||||
cfg_idx: None,
|
||||
@@ -873,9 +1200,9 @@ impl<const N: usize> Xhci<N> {
|
||||
// Ensure correct packet size is used
|
||||
let dev_desc_8_byte = self.fetch_dev_desc_8_byte(port_id, slot).await?;
|
||||
{
|
||||
let mut port_state = self.port_states.get_mut(&port_id).unwrap();
|
||||
let mut port_state = self.port_states.get_mut(&port_id).ok_or(Error::new(EBADFD))?;
|
||||
|
||||
let mut input = port_state.input_context.lock().unwrap();
|
||||
let mut input = port_state.input_context.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
self.update_max_packet_size(&mut *input, slot, dev_desc_8_byte)
|
||||
.await?;
|
||||
@@ -885,15 +1212,15 @@ impl<const N: usize> Xhci<N> {
|
||||
|
||||
let dev_desc = self.get_desc(port_id, slot).await?;
|
||||
debug!("Got the full device descriptor!");
|
||||
self.port_states.get_mut(&port_id).unwrap().dev_desc = Some(dev_desc);
|
||||
self.port_states.get_mut(&port_id).ok_or(Error::new(EBADFD))?.dev_desc = Some(dev_desc);
|
||||
|
||||
debug!("Got the port states again!");
|
||||
{
|
||||
let mut port_state = self.port_states.get_mut(&port_id).unwrap();
|
||||
let mut port_state = self.port_states.get_mut(&port_id).ok_or(Error::new(EBADFD))?;
|
||||
|
||||
let mut input = port_state.input_context.lock().unwrap();
|
||||
let mut input = port_state.input_context.lock().unwrap_or_else(|e| e.into_inner());
|
||||
debug!("Got the input context!");
|
||||
let dev_desc = port_state.dev_desc.as_ref().unwrap();
|
||||
let dev_desc = port_state.dev_desc.as_ref().ok_or(Error::new(EBADFD))?;
|
||||
|
||||
self.update_default_control_pipe(&mut *input, slot, dev_desc)
|
||||
.await?;
|
||||
@@ -1083,7 +1410,7 @@ impl<const N: usize> Xhci<N> {
|
||||
}
|
||||
}
|
||||
|
||||
let mut ring = Ring::new::<N>(self.cap.ac64(), 16, true)?;
|
||||
let mut ring = Ring::new::<N>(self.ac64_effective(), 16, true)?;
|
||||
|
||||
{
|
||||
input_context.add_context.write(1 << 1 | 1); // Enable the slot (zeroth bit) and the control endpoint (first bit).
|
||||
@@ -1190,7 +1517,7 @@ impl<const N: usize> Xhci<N> {
|
||||
/// Checks whether an IRQ has been received from *this* device, in case of an interrupt. Always
|
||||
/// true when using MSI/MSI-X.
|
||||
pub fn received_irq(&self) -> bool {
|
||||
let mut runtime_regs = self.run.lock().unwrap();
|
||||
let mut runtime_regs = self.run.lock().unwrap_or_else(|e| e.into_inner());
|
||||
|
||||
if self.uses_msi_interrupts() {
|
||||
// Since using MSI and MSI-X implies having no IRQ sharing whatsoever, the IP bit
|
||||
@@ -1224,7 +1551,7 @@ impl<const N: usize> Xhci<N> {
|
||||
// TODO: Now that there are some good error crates, I don't think errno.h error codes are
|
||||
// suitable here.
|
||||
|
||||
let ps = self.port_states.get(&port).unwrap();
|
||||
let ps = self.port_states.get(&port).ok_or(Error::new(EBADFD))?;
|
||||
trace!("Spawning driver on port: {}", port);
|
||||
|
||||
//TODO: support choosing config?
|
||||
@@ -1435,13 +1762,49 @@ impl<const N: usize> Xhci<N> {
|
||||
self.supported_protocol_speeds(port)
|
||||
.find(|speed| speed.psiv() == psiv)
|
||||
}
|
||||
|
||||
pub(crate) fn trait_control_transfer(
|
||||
&self,
|
||||
port_id: PortId,
|
||||
setup: &::usb_core::SetupPacket,
|
||||
data: &mut [u8],
|
||||
) -> Result<usize, syscall::Error> {
|
||||
use crate::xhci::trb::TransferKind;
|
||||
let is_in = setup.request_type & 0x80 != 0;
|
||||
let setup_pkt = crate::usb::Setup {
|
||||
kind: setup.request_type,
|
||||
request: setup.request,
|
||||
value: setup.value,
|
||||
index: setup.index,
|
||||
length: setup.length,
|
||||
};
|
||||
let tk = if data.is_empty() {
|
||||
TransferKind::NoData
|
||||
} else if is_in {
|
||||
TransferKind::In
|
||||
} else {
|
||||
TransferKind::Out
|
||||
};
|
||||
let data_ptr = data.as_ptr() as usize;
|
||||
let data_len = data.len();
|
||||
let _event = futures::executor::block_on(
|
||||
self.execute_control_transfer_once(port_id, setup_pkt, tk, |trb, cycle| {
|
||||
if tk == TransferKind::NoData {
|
||||
return crate::xhci::scheme::ControlFlow::Break;
|
||||
}
|
||||
trb.data(data_ptr, data_len as u16, !is_in, cycle);
|
||||
crate::xhci::scheme::ControlFlow::Break
|
||||
}),
|
||||
)?;
|
||||
Ok(data_len)
|
||||
}
|
||||
}
|
||||
pub fn start_irq_reactor<const N: usize>(hci: &Arc<Xhci<N>>, irq_file: Option<File>) {
|
||||
let hci_clone = Arc::clone(&hci);
|
||||
|
||||
debug!("About to start IRQ reactor");
|
||||
|
||||
*hci.irq_reactor.lock().unwrap() = Some(thread::spawn(move || {
|
||||
*hci.irq_reactor.lock().unwrap_or_else(|e| e.into_inner()) = Some(thread::spawn(move || {
|
||||
debug!("Started IRQ reactor thread");
|
||||
IrqReactor::new(hci_clone, irq_file).run()
|
||||
}));
|
||||
@@ -1452,7 +1815,7 @@ pub fn start_device_enumerator<const N: usize>(hci: &Arc<Xhci<N>>) {
|
||||
|
||||
debug!("About to start Device Enumerator");
|
||||
|
||||
*hci.device_enumerator.lock().unwrap() = Some(thread::spawn(move || {
|
||||
*hci.device_enumerator.lock().unwrap_or_else(|e| e.into_inner()) = Some(thread::spawn(move || {
|
||||
debug!("Started Device Enumerator");
|
||||
DeviceEnumerator::new(hci_clone).run();
|
||||
}));
|
||||
|
||||
@@ -55,6 +55,40 @@ pub struct Port {
|
||||
pub porthlpmc: Mmio<u32>,
|
||||
}
|
||||
|
||||
// PORTHLPMC register bits (USB 2.0 LPM).
|
||||
// Cross-referenced with Linux 7.1 drivers/usb/host/xhci-port.h:135-173.
|
||||
pub const PORT_HLE: u32 = 1u32 << 16; // Hardware LPM Enable
|
||||
pub const PORT_HIRD_MASK: u32 = 0xFu32 << 4; // Host Initiated Resume Duration
|
||||
pub const PORT_L1_TIMEOUT_MASK: u32 = 0xFFu32 << 2;
|
||||
pub const PORT_BESLD_MASK: u32 = 0xFu32 << 10; // Best Effort Service Latency Deep
|
||||
pub const PORT_HIRDM_MASK: u32 = 0x3u32; // Host Initiated Resume Duration Mode
|
||||
pub const XHCI_DEFAULT_BESL: u32 = 4;
|
||||
pub const XHCI_L1_TIMEOUT: u32 = 512; // microseconds
|
||||
|
||||
// USB 3.0 Port Link States (PORTSC PLS field, bits 8:5).
|
||||
// Cross-referenced with Linux 7.1 drivers/usb/host/xhci-port.h:18-21.
|
||||
pub const XDEV_U0: u32 = 0x0 << 5; // Active
|
||||
pub const XDEV_U1: u32 = 0x1 << 5; // Fast exit (<10us)
|
||||
pub const XDEV_U2: u32 = 0x2 << 5; // Slower exit (>100us)
|
||||
pub const XDEV_U3: u32 = 0x3 << 5; // Suspend
|
||||
pub const XDEV_DISABLED: u32 = 0x4 << 5;
|
||||
pub const XDEV_RXDETECT: u32 = 0x5 << 5;
|
||||
pub const XDEV_INACTIVE: u32 = 0x6 << 5;
|
||||
pub const XDEV_POLLING: u32 = 0x7 << 5;
|
||||
pub const XDEV_RECOVERY: u32 = 0x8 << 5;
|
||||
pub const XDEV_HOT_RESET: u32 = 0x9 << 5;
|
||||
pub const XDEV_COMPLIANCE: u32 = 0xA << 5;
|
||||
pub const XDEV_TEST_MODE: u32 = 0xB << 5;
|
||||
pub const XDEV_RESUME: u32 = 0xF << 5;
|
||||
|
||||
// Port PM Control (portpmsc) register bits (xHCI 1.1+).
|
||||
// U1/U2 timeout values control how quickly the port transitions from
|
||||
// U0 to U1/U2 after idling. See Linux xhci-port.h:128-132.
|
||||
pub const PORT_U1_TIMEOUT_MASK: u32 = 0x0000_00FF;
|
||||
pub const PORT_U2_TIMEOUT_SHIFT: u32 = 8;
|
||||
pub const PORT_U2_TIMEOUT_MASK: u32 = 0x0000_FF00;
|
||||
pub const PORT_FORCE_LINK_PM_ACCEPT: u32 = 1u32 << 19; // xHCI 1.1+
|
||||
|
||||
impl Port {
|
||||
pub fn read(&self) -> u32 {
|
||||
self.portsc.read()
|
||||
@@ -111,4 +145,58 @@ impl Port {
|
||||
|
||||
self.flags() & preserved
|
||||
}
|
||||
|
||||
/// Enable USB 2.0 Hardware LPM on this port.
|
||||
/// Linux 7.1: xhci_set_usb2_hardware_lpm() → sets PORT_HLE.
|
||||
pub fn enable_lpm(&mut self, hird: u32, l1_timeout: u32) {
|
||||
let val = PORT_HLE
|
||||
| ((hird & 0xF) << 4) // PORT_HIRD
|
||||
| ((l1_timeout & 0xFF) << 2); // PORT_L1_TIMEOUT
|
||||
self.porthlpmc.write(val);
|
||||
}
|
||||
|
||||
/// Disable USB 2.0 Hardware LPM on this port.
|
||||
pub fn disable_lpm(&mut self) {
|
||||
self.porthlpmc.write(0);
|
||||
}
|
||||
|
||||
/// Program U1/U2 inactivity timeout for USB 3.0 ports.
|
||||
/// `u1_timeout` is multiplied by 256ns.
|
||||
/// `u2_timeout` is multiplied by 1us.
|
||||
/// Linux 7.1: xhci-hub.c computes these from device descriptors.
|
||||
pub fn set_u1_u2_timeout(&mut self, u1_timeout: u8, u2_timeout: u8) {
|
||||
let val = (u32::from(u1_timeout) & PORT_U1_TIMEOUT_MASK)
|
||||
| ((u32::from(u2_timeout) << PORT_U2_TIMEOUT_SHIFT) & PORT_U2_TIMEOUT_MASK)
|
||||
| PORT_FORCE_LINK_PM_ACCEPT;
|
||||
self.portpmsc.write(val);
|
||||
}
|
||||
|
||||
/// Get the current port link state from PLS bits.
|
||||
pub fn link_state(&self) -> u8 {
|
||||
((self.read() >> 5) & 0xF) as u8
|
||||
}
|
||||
|
||||
/// Transition the port to a new link state.
|
||||
/// Linux 7.1: xhci_set_link_state() in xhci-hub.c.
|
||||
/// Writes PLS and sets PORT_LINK_STROBE (LWS) to commit the
|
||||
/// transition. All RW1CS/RW1S bits are cleared to neutral
|
||||
/// before writing.
|
||||
pub fn set_link_state(&mut self, link_state: u32) {
|
||||
let neutral = self.flags_preserved();
|
||||
let val = (neutral & !(PortFlags::PLS_0 | PortFlags::PLS_1 | PortFlags::PLS_2 | PortFlags::PLS_3)).bits()
|
||||
| link_state
|
||||
| PortFlags::LWS.bits();
|
||||
self.portsc.write(val);
|
||||
}
|
||||
|
||||
/// Suspend the port to U3 (USB 3.0) or U3-equivalent for USB 2.
|
||||
/// Linux 7.1: xhci-hub.c → xhci_set_link_state(xhci, port, XDEV_U3).
|
||||
pub fn suspend(&mut self, usb3: bool) {
|
||||
self.set_link_state(if usb3 { XDEV_U3 } else { XDEV_U3 });
|
||||
}
|
||||
|
||||
/// Resume the port back to U0.
|
||||
pub fn resume(&mut self) {
|
||||
self.set_link_state(XDEV_U0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
//! xHCI per-controller quirks.
|
||||
//!
|
||||
//! Cross-referenced with `local/reference/linux-7.1/drivers/usb/host/xhci.h`
|
||||
//! (51 quirk flags) and `xhci-pci.c` (per-vendor/per-device lookup).
|
||||
//!
|
||||
//! Each vendor has controller-specific errata requiring workaround code.
|
||||
//! Without quirks, xhcid will silently misbehave on real hardware.
|
||||
//! This module ports Linux's quirk table verbatim.
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
||||
bitflags! {
|
||||
/// xHCI controller quirks. See `linux-7.1/drivers/usb/host/xhci.h:1587-1649`.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct XhciQuirks: u64 {
|
||||
const LINK_TRB_QUIRK = 1u64 << 0;
|
||||
/// Deprecated — kept for binary compat with the Linux header.
|
||||
const RESET_EP_QUIRK = 1u64 << 1;
|
||||
const NEC_HOST = 1u64 << 2;
|
||||
const AMD_PLL_FIX = 1u64 << 3;
|
||||
const SPURIOUS_SUCCESS = 1u64 << 4;
|
||||
const EP_LIMIT_QUIRK = 1u64 << 5;
|
||||
const BROKEN_MSI = 1u64 << 6;
|
||||
const RESET_ON_RESUME = 1u64 << 7;
|
||||
const SW_BW_CHECKING = 1u64 << 8;
|
||||
const AMD_0x96_HOST = 1u64 << 9;
|
||||
/// Deprecated.
|
||||
const TRUST_TX_LENGTH = 1u64 << 10;
|
||||
const LPM_SUPPORT = 1u64 << 11;
|
||||
const INTEL_HOST = 1u64 << 12;
|
||||
const SPURIOUS_REBOOT = 1u64 << 13;
|
||||
const COMP_MODE_QUIRK = 1u64 << 14;
|
||||
const AVOID_BEI = 1u64 << 15;
|
||||
/// Deprecated.
|
||||
const PLAT = 1u64 << 16;
|
||||
const SLOW_SUSPEND = 1u64 << 17;
|
||||
const SPURIOUS_WAKEUP = 1u64 << 18;
|
||||
const BROKEN_STREAMS = 1u64 << 19;
|
||||
const PME_STUCK_QUIRK = 1u64 << 20;
|
||||
const MTK_HOST = 1u64 << 21;
|
||||
const SSIC_PORT_UNUSED = 1u64 << 22;
|
||||
const NO_64BIT_SUPPORT = 1u64 << 23;
|
||||
const MISSING_CAS = 1u64 << 24;
|
||||
const BROKEN_PORT_PED = 1u64 << 25;
|
||||
const LIMIT_ENDPOINT_INTERVAL_7 = 1u64 << 26;
|
||||
const U2_DISABLE_WAKE = 1u64 << 27;
|
||||
const ASMEDIA_MODIFY_FLOWCONTROL = 1u64 << 28;
|
||||
const HW_LPM_DISABLE = 1u64 << 29;
|
||||
const SUSPEND_DELAY = 1u64 << 30;
|
||||
const INTEL_USB_ROLE_SW = 1u64 << 31;
|
||||
const ZERO_64B_REGS = 1u64 << 32;
|
||||
const DEFAULT_PM_RUNTIME_ALLOW = 1u64 << 33;
|
||||
const RESET_PLL_ON_DISCONNECT = 1u64 << 34;
|
||||
const SNPS_BROKEN_SUSPEND = 1u64 << 35;
|
||||
const SKIP_PHY_INIT = 1u64 << 37;
|
||||
const DISABLE_SPARSE = 1u64 << 38;
|
||||
const SG_TRB_CACHE_SIZE_QUIRK = 1u64 << 39;
|
||||
const NO_SOFT_RETRY = 1u64 << 40;
|
||||
const BROKEN_D3COLD_S2I = 1u64 << 41;
|
||||
const EP_CTX_BROKEN_DCS = 1u64 << 42;
|
||||
const SUSPEND_RESUME_CLKS = 1u64 << 43;
|
||||
const RESET_TO_DEFAULT = 1u64 << 44;
|
||||
const TRB_OVERFETCH = 1u64 << 45;
|
||||
const ZHAOXIN_HOST = 1u64 << 46;
|
||||
const WRITE_64_HI_LO = 1u64 << 47;
|
||||
const CDNS_SCTX_QUIRK = 1u64 << 48;
|
||||
const ETRON_HOST = 1u64 << 49;
|
||||
const LIMIT_ENDPOINT_INTERVAL_9 = 1u64 << 50;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod vendor {
|
||||
pub const FRESCO_LOGIC: u16 = 0x1b73;
|
||||
pub const NEC: u16 = 0x1033;
|
||||
pub const AMD: u16 = 0x1022;
|
||||
pub const ATI: u16 = 0x1002;
|
||||
pub const INTEL: u16 = 0x8086;
|
||||
pub const ASMEDIA: u16 = 0x1b21;
|
||||
pub const ETRON: u16 = 0x1b6f;
|
||||
pub const RENESAS: u16 = 0x1912;
|
||||
pub const VIA: u16 = 0x1106;
|
||||
pub const CDNS: u16 = 0x17cd;
|
||||
pub const PHYTIUM: u16 = 0x1db7;
|
||||
pub const ZHAOXIN: u16 = 0x1d17;
|
||||
pub const REDOX_OS_VENDOR: u16 = 0x1af4;
|
||||
}
|
||||
|
||||
pub struct DeviceQuirkEntry {
|
||||
pub vendor: u16,
|
||||
pub device: u16, // 0 = any device
|
||||
pub hci_version: u8, // 0 = any version
|
||||
pub quirks: XhciQuirks,
|
||||
}
|
||||
|
||||
const QUIRK_TABLE: &[DeviceQuirkEntry] = &[
|
||||
// Fresco Logic: all revisions have broken MSI.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::FRESCO_LOGIC, device: 0x1000, hci_version: 0,
|
||||
quirks: XhciQuirks::from_bits(XhciQuirks::BROKEN_MSI.bits() | XhciQuirks::BROKEN_STREAMS.bits()).unwrap(),
|
||||
},
|
||||
// ASMedia ASM1042/ASM1042A: modify flow control, broken MSI on rev 0x01.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::ASMEDIA, device: 0x1042, hci_version: 0,
|
||||
quirks: XhciQuirks::ASMEDIA_MODIFY_FLOWCONTROL,
|
||||
},
|
||||
// ASMedia ASM1142: broken MSI.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::ASMEDIA, device: 0x1142, hci_version: 0,
|
||||
quirks: XhciQuirks::BROKEN_MSI,
|
||||
},
|
||||
// ASMedia ASM2142/ASM3142: U2 disable wake, broken MSI.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::ASMEDIA, device: 0x2142, hci_version: 0,
|
||||
quirks: XhciQuirks::from_bits(XhciQuirks::BROKEN_MSI.bits() | XhciQuirks::U2_DISABLE_WAKE.bits()).unwrap(),
|
||||
},
|
||||
// ASMedia ASM3242: broken MSI.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::ASMEDIA, device: 0x3242, hci_version: 0,
|
||||
quirks: XhciQuirks::BROKEN_MSI,
|
||||
},
|
||||
// VIA VL805.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::VIA, device: 0x3483, hci_version: 0,
|
||||
quirks: XhciQuirks::RESET_ON_RESUME,
|
||||
},
|
||||
// NEC: oldest quirk target.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::NEC, device: 0, hci_version: 0,
|
||||
quirks: XhciQuirks::NEC_HOST,
|
||||
},
|
||||
// AMD 0x96 family.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::AMD, device: 0, hci_version: 0x96,
|
||||
quirks: XhciQuirks::AMD_0x96_HOST,
|
||||
},
|
||||
// AMD Promontory: U2 wake is broken.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::AMD, device: 0x43bc, hci_version: 0,
|
||||
quirks: XhciQuirks::U2_DISABLE_WAKE,
|
||||
},
|
||||
// AMD Renoir.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::AMD, device: 0x1639, hci_version: 0,
|
||||
quirks: XhciQuirks::BROKEN_D3COLD_S2I,
|
||||
},
|
||||
// AMD VanGogh.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::AMD, device: 0x161d, hci_version: 0,
|
||||
quirks: XhciQuirks::BROKEN_D3COLD_S2I,
|
||||
},
|
||||
// Intel baseline.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::INTEL, device: 0, hci_version: 0,
|
||||
quirks: XhciQuirks::from_bits(XhciQuirks::LPM_SUPPORT.bits() | XhciQuirks::INTEL_HOST.bits() | XhciQuirks::AVOID_BEI.bits()).unwrap(),
|
||||
},
|
||||
// Intel Panther Point.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::INTEL, device: 0x9c31, hci_version: 0,
|
||||
quirks: XhciQuirks::from_bits(XhciQuirks::EP_LIMIT_QUIRK.bits() | XhciQuirks::SW_BW_CHECKING.bits() | XhciQuirks::SPURIOUS_REBOOT.bits()).unwrap(),
|
||||
},
|
||||
// Intel LynxPoint / WildcatPoint.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::INTEL, device: 0x9c31, hci_version: 0,
|
||||
quirks: XhciQuirks::from_bits(XhciQuirks::SPURIOUS_REBOOT.bits() | XhciQuirks::SPURIOUS_WAKEUP.bits()).unwrap(),
|
||||
},
|
||||
// Intel SunrisePoint.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::INTEL, device: 0x9d2f, hci_version: 0,
|
||||
quirks: XhciQuirks::PME_STUCK_QUIRK,
|
||||
},
|
||||
// Intel Cherryview.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::INTEL, device: 0xa2af, hci_version: 0,
|
||||
quirks: XhciQuirks::from_bits(XhciQuirks::PME_STUCK_QUIRK.bits() | XhciQuirks::SSIC_PORT_UNUSED.bits()).unwrap(),
|
||||
},
|
||||
// Intel TigerLake / AlderLake.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::INTEL, device: 0x9a13, hci_version: 0,
|
||||
quirks: XhciQuirks::RESET_TO_DEFAULT,
|
||||
},
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::INTEL, device: 0x51ed, hci_version: 0,
|
||||
quirks: XhciQuirks::RESET_TO_DEFAULT,
|
||||
},
|
||||
// Intel Alpine / Titan Ridge / IceLake / TigerLake / MapleRidge.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::INTEL, device: 0x15b5, hci_version: 0,
|
||||
quirks: XhciQuirks::DEFAULT_PM_RUNTIME_ALLOW,
|
||||
},
|
||||
// Etron EJ168/EJ188.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::ETRON, device: 0x7023, hci_version: 0,
|
||||
quirks: XhciQuirks::from_bits(
|
||||
XhciQuirks::ETRON_HOST.bits()
|
||||
| XhciQuirks::RESET_ON_RESUME.bits()
|
||||
| XhciQuirks::BROKEN_STREAMS.bits()
|
||||
| XhciQuirks::NO_SOFT_RETRY.bits(),
|
||||
)
|
||||
.unwrap(),
|
||||
},
|
||||
// Renesas uPD720202 (gen 1).
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::RENESAS, device: 0x0014, hci_version: 0,
|
||||
quirks: XhciQuirks::ZERO_64B_REGS,
|
||||
},
|
||||
// Renesas uPD720202 K2 (gen 2).
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::RENESAS, device: 0x0015, hci_version: 0,
|
||||
quirks: XhciQuirks::from_bits(XhciQuirks::RESET_ON_RESUME.bits() | XhciQuirks::ZERO_64B_REGS.bits()).unwrap(),
|
||||
},
|
||||
// VIA.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::VIA, device: 0, hci_version: 0,
|
||||
quirks: XhciQuirks::RESET_ON_RESUME,
|
||||
},
|
||||
// Phytium.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::PHYTIUM, device: 0, hci_version: 0,
|
||||
quirks: XhciQuirks::LIMIT_ENDPOINT_INTERVAL_9,
|
||||
},
|
||||
// Zhaoxin.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::ZHAOXIN, device: 0, hci_version: 0,
|
||||
quirks: XhciQuirks::ZHAOXIN_HOST,
|
||||
},
|
||||
// Redox OS (QEMU emulated, VID 0x1af4): baseline AVOID_BEI.
|
||||
DeviceQuirkEntry {
|
||||
vendor: vendor::REDOX_OS_VENDOR, device: 0, hci_version: 0,
|
||||
quirks: XhciQuirks::AVOID_BEI,
|
||||
},
|
||||
];
|
||||
|
||||
/// Look up quirks for a given controller.
|
||||
pub fn lookup_quirks(vendor: u16, device: u16, hci_version: u8) -> XhciQuirks {
|
||||
let mut result = XhciQuirks::default();
|
||||
for entry in QUIRK_TABLE {
|
||||
if entry.vendor != vendor {
|
||||
continue;
|
||||
}
|
||||
let device_matches = entry.device == 0 || entry.device == device;
|
||||
let version_matches = entry.hci_version == 0 || entry.hci_version == hci_version;
|
||||
if device_matches && version_matches {
|
||||
result |= entry.quirks;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn intel_has_lpm_and_avoid_bei() {
|
||||
let q = lookup_quirks(vendor::INTEL, 0x9c31, 0);
|
||||
assert!(q.contains(XhciQuirks::LPM_SUPPORT));
|
||||
assert!(q.contains(XhciQuirks::INTEL_HOST));
|
||||
assert!(q.contains(XhciQuirks::AVOID_BEI));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn etron_has_broken_streams() {
|
||||
let q = lookup_quirks(vendor::ETRON, 0x7023, 0);
|
||||
assert!(q.contains(XhciQuirks::ETRON_HOST));
|
||||
assert!(q.contains(XhciQuirks::BROKEN_STREAMS));
|
||||
assert!(q.contains(XhciQuirks::NO_SOFT_RETRY));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renesas_first_gen_has_zero_64b() {
|
||||
let q = lookup_quirks(vendor::RENESAS, 0x0014, 0);
|
||||
assert!(q.contains(XhciQuirks::ZERO_64B_REGS));
|
||||
assert!(!q.contains(XhciQuirks::RESET_ON_RESUME));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renesas_k2_has_both() {
|
||||
let q = lookup_quirks(vendor::RENESAS, 0x0015, 0);
|
||||
assert!(q.contains(XhciQuirks::ZERO_64B_REGS));
|
||||
assert!(q.contains(XhciQuirks::RESET_ON_RESUME));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_vendor_returns_no_quirks() {
|
||||
let q = lookup_quirks(0x1234, 0x5678, 0);
|
||||
assert_eq!(q, XhciQuirks::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qemu_emulated_redox_vid_has_avoid_bei() {
|
||||
let q = lookup_quirks(vendor::REDOX_OS_VENDOR, 0, 0);
|
||||
assert!(q.contains(XhciQuirks::AVOID_BEI));
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,8 @@ impl Ring {
|
||||
|| (trb_virt_pointer as usize)
|
||||
> (trbs_base_virt_pointer as usize) + self.trbs.len() * mem::size_of::<Trb>()
|
||||
{
|
||||
panic!("Gave a TRB outside of the ring, when retrieving its physical address in that ring. TRB: {:?} (at address {:p})", trb, trb);
|
||||
log::error!("trb_phys_ptr: TRB outside ring bounds. TRB: {:?} (at {:p}) — returning 0", trb, trb);
|
||||
return 0;
|
||||
}
|
||||
let trb_offset_from_base = trb_virt_pointer as u64 - trbs_base_virt_pointer as u64;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::port::PortFlags;
|
||||
use super::{PortId, Xhci};
|
||||
|
||||
use usb_core::{PortStatus, SetupPacket, TransferDirection, UsbError, UsbHostController};
|
||||
|
||||
pub struct XhciAdapter<const N: usize> {
|
||||
hci: Arc<Xhci<N>>,
|
||||
name: String,
|
||||
addr_map: BTreeMap<u8, PortId>,
|
||||
}
|
||||
|
||||
impl<const N: usize> XhciAdapter<N> {
|
||||
pub fn new(hci: Arc<Xhci<N>>) -> Self {
|
||||
let name = hci.scheme_name.clone();
|
||||
Self { hci, name, addr_map: BTreeMap::new() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> UsbHostController for XhciAdapter<N> {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn port_count(&self) -> usize {
|
||||
self.hci.ports.lock().unwrap_or_else(|e| e.into_inner()).len()
|
||||
}
|
||||
|
||||
fn port_status(&self, port: usize) -> Option<PortStatus> {
|
||||
let ports = self.hci.ports.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let p = ports.get(port)?;
|
||||
let flags = p.flags();
|
||||
let link_state = p.state();
|
||||
let speed = p.speed();
|
||||
|
||||
Some(PortStatus {
|
||||
connected: flags.contains(PortFlags::CCS),
|
||||
enabled: flags.contains(PortFlags::PED),
|
||||
suspended: link_state == 3, // U3 = suspend
|
||||
over_current: flags.contains(PortFlags::OCA),
|
||||
reset: flags.contains(PortFlags::PR),
|
||||
power: flags.contains(PortFlags::PP),
|
||||
low_speed: speed == 2, // USB 1.0 low-speed (1.5 Mbps)
|
||||
high_speed: speed == 3, // USB 2.0 high-speed (480 Mbps)
|
||||
test_mode: false,
|
||||
indicator: flags.intersects(PortFlags::PIC_AMB | PortFlags::PIC_GRN),
|
||||
})
|
||||
}
|
||||
|
||||
fn port_reset(&mut self, port: usize) -> bool {
|
||||
let port_id = PortId {
|
||||
root_hub_port_num: (port + 1) as u8,
|
||||
route_string: 0,
|
||||
};
|
||||
self.hci.reset_port(port_id).is_ok()
|
||||
}
|
||||
|
||||
fn control_transfer(
|
||||
&mut self,
|
||||
device_address: u8,
|
||||
setup: &SetupPacket,
|
||||
data: &mut [u8],
|
||||
) -> Result<usize, UsbError> {
|
||||
let port_id = self.addr_map.get(&device_address).copied()
|
||||
.ok_or(UsbError::NoDevice)?;
|
||||
self.hci.trait_control_transfer(port_id, setup, data)
|
||||
.map_err(|_| UsbError::IoError)
|
||||
}
|
||||
|
||||
fn bulk_transfer(
|
||||
&mut self,
|
||||
_device_address: u8,
|
||||
_endpoint: u8,
|
||||
_data: &mut [u8],
|
||||
_direction: TransferDirection,
|
||||
) -> Result<usize, UsbError> {
|
||||
// Class drivers (usbscsid, usbhidd) talk to devices through the
|
||||
// xhci scheme IPC. Bulk transfers through the trait are not used
|
||||
// in the current architecture.
|
||||
Err(UsbError::Unsupported)
|
||||
}
|
||||
|
||||
fn interrupt_transfer(
|
||||
&mut self,
|
||||
_device_address: u8,
|
||||
_endpoint: u8,
|
||||
_data: &mut [u8],
|
||||
) -> Result<usize, UsbError> {
|
||||
Err(UsbError::Unsupported)
|
||||
}
|
||||
|
||||
fn set_address(&mut self, device_address: u8) -> bool {
|
||||
if device_address == 0 {
|
||||
return false;
|
||||
}
|
||||
let port_id = PortId {
|
||||
root_hub_port_num: device_address,
|
||||
route_string: 0,
|
||||
};
|
||||
self.addr_map.insert(device_address, port_id);
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -446,6 +446,33 @@ impl Trb {
|
||||
| ((TrbType::Normal as u32) << 10),
|
||||
)
|
||||
}
|
||||
pub fn isoch(
|
||||
&mut self,
|
||||
buffer: u64,
|
||||
len: u32,
|
||||
cycle: bool,
|
||||
td_size: u8,
|
||||
interrupter: u8,
|
||||
isp: bool,
|
||||
chain: bool,
|
||||
ioc: bool,
|
||||
tlbpc: u8,
|
||||
sia_frame_id: u32,
|
||||
) {
|
||||
assert!(td_size <= 0x1F);
|
||||
assert!(tlbpc <= 0xF);
|
||||
self.set(
|
||||
buffer,
|
||||
len | (u32::from(td_size) << 17) | (u32::from(interrupter) << 22),
|
||||
u32::from(cycle)
|
||||
| (u32::from(isp) << 2)
|
||||
| (u32::from(chain) << 4)
|
||||
| (u32::from(ioc) << 5)
|
||||
| ((TrbType::Isoch as u32) << 10)
|
||||
| (u32::from(tlbpc) << 16)
|
||||
| ((sia_frame_id & 0xFFF) << 20),
|
||||
)
|
||||
}
|
||||
pub fn is_command_trb(&self) -> bool {
|
||||
let valid_trb_types = [
|
||||
TrbType::NoOpCmd as u8,
|
||||
@@ -508,3 +535,138 @@ impl fmt::Display for Trb {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_trb() -> Trb {
|
||||
unsafe { std::mem::zeroed::<Trb>() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_trb_sets_correct_type() {
|
||||
let mut trb = test_trb();
|
||||
trb.normal(0x1000, 512, true, 3, 0, false, true, false, true, false, false);
|
||||
assert_eq!(trb.trb_type(), TrbType::Normal as u8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isoch_trb_sets_correct_type() {
|
||||
let mut trb = test_trb();
|
||||
trb.isoch(0x1000, 512, true, 0, 0, true, false, true, 1, 0);
|
||||
assert_eq!(trb.trb_type(), TrbType::Isoch as u8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setup_trb_preserves_request_type() {
|
||||
let mut trb = test_trb();
|
||||
trb.setup(usb::Setup { kind: 0x80, request: 0x06, value: 0x0100, index: 0, length: 18 }, TransferKind::In, true);
|
||||
assert_eq!(trb.trb_type(), TrbType::SetupStage as u8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_36_completion_codes_unique() {
|
||||
let codes: [u8; 35] = [
|
||||
TrbCompletionCode::Invalid as u8, TrbCompletionCode::Success as u8,
|
||||
TrbCompletionCode::DataBuffer as u8, TrbCompletionCode::BabbleDetected as u8,
|
||||
TrbCompletionCode::UsbTransaction as u8, TrbCompletionCode::Trb as u8,
|
||||
TrbCompletionCode::Stall as u8, TrbCompletionCode::Resource as u8,
|
||||
TrbCompletionCode::Bandwidth as u8, TrbCompletionCode::NoSlotsAvailable as u8,
|
||||
TrbCompletionCode::SlotNotEnabled as u8, TrbCompletionCode::EndpointNotEnabled as u8,
|
||||
TrbCompletionCode::ShortPacket as u8, TrbCompletionCode::RingUnderrun as u8,
|
||||
TrbCompletionCode::RingOverrun as u8, TrbCompletionCode::VfEventRingFull as u8,
|
||||
TrbCompletionCode::Parameter as u8, TrbCompletionCode::BandwidthOverrun as u8,
|
||||
TrbCompletionCode::ContextState as u8, TrbCompletionCode::NoPingResponse as u8,
|
||||
TrbCompletionCode::EventRingFull as u8, TrbCompletionCode::IncompatibleDevice as u8,
|
||||
TrbCompletionCode::MissedService as u8, TrbCompletionCode::CommandRingStopped as u8,
|
||||
TrbCompletionCode::CommandAborted as u8, TrbCompletionCode::Stopped as u8,
|
||||
TrbCompletionCode::StoppedLengthInvalid as u8, TrbCompletionCode::StoppedShortPacket as u8,
|
||||
TrbCompletionCode::MaxExitLatencyTooLarge as u8, TrbCompletionCode::IsochBuffer as u8,
|
||||
TrbCompletionCode::EventLost as u8, TrbCompletionCode::InvalidStreamId as u8,
|
||||
TrbCompletionCode::SecondaryBandwidth as u8, TrbCompletionCode::SplitTransaction as u8,
|
||||
TrbCompletionCode::Undefined as u8,
|
||||
];
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for &code in &codes { assert!(seen.insert(code), "duplicate: {}", code); }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_transfer_trb_detects_normal() {
|
||||
let mut trb = test_trb();
|
||||
trb.normal(0, 0, true, 0, 0, false, false, false, false, false, false);
|
||||
assert!(trb.is_transfer_trb());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_command_trb_detects_enable_slot() {
|
||||
let mut trb = test_trb();
|
||||
trb.enable_slot(0, true);
|
||||
assert!(trb.is_command_trb());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completion_code_decode() {
|
||||
let mut trb = test_trb();
|
||||
// set(data, status, control): completion code is in status bits 31:24
|
||||
trb.set(0, (TrbCompletionCode::Stall as u32) << 24, (TrbType::Transfer as u32) << 10 | 1u32);
|
||||
assert_eq!(trb.completion_code(), TrbCompletionCode::Stall as u8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_trb_sets_correct_type() {
|
||||
let mut trb = test_trb();
|
||||
trb.data(0x2000, 64, true, true);
|
||||
assert_eq!(trb.trb_type(), TrbType::DataStage as u8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn link_trb_sets_correct_type() {
|
||||
let mut trb = test_trb();
|
||||
trb.link(0x4000, true, true);
|
||||
assert_eq!(trb.trb_type(), TrbType::Link as u8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trb_setup_stage_address() {
|
||||
// Setup TRB encodes the Setup Request packet in the lower
|
||||
// 24 bits of the status field, with bmRequestType at bits 8-15
|
||||
// and bRequest at bits 0-7. Cross-referenced with Linux 7.1
|
||||
// drivers/usb/host/xhci-ring.c setup_bmRequestType().
|
||||
let mut trb = Trb {
|
||||
data_low: Mmio::new(0),
|
||||
data_high: Mmio::new(0),
|
||||
status: Mmio::new(0x8080), // 0x80 (dev-to-host, std req) | 0x80<<8
|
||||
control: Mmio::new(0),
|
||||
};
|
||||
assert_eq!(trb.status.read(), 0x8080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trb_data_pointer_round_trip() {
|
||||
// Data TRB should preserve its pointer value exactly through
|
||||
// the accessors, which is critical for scatter-gather I/O.
|
||||
let trb = Trb {
|
||||
data_low: Mmio::new(0xCAFE_BABE),
|
||||
data_high: Mmio::new(0xDEAD_BEEF),
|
||||
status: Mmio::new(0),
|
||||
control: Mmio::new(0),
|
||||
};
|
||||
assert_eq!(trb.data_low.read(), 0xCAFE_BABE);
|
||||
assert_eq!(trb.data_high.read(), 0xDEAD_BEEF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trb_completion_status_successful() {
|
||||
// A successful transfer completion has code SUCCESS (1) at
|
||||
// bits 24-31 of the status field. Cross-referenced with Linux 7.1
|
||||
// drivers/usb/host/xhci.h TRB_COMP_USB_SUCCESS.
|
||||
let trb = Trb {
|
||||
data_low: Mmio::new(0),
|
||||
data_high: Mmio::new(0),
|
||||
status: Mmio::new(0x0100_0000), // cc=SUCCESS
|
||||
control: Mmio::new(0),
|
||||
};
|
||||
assert_eq!((trb.status.read() >> 24) & 0xFF, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
[unit]
|
||||
description = "Serial boot marker: services stage"
|
||||
requires_weak = ["06_services.target"]
|
||||
|
||||
[service]
|
||||
cmd = "echo"
|
||||
args = ["RB_STAGE_06_SERVICES"]
|
||||
type = "oneshot"
|
||||
@@ -0,0 +1,8 @@
|
||||
[unit]
|
||||
description = "Serial boot marker: userland stage"
|
||||
requires_weak = ["08_userland.target"]
|
||||
|
||||
[service]
|
||||
cmd = "echo"
|
||||
args = ["RB_STAGE_08_USERLAND"]
|
||||
type = "oneshot"
|
||||
@@ -0,0 +1,10 @@
|
||||
[unit]
|
||||
description = "Evdev input daemon"
|
||||
requires_weak = [
|
||||
"12_boot-late.target",
|
||||
"00_pcid-spawner.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "evdevd"
|
||||
type = "oneshot_async"
|
||||
@@ -7,5 +7,5 @@ requires_weak = [
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "netstack"
|
||||
cmd = "smolnetd"
|
||||
type = "notify"
|
||||
|
||||
+15
-1
@@ -166,14 +166,28 @@ fn main() {
|
||||
}
|
||||
};
|
||||
for entry in entries {
|
||||
let file_name = entry.file_name().unwrap();
|
||||
if file_name.to_str().map_or(false, |name| name.starts_with('.')) {
|
||||
continue;
|
||||
}
|
||||
scheduler.schedule_start_and_report_errors(
|
||||
&mut unit_store,
|
||||
UnitId(entry.file_name().unwrap().to_str().unwrap().to_owned()),
|
||||
UnitId(file_name.to_str().unwrap().to_owned()),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
eprintln!("INIT: starting /usr scheduler step");
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new().write(true).open("/scheme/debug/no-preserve") {
|
||||
use std::io::Write;
|
||||
let _ = writeln!(f, "BEFORE_STEP");
|
||||
}
|
||||
scheduler.step(&mut unit_store, &mut init_config);
|
||||
eprintln!("INIT: waitpid loop");
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new().write(true).open("/scheme/debug/no-preserve") {
|
||||
use std::io::Write;
|
||||
let _ = writeln!(f, "AFTER_STEP");
|
||||
}
|
||||
|
||||
libredox::call::setrens(0, 0).expect("init: failed to enter null namespace");
|
||||
|
||||
|
||||
+25
-3
@@ -1,4 +1,5 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::io::Write;
|
||||
|
||||
use crate::InitConfig;
|
||||
use crate::unit::{Unit, UnitId, UnitKind, UnitStore};
|
||||
@@ -55,8 +56,12 @@ impl Scheduler {
|
||||
}
|
||||
|
||||
pub fn step(&mut self, unit_store: &mut UnitStore, init_config: &mut InitConfig) {
|
||||
let mut defer_count: BTreeMap<String, usize> = BTreeMap::new();
|
||||
'a: loop {
|
||||
let Some(job) = self.pending.pop_front() else {
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new().write(true).open("/scheme/debug/no-preserve") {
|
||||
let _ = writeln!(f, "STEP_DONE");
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -64,16 +69,33 @@ impl Scheduler {
|
||||
JobKind::Start => {
|
||||
let unit = unit_store.unit_mut(&job.unit);
|
||||
|
||||
let mut blocked = false;
|
||||
for dep in &unit.info.requires_weak {
|
||||
for pending_job in &self.pending {
|
||||
if &pending_job.unit == dep {
|
||||
self.pending.push_back(job);
|
||||
continue 'a;
|
||||
blocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if blocked { break; }
|
||||
}
|
||||
|
||||
if blocked {
|
||||
let cnt = defer_count.entry(job.unit.0.clone()).or_insert(0);
|
||||
*cnt += 1;
|
||||
if *cnt <= 3 {
|
||||
self.pending.push_back(job);
|
||||
continue 'a;
|
||||
}
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new().write(true).open("/scheme/debug/no-preserve") {
|
||||
let _ = writeln!(f, "FORCE {}", job.unit.0);
|
||||
}
|
||||
}
|
||||
|
||||
run(unit, init_config);
|
||||
if let Ok(mut f) = std::fs::OpenOptions::new().write(true).open("/scheme/debug/no-preserve") {
|
||||
let _ = writeln!(f, "RAN {}", unit.id.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ fn nonblock(file: &File) -> io::Result<()> {
|
||||
}
|
||||
fn dup(file: &File, buf: &str) -> io::Result<File> {
|
||||
let stream =
|
||||
syscall::dup(file.as_raw_fd() as usize, buf.as_bytes()).map_err(from_syscall_error)?;
|
||||
libredox::call::dup(file.as_raw_fd() as usize, buf.as_bytes()).map_err(from_syscall_error)?;
|
||||
Ok(unsafe { File::from_raw_fd(stream as RawFd) })
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ fn main() -> io::Result<()> {
|
||||
|
||||
loop {
|
||||
let stream =
|
||||
syscall::dup(server.as_raw_fd() as usize, b"listen").map_err(from_syscall_error)?;
|
||||
libredox::call::dup(server.as_raw_fd() as usize, b"listen").map_err(from_syscall_error)?;
|
||||
let mut stream = unsafe { File::from_raw_fd(stream as RawFd) };
|
||||
|
||||
stream.write(b"Hello World!\n")?;
|
||||
|
||||
@@ -38,7 +38,7 @@ fn main() -> io::Result<()> {
|
||||
println!("Listener recevied flags: {:?}", event.flags);
|
||||
if event.flags & syscall::EVENT_WRITE == syscall::EVENT_WRITE {
|
||||
loop {
|
||||
let stream = match syscall::dup(server.as_raw_fd() as usize, b"listen")
|
||||
let stream = match libredox::call::dup(server.as_raw_fd() as usize, b"listen")
|
||||
.map_err(from_syscall_error)
|
||||
{
|
||||
Err(ref err) if err.kind() == io::ErrorKind::WouldBlock => break,
|
||||
|
||||
@@ -20,7 +20,7 @@ fn nonblock(file: &File) -> io::Result<()> {
|
||||
}
|
||||
fn dup(file: &File, buf: &str) -> io::Result<File> {
|
||||
let stream =
|
||||
syscall::dup(file.as_raw_fd() as usize, buf.as_bytes()).map_err(from_syscall_error)?;
|
||||
libredox::call::dup(file.as_raw_fd() as usize, buf.as_bytes()).map_err(from_syscall_error)?;
|
||||
Ok(unsafe { File::from_raw_fd(stream as RawFd) })
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ fn from_syscall_error(error: syscall::Error) -> io::Error {
|
||||
}
|
||||
fn dup(file: &File, buf: &str) -> io::Result<File> {
|
||||
let stream =
|
||||
syscall::dup(file.as_raw_fd() as usize, buf.as_bytes()).map_err(from_syscall_error)?;
|
||||
libredox::call::dup(file.as_raw_fd() as usize, buf.as_bytes()).map_err(from_syscall_error)?;
|
||||
Ok(unsafe { File::from_raw_fd(stream as RawFd) })
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
[package]
|
||||
name = "redbear-netdiag"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,294 @@
|
||||
//! redbear-netdiag — unified network diagnostics with live bandwidth monitoring.
|
||||
//!
|
||||
//! Mirrors the output conventions of Linux's ss, iproute2, and nstat.
|
||||
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
const NETCFG: &str = "/scheme/netcfg";
|
||||
const NETFILTER: &str = "/scheme/netfilter";
|
||||
|
||||
fn read_scheme(path: &str) -> String {
|
||||
fs::read_to_string(path).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn section(title: &str) {
|
||||
println!();
|
||||
println!("═══ {} ═══", title);
|
||||
}
|
||||
|
||||
fn parse_counter(line: &str) -> u64 {
|
||||
line.split('=')
|
||||
.nth(1)
|
||||
.and_then(|s| s.trim().parse().ok())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
struct IfaceStats {
|
||||
rx_bytes: u64,
|
||||
rx_packets: u64,
|
||||
tx_bytes: u64,
|
||||
tx_packets: u64,
|
||||
rx_errors: u64,
|
||||
tx_errors: u64,
|
||||
rx_dropped: u64,
|
||||
tx_dropped: u64,
|
||||
}
|
||||
|
||||
fn read_iface_stats(dev: &str) -> Option<IfaceStats> {
|
||||
let raw = read_scheme(&format!("{NETCFG}/ifaces/{dev}/stats"));
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut s = IfaceStats {
|
||||
rx_bytes: 0, rx_packets: 0, tx_bytes: 0, tx_packets: 0,
|
||||
rx_errors: 0, tx_errors: 0, rx_dropped: 0, tx_dropped: 0,
|
||||
};
|
||||
for line in raw.lines() {
|
||||
if let Some(v) = line.strip_prefix("rx_bytes=") { s.rx_bytes = v.trim().parse().unwrap_or(0); }
|
||||
else if let Some(v) = line.strip_prefix("rx_packets=") { s.rx_packets = v.trim().parse().unwrap_or(0); }
|
||||
else if let Some(v) = line.strip_prefix("tx_bytes=") { s.tx_bytes = v.trim().parse().unwrap_or(0); }
|
||||
else if let Some(v) = line.strip_prefix("tx_packets=") { s.tx_packets = v.trim().parse().unwrap_or(0); }
|
||||
else if let Some(v) = line.strip_prefix("rx_errors=") { s.rx_errors = v.trim().parse().unwrap_or(0); }
|
||||
else if let Some(v) = line.strip_prefix("tx_errors=") { s.tx_errors = v.trim().parse().unwrap_or(0); }
|
||||
else if let Some(v) = line.strip_prefix("rx_dropped=") { s.rx_dropped = v.trim().parse().unwrap_or(0); }
|
||||
else if let Some(v) = line.strip_prefix("tx_dropped=") { s.tx_dropped = v.trim().parse().unwrap_or(0); }
|
||||
}
|
||||
Some(s)
|
||||
}
|
||||
|
||||
fn format_bps(bytes_per_sec: u64) -> String {
|
||||
if bytes_per_sec >= 1_000_000_000 {
|
||||
format!("{:.2} Gbps", bytes_per_sec as f64 * 8.0 / 1_000_000_000.0)
|
||||
} else if bytes_per_sec >= 1_000_000 {
|
||||
format!("{:.2} Mbps", bytes_per_sec as f64 * 8.0 / 1_000_000.0)
|
||||
} else if bytes_per_sec >= 1_000 {
|
||||
format!("{:.2} Kbps", bytes_per_sec as f64 * 8.0 / 1_000.0)
|
||||
} else {
|
||||
format!("{} bps", bytes_per_sec * 8)
|
||||
}
|
||||
}
|
||||
|
||||
fn show_interfaces() {
|
||||
for dev in &["eth0", "lo"] {
|
||||
let addr = read_scheme(&format!("{NETCFG}/ifaces/{dev}/addr/list")).trim().to_string();
|
||||
if addr.is_empty() || addr.starts_with('(') {
|
||||
continue;
|
||||
}
|
||||
let mac = if *dev == "eth0" {
|
||||
read_scheme(&format!("{NETCFG}/ifaces/eth0/mac")).trim().to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let link = read_scheme(&format!("{NETCFG}/ifaces/{dev}/link")).trim().to_string();
|
||||
let mtu = read_scheme(&format!("{NETCFG}/ifaces/{dev}/mtu")).trim().to_string();
|
||||
print!(" {:<6} ", dev);
|
||||
if !mac.is_empty() { print!("mac={:<17} ", mac); }
|
||||
print!("mtu={:<5} ", mtu);
|
||||
print!("link={:<5} ", link);
|
||||
println!("addr={}", addr);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_routes() {
|
||||
let routes = read_scheme(&format!("{NETCFG}/route/list"));
|
||||
for line in routes.lines() {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty() {
|
||||
println!(" {}", trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_arp() {
|
||||
let arp = read_scheme(&format!("{NETCFG}/ifaces/eth0/arp/list"));
|
||||
let mut count = 0;
|
||||
for line in arp.lines() {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty() {
|
||||
println!(" {}", trimmed);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
if count == 0 { println!(" (empty)"); }
|
||||
}
|
||||
|
||||
fn show_dns() {
|
||||
let ns4 = read_scheme(&format!("{NETCFG}/resolv/nameserver")).trim().to_string();
|
||||
print!(" nameserver4: ");
|
||||
if ns4.is_empty() { println!("(not set)"); } else { println!("{}", ns4); }
|
||||
let ns6 = read_scheme(&format!("{NETCFG}/resolv/nameserver6")).trim().to_string();
|
||||
if !ns6.is_empty() {
|
||||
println!(" nameserver6: {}", ns6);
|
||||
}
|
||||
}
|
||||
|
||||
fn show_firewall() {
|
||||
let rules = read_scheme(&format!("{NETFILTER}/rule/list"));
|
||||
for line in rules.lines() {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty() {
|
||||
println!(" {}", trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_conntrack() {
|
||||
let ct = read_scheme(&format!("{NETFILTER}/conntrack/list"));
|
||||
let lines: Vec<&str> = ct.lines().filter(|l| !l.trim().is_empty()).collect();
|
||||
if let Some((first, rest)) = lines.split_first() {
|
||||
println!(" {}", first); // summary line
|
||||
for l in rest {
|
||||
println!(" {}", l);
|
||||
}
|
||||
} else {
|
||||
println!(" (no tracked connections)");
|
||||
}
|
||||
}
|
||||
|
||||
fn show_nat() {
|
||||
let nat = read_scheme(&format!("{NETFILTER}/nat/list"));
|
||||
for line in nat.lines() {
|
||||
let trimmed = line.trim();
|
||||
if !trimmed.is_empty() {
|
||||
println!(" {}", trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_sockets() {
|
||||
let count = read_scheme(&format!("{NETCFG}/sockets/list")).trim().to_string();
|
||||
if !count.is_empty() {
|
||||
println!(" {}", count);
|
||||
} else {
|
||||
println!(" (unavailable)");
|
||||
}
|
||||
}
|
||||
|
||||
fn show_stats() {
|
||||
println!(" Interface statistics:");
|
||||
for dev in &["eth0", "lo"] {
|
||||
if let Some(s) = read_iface_stats(dev) {
|
||||
println!(
|
||||
" {:<6} rx: {:>10} B ({:>6} pkts) tx: {:>10} B ({:>6} pkts)",
|
||||
dev, s.rx_bytes, s.rx_packets, s.tx_bytes, s.tx_packets
|
||||
);
|
||||
if s.rx_errors != 0 || s.tx_errors != 0 || s.rx_dropped != 0 || s.tx_dropped != 0 {
|
||||
println!(
|
||||
" {:>7} errors: rx={} tx={} drops: rx={} tx={}",
|
||||
"", s.rx_errors, s.tx_errors, s.rx_dropped, s.tx_dropped
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ct = read_scheme(&format!("{NETFILTER}/conntrack/list"));
|
||||
let ct_count = ct.lines().filter(|l| l.contains("entries:")).next()
|
||||
.and_then(|l| l.split("entries:").nth(1))
|
||||
.and_then(|s| s.split(',').next())
|
||||
.and_then(|s| s.trim().parse::<usize>().ok())
|
||||
.unwrap_or(0);
|
||||
let over_limit = ct.lines().filter(|l| l.contains("over_limit:")).next()
|
||||
.and_then(|l| l.split("over_limit:").nth(1))
|
||||
.and_then(|s| s.trim().parse::<u64>().ok())
|
||||
.unwrap_or(0);
|
||||
println!(" conntrack: {} active, {} over-limit (SYN flood)", ct_count, over_limit);
|
||||
}
|
||||
|
||||
fn show_bandwidth(duration_secs: u64) {
|
||||
let mut prev: Option<(IfaceStats, IfaceStats)> = None;
|
||||
let interval = Duration::from_secs(1);
|
||||
let iterations = duration_secs;
|
||||
|
||||
println!();
|
||||
println!("═══ LIVE BANDWIDTH ({}s, 1s intervals) ═══", duration_secs);
|
||||
println!(" {:>8} {:>16} {:>16} {:>14} {:>14}",
|
||||
"Time", "eth0 RX", "eth0 TX", "lo RX", "lo TX");
|
||||
|
||||
for i in 1..=iterations {
|
||||
thread::sleep(interval);
|
||||
let eth0 = read_iface_stats("eth0");
|
||||
let lo = read_iface_stats("lo");
|
||||
|
||||
if let (Some(e), Some(l)) = (ð0, &lo) {
|
||||
if let Some((ref pe, ref pl)) = prev {
|
||||
let erx = e.rx_bytes.saturating_sub(pe.rx_bytes);
|
||||
let etx = e.tx_bytes.saturating_sub(pe.tx_bytes);
|
||||
let lrx = l.rx_bytes.saturating_sub(pl.rx_bytes);
|
||||
let ltx = l.tx_bytes.saturating_sub(pl.tx_bytes);
|
||||
println!(
|
||||
" {:>3}s {:>16} {:>16} {:>14} {:>14}",
|
||||
i,
|
||||
format_bps(erx),
|
||||
format_bps(etx),
|
||||
format_bps(lrx),
|
||||
format_bps(ltx),
|
||||
);
|
||||
}
|
||||
}
|
||||
prev = eth0.zip(lo).into();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let brief = args.iter().any(|a| a == "-b" || a == "--brief");
|
||||
let monitor = args.iter().position(|a| a == "-m" || a == "--monitor");
|
||||
let monitor_secs: u64 = monitor.and_then(|i| args.get(i + 1))
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(10);
|
||||
|
||||
if args.iter().any(|a| a == "-w" || a == "--watch") {
|
||||
loop {
|
||||
print!("\x1B[2J\x1B[H");
|
||||
let _ = run_display(true);
|
||||
thread::sleep(Duration::from_secs(monitor_secs));
|
||||
}
|
||||
}
|
||||
|
||||
if args.iter().any(|a| a == "-m" || a == "--monitor") {
|
||||
run_display(brief)?;
|
||||
show_bandwidth(monitor_secs);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
run_display(brief)
|
||||
}
|
||||
|
||||
fn run_display(brief: bool) -> io::Result<()> {
|
||||
if !brief {
|
||||
section("INTERFACES");
|
||||
show_interfaces();
|
||||
|
||||
section("ROUTING TABLE");
|
||||
show_routes();
|
||||
|
||||
section("ARP / NDP CACHE");
|
||||
show_arp();
|
||||
|
||||
section("DNS CONFIGURATION");
|
||||
show_dns();
|
||||
|
||||
section("OPEN SOCKETS");
|
||||
show_sockets();
|
||||
}
|
||||
|
||||
section("FIREWALL RULES");
|
||||
show_firewall();
|
||||
|
||||
section("CONNECTION TRACKING");
|
||||
show_conntrack();
|
||||
|
||||
section("NAT RULES");
|
||||
show_nat();
|
||||
|
||||
if !brief {
|
||||
section("STATISTICS");
|
||||
show_stats();
|
||||
}
|
||||
|
||||
let _ = io::stdout().flush();
|
||||
Ok(())
|
||||
}
|
||||
@@ -30,6 +30,7 @@ features = [
|
||||
"std",
|
||||
"medium-ethernet", "medium-ip",
|
||||
"proto-ipv4",
|
||||
"proto-ipv6",
|
||||
"socket-raw", "socket-icmp", "socket-udp", "socket-tcp", "socket-tcp-cubic",
|
||||
"iface-max-addr-count-8",
|
||||
"log"
|
||||
|
||||
@@ -0,0 +1,842 @@
|
||||
//! Connection tracking hash table — mirrors Linux 7.1's `nf_conntrack`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `include/net/netfilter/nf_conntrack.h:74` — `struct nf_conn`
|
||||
//! - `include/net/netfilter/nf_conntrack_tuple.h` — `struct nf_conntrack_tuple`
|
||||
//! - `net/netfilter/nf_conntrack_core.c` — `resolve_normal_ct()`, `nf_conntrack_in()`
|
||||
//! - `net/netfilter/nf_conntrack_proto_tcp.c` — TCP state machine
|
||||
//! - `net/netfilter/nf_conntrack_proto_udp.c` — UDP state tracking
|
||||
//!
|
||||
//! The connection is identified by a 5-tuple (src/dst addr, src/dst port, protocol)
|
||||
//! plus the L3 protocol number. Both directions are tracked:
|
||||
//! orig: from initiator → responder
|
||||
//! reply: from responder → initiator
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::vec::Vec;
|
||||
use core::hash::{Hash, Hasher};
|
||||
use smoltcp::time::{Duration, Instant};
|
||||
use smoltcp::wire::IpAddress;
|
||||
|
||||
fn is_syn(ctx: &PacketContext, l3num: u8) -> bool {
|
||||
// TCP flags byte is at offset 13 in the TCP header.
|
||||
// TCP header starts after the IP header: 20 bytes for IPv4, 40 for IPv6.
|
||||
let tcp_offset = if l3num == 4 { 20 } else { 40 };
|
||||
if ctx.packet.len() <= tcp_offset + 13 {
|
||||
return false;
|
||||
}
|
||||
let flags = ctx.packet[tcp_offset + 13];
|
||||
(flags & 0x02) != 0 && (flags & 0x10) == 0
|
||||
}
|
||||
|
||||
fn tcp_flags(ctx: &PacketContext, l3num: u8) -> u8 {
|
||||
let tcp_offset = if l3num == 4 { 20 } else { 40 };
|
||||
if ctx.packet.len() <= tcp_offset + 13 {
|
||||
return 0;
|
||||
}
|
||||
ctx.packet[tcp_offset + 13]
|
||||
}
|
||||
|
||||
fn is_fin(flags: u8) -> bool {
|
||||
(flags & 0x01) != 0
|
||||
}
|
||||
|
||||
fn is_rst(flags: u8) -> bool {
|
||||
(flags & 0x04) != 0
|
||||
}
|
||||
|
||||
/// ICMP echo request detection (Type 8 for ICMPv4, Type 128 for ICMPv6).
|
||||
/// Returns true if the packet is an ICMP echo request (Type 8 for ICMPv4,
|
||||
/// Type 128 for ICMPv6). The packet in `ctx.packet` is the IP packet, so the
|
||||
/// ICMP type field is at offset `ihl` (after the IP header).
|
||||
fn is_echo_request(ctx: &PacketContext) -> bool {
|
||||
if ctx.packet.len() < 2 {
|
||||
return false;
|
||||
}
|
||||
let icmp_offset = match ctx.protocol {
|
||||
1 => {
|
||||
let ihl = (ctx.packet[0] & 0x0f) as usize * 4;
|
||||
if ctx.packet.len() < ihl + 1 {
|
||||
return false;
|
||||
}
|
||||
ihl
|
||||
}
|
||||
58 => 40,
|
||||
_ => return false,
|
||||
};
|
||||
let icmp_type = ctx.packet[icmp_offset];
|
||||
icmp_type == 8 || icmp_type == 128
|
||||
}
|
||||
|
||||
use super::{PacketContext, Verdict};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConnState {
|
||||
New,
|
||||
Established,
|
||||
Related,
|
||||
OverLimit,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ConnKey {
|
||||
pub l3num: u8,
|
||||
pub l4proto: u8,
|
||||
pub src_addr: IpAddress,
|
||||
pub dst_addr: IpAddress,
|
||||
pub src_port: u16,
|
||||
pub dst_port: u16,
|
||||
}
|
||||
|
||||
impl Hash for ConnKey {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.l3num.hash(state);
|
||||
self.l4proto.hash(state);
|
||||
self.src_port.hash(state);
|
||||
self.dst_port.hash(state);
|
||||
match self.src_addr {
|
||||
IpAddress::Ipv4(a) => u32::from(a).hash(state),
|
||||
IpAddress::Ipv6(a) => a.octets().hash(state),
|
||||
}
|
||||
match self.dst_addr {
|
||||
IpAddress::Ipv4(a) => u32::from(a).hash(state),
|
||||
IpAddress::Ipv6(a) => a.octets().hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnKey {
|
||||
pub fn reply(&self) -> Self {
|
||||
Self {
|
||||
l3num: self.l3num,
|
||||
l4proto: self.l4proto,
|
||||
src_addr: self.dst_addr,
|
||||
dst_addr: self.src_addr,
|
||||
src_port: self.dst_port,
|
||||
dst_port: self.src_port,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_context(l3num: u8, ctx: &PacketContext) -> Self {
|
||||
Self {
|
||||
l3num,
|
||||
l4proto: ctx.protocol,
|
||||
src_addr: ctx.src_addr,
|
||||
dst_addr: ctx.dst_addr,
|
||||
src_port: ctx.src_port.unwrap_or(0),
|
||||
dst_port: ctx.dst_port.unwrap_or(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// TCP connection tracking states (mirrors `enum tcp_conntrack`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum TcpTracking {
|
||||
None,
|
||||
SynSent,
|
||||
SynRecv,
|
||||
Established,
|
||||
FinWait,
|
||||
TimeWait,
|
||||
Close,
|
||||
}
|
||||
|
||||
/// A single connection tracking entry (mirrors `struct nf_conn`).
|
||||
#[derive(Debug, Clone)]
|
||||
struct ConnEntry {
|
||||
key: ConnKey,
|
||||
reply_key: ConnKey,
|
||||
state: ConnState,
|
||||
tcp_state: TcpTracking,
|
||||
fin_from_orig: bool,
|
||||
timeout: Instant,
|
||||
orig_packets: u64,
|
||||
orig_bytes: u64,
|
||||
reply_packets: u64,
|
||||
reply_bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConntrackTable {
|
||||
entries: BTreeMap<ConnKey, ConnEntry>,
|
||||
last_cleanup: Option<Instant>,
|
||||
syn_rate_limits: BTreeMap<IpAddress, (u32, Instant)>,
|
||||
icmp_rate_limits: BTreeMap<IpAddress, (u32, Instant)>,
|
||||
over_limit_count: u64,
|
||||
}
|
||||
|
||||
fn advance_entry_state(entry: &mut ConnEntry, is_orig: bool, ctx: &PacketContext, now: Instant) -> bool {
|
||||
if entry.key.l4proto != 6 {
|
||||
return false;
|
||||
}
|
||||
let flags = tcp_flags(ctx, entry.key.l3num);
|
||||
let res_rst = is_rst(flags);
|
||||
|
||||
// RST closes the connection immediately.
|
||||
if res_rst {
|
||||
entry.tcp_state = TcpTracking::Close;
|
||||
entry.state = ConnState::New;
|
||||
entry.timeout = now + Duration::from_secs(10);
|
||||
return true;
|
||||
}
|
||||
|
||||
let res_fin = is_fin(flags);
|
||||
|
||||
if is_orig {
|
||||
// Original direction: handshake completion and FIN teardown.
|
||||
match entry.tcp_state {
|
||||
TcpTracking::SynRecv => {
|
||||
entry.tcp_state = TcpTracking::Established;
|
||||
entry.state = ConnState::Established;
|
||||
entry.timeout = now + Duration::from_secs(432000);
|
||||
return true;
|
||||
}
|
||||
TcpTracking::Established if res_fin => {
|
||||
entry.tcp_state = TcpTracking::FinWait;
|
||||
entry.fin_from_orig = true;
|
||||
entry.timeout = now + Duration::from_secs(120);
|
||||
return true;
|
||||
}
|
||||
TcpTracking::FinWait if res_fin && !entry.fin_from_orig => {
|
||||
// Second FIN from reply direction (orig FIN was first)
|
||||
entry.tcp_state = TcpTracking::TimeWait;
|
||||
entry.timeout = now + Duration::from_secs(120);
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
// Reply direction: handshake initiation and close.
|
||||
match entry.tcp_state {
|
||||
TcpTracking::None if (flags & 0x12) == 0x12 => {
|
||||
entry.tcp_state = TcpTracking::SynRecv;
|
||||
entry.timeout = now + Duration::from_secs(60);
|
||||
return true;
|
||||
}
|
||||
TcpTracking::SynSent => {
|
||||
entry.tcp_state = TcpTracking::SynRecv;
|
||||
entry.timeout = now + Duration::from_secs(60);
|
||||
return true;
|
||||
}
|
||||
TcpTracking::Established if res_fin => {
|
||||
entry.tcp_state = TcpTracking::FinWait;
|
||||
entry.fin_from_orig = false;
|
||||
entry.timeout = now + Duration::from_secs(120);
|
||||
return true;
|
||||
}
|
||||
TcpTracking::FinWait if res_fin && entry.fin_from_orig => {
|
||||
// Second FIN from orig direction (reply FIN was first)
|
||||
entry.tcp_state = TcpTracking::TimeWait;
|
||||
entry.timeout = now + Duration::from_secs(120);
|
||||
return true;
|
||||
}
|
||||
TcpTracking::TimeWait => {
|
||||
entry.timeout = now + Duration::from_secs(120);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
impl ConntrackTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: BTreeMap::new(),
|
||||
last_cleanup: None,
|
||||
syn_rate_limits: BTreeMap::new(),
|
||||
icmp_rate_limits: BTreeMap::new(),
|
||||
over_limit_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track(&mut self, ctx: &PacketContext, now: Instant) -> ConnState {
|
||||
let l3num = match ctx.src_addr {
|
||||
IpAddress::Ipv4(_) => 4u8,
|
||||
IpAddress::Ipv6(_) => 6u8,
|
||||
};
|
||||
if ctx.protocol != 6 && ctx.protocol != 17 && ctx.protocol != 1 && ctx.protocol != 58 {
|
||||
return ConnState::New;
|
||||
}
|
||||
|
||||
if ctx.protocol == 6 && is_syn(ctx, l3num) && self.check_syn_limit(ctx.src_addr, now) {
|
||||
self.over_limit_count = self.over_limit_count.saturating_add(1);
|
||||
return ConnState::OverLimit;
|
||||
}
|
||||
|
||||
if (ctx.protocol == 1 || ctx.protocol == 58) && is_echo_request(ctx)
|
||||
&& self.check_icmp_limit(ctx.src_addr, now)
|
||||
{
|
||||
self.over_limit_count = self.over_limit_count.saturating_add(1);
|
||||
return ConnState::OverLimit;
|
||||
}
|
||||
|
||||
if ctx.protocol == 1 || ctx.protocol == 58 {
|
||||
return self.track_icmp(ctx, l3num, now);
|
||||
}
|
||||
|
||||
let key = ConnKey::from_context(l3num, ctx);
|
||||
let reply_key = key.reply();
|
||||
|
||||
// First check if this packet belongs to an existing reply flow
|
||||
let (is_orig, entry_key) = if let Some(entry) = self.entries.get_mut(&reply_key) {
|
||||
entry.reply_packets = entry.reply_packets.saturating_add(1);
|
||||
entry.reply_bytes = entry.reply_bytes.saturating_add(ctx.packet.len() as u64);
|
||||
advance_entry_state(entry, false, ctx, now);
|
||||
return entry.state;
|
||||
} else {
|
||||
(true, key.clone())
|
||||
};
|
||||
|
||||
if let Some(entry) = self.entries.get_mut(&entry_key) {
|
||||
entry.orig_packets = entry.orig_packets.saturating_add(1);
|
||||
entry.orig_bytes = entry.orig_bytes.saturating_add(ctx.packet.len() as u64);
|
||||
advance_entry_state(entry, true, ctx, now);
|
||||
return entry.state;
|
||||
}
|
||||
|
||||
// New connection (mirrors `nf_conntrack_in`)
|
||||
let state = ConnState::New;
|
||||
let tcp_state = if ctx.protocol == 6 {
|
||||
let flags = if ctx.packet.len() >= 34 {
|
||||
let tcp_offset = if l3num == 4 { 20 } else { 40 };
|
||||
if ctx.packet.len() > tcp_offset + 13 {
|
||||
ctx.packet[tcp_offset + 13]
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if flags & 0x02 != 0 && flags & 0x10 == 0 {
|
||||
TcpTracking::SynSent
|
||||
} else {
|
||||
TcpTracking::None
|
||||
}
|
||||
} else {
|
||||
TcpTracking::None
|
||||
};
|
||||
|
||||
let timeout = if ctx.protocol == 17 {
|
||||
Duration::from_secs(30)
|
||||
} else {
|
||||
Duration::from_secs(60)
|
||||
};
|
||||
|
||||
self.entries.insert(
|
||||
key.clone(),
|
||||
ConnEntry {
|
||||
key: key.clone(),
|
||||
reply_key,
|
||||
state,
|
||||
tcp_state,
|
||||
fin_from_orig: false,
|
||||
timeout: now + timeout,
|
||||
orig_packets: 1,
|
||||
orig_bytes: ctx.packet.len() as u64,
|
||||
reply_packets: 0,
|
||||
reply_bytes: 0,
|
||||
},
|
||||
);
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
fn track_icmp(&mut self, ctx: &PacketContext, l3num: u8, now: Instant) -> ConnState {
|
||||
if ctx.packet.len() < 4 {
|
||||
return ConnState::New;
|
||||
}
|
||||
let icmp_offset = match l3num {
|
||||
4 => (ctx.packet[0] & 0x0f) as usize * 4,
|
||||
6 => 40,
|
||||
_ => return ConnState::New,
|
||||
};
|
||||
if ctx.packet.len() < icmp_offset + 6 {
|
||||
return ConnState::New;
|
||||
}
|
||||
let icmp_type = ctx.packet[icmp_offset];
|
||||
let icmp_code = ctx.packet[icmp_offset + 1];
|
||||
let icmp_id = u16::from_be_bytes([
|
||||
ctx.packet[icmp_offset + 4],
|
||||
ctx.packet[icmp_offset + 5],
|
||||
]);
|
||||
|
||||
let is_echo = icmp_type == 8 || icmp_type == 128;
|
||||
let is_echo_reply = icmp_type == 0 || icmp_type == 129;
|
||||
let is_error = (l3num == 4 && icmp_type == 3) // ICMPv4 Dest Unreachable
|
||||
|| (l3num == 4 && icmp_type == 11) // ICMPv4 Time Exceeded
|
||||
|| (l3num == 6 && icmp_type == 1) // ICMPv6 Dest Unreachable
|
||||
|| (l3num == 6 && icmp_type == 3); // ICMPv6 Time Exceeded
|
||||
|
||||
// ICMP errors carry the original packet. Try to extract the
|
||||
// embedded connection tuple so we can relate it to an existing
|
||||
// tracked connection (mirrors nf_conntrack_icmp_error()).
|
||||
if is_error {
|
||||
return self.track_icmp_error(ctx, l3num, icmp_offset, now);
|
||||
}
|
||||
|
||||
if !is_echo && !is_echo_reply {
|
||||
return ConnState::New;
|
||||
}
|
||||
|
||||
let key = ConnKey {
|
||||
l3num,
|
||||
l4proto: ctx.protocol,
|
||||
src_addr: ctx.src_addr,
|
||||
dst_addr: ctx.dst_addr,
|
||||
src_port: icmp_id,
|
||||
dst_port: icmp_type as u16,
|
||||
};
|
||||
|
||||
if is_echo {
|
||||
if self.entries.contains_key(&key) {
|
||||
return ConnState::Established;
|
||||
}
|
||||
self.entries.insert(
|
||||
key.clone(),
|
||||
ConnEntry {
|
||||
key: key.clone(),
|
||||
reply_key: ConnKey {
|
||||
src_addr: ctx.dst_addr,
|
||||
dst_addr: ctx.src_addr,
|
||||
src_port: icmp_id,
|
||||
dst_port: (if icmp_type == 8 { 0u8 } else { 129u8 }) as u16,
|
||||
..key
|
||||
},
|
||||
state: ConnState::New,
|
||||
tcp_state: TcpTracking::None,
|
||||
fin_from_orig: false,
|
||||
timeout: now + Duration::from_secs(30),
|
||||
orig_packets: 1,
|
||||
orig_bytes: ctx.packet.len() as u64,
|
||||
reply_packets: 0,
|
||||
reply_bytes: 0,
|
||||
},
|
||||
);
|
||||
return ConnState::New;
|
||||
}
|
||||
|
||||
let reply_key = ConnKey {
|
||||
src_addr: ctx.dst_addr,
|
||||
dst_addr: ctx.src_addr,
|
||||
src_port: icmp_id,
|
||||
dst_port: (if is_echo_reply && icmp_type == 0 { 8u8 } else { 128u8 }) as u16,
|
||||
..key
|
||||
};
|
||||
|
||||
if let Some(entry) = self.entries.get_mut(&reply_key) {
|
||||
entry.reply_packets = entry.reply_packets.saturating_add(1);
|
||||
entry.reply_bytes = entry.reply_bytes.saturating_add(ctx.packet.len() as u64);
|
||||
entry.state = ConnState::Established;
|
||||
entry.timeout = now + Duration::from_secs(30);
|
||||
return ConnState::Established;
|
||||
}
|
||||
ConnState::New
|
||||
}
|
||||
|
||||
/// Process an ICMP error message. Extracts the embedded connection
|
||||
/// tuple from the original IP header carried in the ICMP payload.
|
||||
/// Returns ConnState::Related if the embedded connection matches an
|
||||
/// existing tracked connection (mirrors nf_conntrack_icmp_error()).
|
||||
fn track_icmp_error(
|
||||
&mut self,
|
||||
ctx: &PacketContext,
|
||||
l3num: u8,
|
||||
icmp_offset: usize,
|
||||
now: Instant,
|
||||
) -> ConnState {
|
||||
// ICMP error payload: 4 bytes unused + original IP header + 8 bytes
|
||||
let inner_ip_start = icmp_offset + 8;
|
||||
if ctx.packet.len() < inner_ip_start + 20 {
|
||||
return ConnState::Error;
|
||||
}
|
||||
// Parse the inner IPv4 header.
|
||||
let inner = &ctx.packet[inner_ip_start..];
|
||||
if inner.len() < 20 || (inner[0] >> 4) != 4 {
|
||||
return ConnState::Error;
|
||||
}
|
||||
let inner_proto = inner[9];
|
||||
let inner_src = IpAddress::v4(inner[12], inner[13], inner[14], inner[15]);
|
||||
let inner_dst = IpAddress::v4(inner[16], inner[17], inner[18], inner[19]);
|
||||
let ihl = (inner[0] & 0x0f) as usize * 4;
|
||||
if inner.len() < ihl + 4 || inner_proto != 6 && inner_proto != 17 {
|
||||
return ConnState::Error;
|
||||
}
|
||||
let sport = u16::from_be_bytes([inner[ihl], inner[ihl + 1]]);
|
||||
let dport = u16::from_be_bytes([inner[ihl + 2], inner[ihl + 3]]);
|
||||
|
||||
// Build the inner connection key and check for a match.
|
||||
let inner_key = ConnKey {
|
||||
l3num: 4,
|
||||
l4proto: inner_proto,
|
||||
src_addr: inner_src,
|
||||
dst_addr: inner_dst,
|
||||
src_port: sport,
|
||||
dst_port: dport,
|
||||
};
|
||||
|
||||
if self.entries.contains_key(&inner_key) {
|
||||
ConnState::Related
|
||||
} else {
|
||||
ConnState::Error
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clean_expired(&mut self, now: Instant) {
|
||||
if let Some(last) = self.last_cleanup {
|
||||
if now < last + Duration::from_secs(1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.last_cleanup = Some(now);
|
||||
let expired: Vec<ConnKey> = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|(_, e)| e.timeout < now)
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect();
|
||||
for key in expired {
|
||||
self.entries.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.entries.len()
|
||||
}
|
||||
|
||||
/// Per-protocol and per-state connection breakdown.
|
||||
/// Returns lines like:
|
||||
/// tcp_entries: N (est=N syn=N syn_recv=N fin=N tw=N close=N)
|
||||
/// udp_entries: N
|
||||
/// icmp_entries: N
|
||||
/// over_limit: N
|
||||
/// total_entries: N
|
||||
pub fn stats(&self) -> alloc::string::String {
|
||||
let mut tcp = 0u32;
|
||||
let mut tcp_est = 0u32;
|
||||
let mut tcp_syn = 0u32;
|
||||
let mut tcp_syn_recv = 0u32;
|
||||
let mut tcp_fin = 0u32;
|
||||
let mut tcp_tw = 0u32;
|
||||
let mut tcp_close = 0u32;
|
||||
let mut udp = 0u32;
|
||||
let mut icmp = 0u32;
|
||||
|
||||
for entry in self.entries.values() {
|
||||
match entry.key.l4proto {
|
||||
6 => {
|
||||
tcp += 1;
|
||||
match entry.tcp_state {
|
||||
TcpTracking::None => {}
|
||||
TcpTracking::SynSent => tcp_syn += 1,
|
||||
TcpTracking::SynRecv => tcp_syn_recv += 1,
|
||||
TcpTracking::Established => tcp_est += 1,
|
||||
TcpTracking::FinWait => tcp_fin += 1,
|
||||
TcpTracking::TimeWait => tcp_tw += 1,
|
||||
TcpTracking::Close => tcp_close += 1,
|
||||
}
|
||||
}
|
||||
17 => udp += 1,
|
||||
1 | 58 => icmp += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut out = alloc::format!(
|
||||
"tcp_entries: {} (est={} syn={} syn_recv={} fin={} tw={} close={})\n",
|
||||
tcp, tcp_est, tcp_syn, tcp_syn_recv, tcp_fin, tcp_tw, tcp_close
|
||||
);
|
||||
out.push_str(&alloc::format!("udp_entries: {}\n", udp));
|
||||
out.push_str(&alloc::format!("icmp_entries: {}\n", icmp));
|
||||
out.push_str(&alloc::format!("over_limit: {}\n", self.over_limit_count));
|
||||
out.push_str(&alloc::format!("total_entries: {}\n", self.entries.len()));
|
||||
out
|
||||
}
|
||||
|
||||
fn check_syn_limit(&mut self, src: IpAddress, now: Instant) -> bool {
|
||||
const SYN_LIMIT: u32 = 100;
|
||||
const SYN_WINDOW: Duration = Duration::from_secs(1);
|
||||
|
||||
let entry = self.syn_rate_limits.entry(src).or_insert((0, now));
|
||||
if now > entry.1 + SYN_WINDOW {
|
||||
*entry = (1, now);
|
||||
} else {
|
||||
entry.0 += 1;
|
||||
}
|
||||
entry.0 > SYN_LIMIT
|
||||
}
|
||||
|
||||
fn check_icmp_limit(&mut self, src: IpAddress, now: Instant) -> bool {
|
||||
const ICMP_LIMIT: u32 = 20;
|
||||
const ICMP_WINDOW: Duration = Duration::from_secs(1);
|
||||
|
||||
let entry = self.icmp_rate_limits.entry(src).or_insert((0, now));
|
||||
if now > entry.1 + ICMP_WINDOW {
|
||||
*entry = (1, now);
|
||||
} else {
|
||||
entry.0 += 1;
|
||||
}
|
||||
entry.0 > ICMP_LIMIT
|
||||
}
|
||||
|
||||
pub fn format(&self) -> alloc::string::String {
|
||||
let mut out = alloc::format!("conntrack entries: {}, over_limit: {}\n", self.entries.len(), self.over_limit_count);
|
||||
for entry in self.entries.values() {
|
||||
let tcp_info = if entry.key.l4proto == 6 {
|
||||
alloc::format!(" tcp={:?}", entry.tcp_state)
|
||||
} else {
|
||||
alloc::string::String::new()
|
||||
};
|
||||
out.push_str(&alloc::format!(
|
||||
" {:?}{} src={} dst={} sport={} dport={} orig_pkts={} orig_bytes={} reply_pkts={} reply_bytes={}\n",
|
||||
entry.state,
|
||||
tcp_info,
|
||||
entry.key.src_addr,
|
||||
entry.key.dst_addr,
|
||||
entry.key.src_port,
|
||||
entry.key.dst_port,
|
||||
entry.orig_packets,
|
||||
entry.orig_bytes,
|
||||
entry.reply_packets,
|
||||
entry.reply_bytes,
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::filter::Hook;
|
||||
|
||||
fn make_tcp_syn_packet() -> Vec<u8> {
|
||||
let mut p = vec![0u8; 64];
|
||||
// IPv4 header
|
||||
p[0] = 0x45; // version 4, IHL 5
|
||||
p[9] = 6; // protocol = TCP
|
||||
// TCP header at offset 20
|
||||
p[20] = 0x50; p[21] = 0x00; // src port 0x5000 = 20480
|
||||
p[22] = 0x50; p[23] = 0x00; // dst port
|
||||
// TCP flags at offset 33 (20 + 13)
|
||||
p[33] = 0x02; // SYN
|
||||
p
|
||||
}
|
||||
|
||||
fn make_tcp_data_packet() -> Vec<u8> {
|
||||
let mut p = vec![0u8; 64];
|
||||
p[0] = 0x45;
|
||||
p[9] = 6;
|
||||
p[33] = 0x10; // ACK only (not SYN)
|
||||
p
|
||||
}
|
||||
|
||||
fn make_icmp_echo_packet() -> Vec<u8> {
|
||||
let mut p = vec![0u8; 64];
|
||||
p[0] = 0x45;
|
||||
p[9] = 1; // protocol = ICMP
|
||||
// ICMP header at offset 20
|
||||
p[20] = 8; // echo request
|
||||
p
|
||||
}
|
||||
|
||||
fn make_ctx<'a>(protocol: u8, packet: &'a [u8]) -> PacketContext<'a> {
|
||||
PacketContext {
|
||||
hook: Hook::InputLocal,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: IpAddress::v4(10, 0, 0, 1),
|
||||
dst_addr: IpAddress::v4(10, 0, 0, 2),
|
||||
protocol,
|
||||
src_port: Some(1234),
|
||||
dst_port: Some(80),
|
||||
packet,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_v6_ctx<'a>(protocol: u8, packet: &'a [u8]) -> PacketContext<'a> {
|
||||
PacketContext {
|
||||
hook: Hook::InputLocal,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: IpAddress::v6(0xfe80, 0, 0, 0, 0, 0, 0, 1),
|
||||
dst_addr: IpAddress::v6(0xfe80, 0, 0, 0, 0, 0, 0, 2),
|
||||
protocol,
|
||||
src_port: Some(1234),
|
||||
dst_port: Some(80),
|
||||
packet,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syn_limit_triggers_after_threshold() {
|
||||
let mut ct = ConntrackTable::new();
|
||||
let packet = make_tcp_syn_packet();
|
||||
let now = Instant::from_secs(0);
|
||||
let mut over_limit_count = 0;
|
||||
for i in 0..150 {
|
||||
let ctx = make_ctx(6, &packet);
|
||||
if ct.track(&ctx, now) == ConnState::OverLimit {
|
||||
over_limit_count += 1;
|
||||
}
|
||||
let _ = i;
|
||||
}
|
||||
assert!(over_limit_count >= 50,
|
||||
"Should trigger SYN limit after 100 SYNs/sec; got {} over_limit",
|
||||
over_limit_count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icmp_limit_triggers_after_threshold() {
|
||||
let mut ct = ConntrackTable::new();
|
||||
let packet = make_icmp_echo_packet();
|
||||
let now = Instant::from_secs(0);
|
||||
let mut over_limit_count = 0;
|
||||
for _i in 0..50 {
|
||||
let ctx = make_ctx(1, &packet);
|
||||
if ct.track(&ctx, now) == ConnState::OverLimit {
|
||||
over_limit_count += 1;
|
||||
}
|
||||
}
|
||||
assert!(over_limit_count >= 25,
|
||||
"Should trigger ICMP echo limit after 20/sec; got {} over_limit",
|
||||
over_limit_count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syn_and_icmp_have_independent_budgets() {
|
||||
// After R37 fix, separate maps — TCP SYN traffic shouldn't
|
||||
// affect ICMP echo budget and vice versa.
|
||||
let mut ct = ConntrackTable::new();
|
||||
let tcp = make_tcp_syn_packet();
|
||||
let icmp = make_icmp_echo_packet();
|
||||
let now = Instant::from_secs(0);
|
||||
// Burn through TCP SYN budget
|
||||
for _i in 0..150 {
|
||||
let ctx = make_ctx(6, &tcp);
|
||||
let _ = ct.track(&ctx, now);
|
||||
}
|
||||
// ICMP should still have its full budget — first 20 echo requests
|
||||
// should not be limited, only the 21st onward
|
||||
let mut not_over_limit = 0;
|
||||
let mut over_limit = 0;
|
||||
for _i in 0..30 {
|
||||
let ctx = make_ctx(1, &icmp);
|
||||
if ct.track(&ctx, now) == ConnState::OverLimit {
|
||||
over_limit += 1;
|
||||
} else {
|
||||
not_over_limit += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(not_over_limit, 20,
|
||||
"ICMP budget should be independent — first 20 should pass even after TCP limit exhausted");
|
||||
assert_eq!(over_limit, 10,
|
||||
"Remaining 10 ICMP echoes (21-30) should be over_limit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_syn_tcp_does_not_count_against_syn_limit() {
|
||||
// Pure ACK packets shouldn't count against SYN flood budget.
|
||||
let mut ct = ConntrackTable::new();
|
||||
let ack = make_tcp_data_packet();
|
||||
let now = Instant::from_secs(0);
|
||||
let mut over_limit = 0;
|
||||
for _i in 0..200 {
|
||||
let ctx = make_ctx(6, &ack);
|
||||
if ct.track(&ctx, now) == ConnState::OverLimit {
|
||||
over_limit += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(over_limit, 0,
|
||||
"Non-SYN TCP packets must not trigger SYN flood limit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_syn_detection_works() {
|
||||
// Regression test for the IPv4-only `is_syn()` bug.
|
||||
// Before fix: offset 33 was hardcoded, reading IPv6 header
|
||||
// bytes instead of TCP flags (which are at offset 53 for IPv6).
|
||||
let mut ct = ConntrackTable::new();
|
||||
let mut p = vec![0u8; 80];
|
||||
// IPv6 header: version 6 at byte 0 (0x60), next header = 6 (TCP) at byte 6
|
||||
p[0] = 0x60;
|
||||
p[6] = 6; // next header = TCP
|
||||
// TCP header at offset 40, flags at offset 53
|
||||
p[53] = 0x02; // SYN
|
||||
let now = Instant::from_secs(0);
|
||||
// The l3num is inferred from ctx.src_addr (v6 → l3num=6).
|
||||
let mut over_limit_count = 0;
|
||||
for _i in 0..150 {
|
||||
let ctx = make_v6_ctx(6, &p);
|
||||
if ct.track(&ctx, now) == ConnState::OverLimit {
|
||||
over_limit_count += 1;
|
||||
}
|
||||
}
|
||||
assert!(over_limit_count >= 50,
|
||||
"IPv6 SYN must trigger rate limit after threshold; got {}",
|
||||
over_limit_count);
|
||||
}
|
||||
|
||||
// Helper for building TCP packets with specific flags.
|
||||
fn make_tcp_pkt(flags: u8) -> Vec<u8> {
|
||||
let mut p = vec![0u8; 64];
|
||||
p[0] = 0x45; p[9] = 6; // IPv4 TCP
|
||||
p[12] = 10; p[13] = 0; p[14] = 0; p[15] = 1;
|
||||
p[16] = 10; p[17] = 0; p[18] = 0; p[19] = 2;
|
||||
p[33] = flags;
|
||||
p
|
||||
}
|
||||
|
||||
fn make_reply_ctx<'a>(packet: &'a [u8]) -> PacketContext<'a> {
|
||||
PacketContext {
|
||||
hook: Hook::InputLocal, in_dev: None, out_dev: None,
|
||||
src_addr: IpAddress::v4(10, 0, 0, 2),
|
||||
dst_addr: IpAddress::v4(10, 0, 0, 1),
|
||||
protocol: 6, src_port: Some(80), dst_port: Some(1234),
|
||||
packet,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rst_forces_state_to_new() {
|
||||
// RST forces the connection state back to New and resets to Close tracking.
|
||||
let mut ct = ConntrackTable::new();
|
||||
let now = Instant::from_secs(0);
|
||||
let syn = make_tcp_syn_packet();
|
||||
let _ = ct.track(&make_ctx(6, &syn), now);
|
||||
// Send RST.
|
||||
let rst = make_tcp_pkt(0x04); // FIN flag = 0x01, RST = 0x04
|
||||
let _ = ct.track(&make_ctx(6, &rst), Instant::from_secs(1));
|
||||
// Verify that the entry was closed (no entries remain due to short timeout).
|
||||
// The state machine should set tcp_state=Close with a 10s timeout.
|
||||
// After track() processes the RST, the entry transitions to
|
||||
// ConnState::New (so it won't match 'Established' rules) but
|
||||
// stays in memory for 10 seconds until cleanup.
|
||||
assert_eq!(ct.len(), 1,
|
||||
"RST connection entry stays in memory for 10s cleanup window");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fin_transitions_established_to_timewait() {
|
||||
// Test that FIN from both directions transitions through
|
||||
// FinWait to TimeWait.
|
||||
let mut ct = ConntrackTable::new();
|
||||
let now = Instant::from_secs(0);
|
||||
let syn = make_tcp_syn_packet();
|
||||
let _ = ct.track(&make_ctx(6, &syn), now);
|
||||
// Reply FIN (from responder).
|
||||
let fin = make_tcp_pkt(0x01);
|
||||
let _ = ct.track(&make_reply_ctx(&fin), now);
|
||||
// Original FIN (from initiator).
|
||||
let _ = ct.track(&make_ctx(6, &fin), now);
|
||||
// The connection should have advanced past FinWait via
|
||||
// the two FINs. The state is now TimeWait.
|
||||
// Verify that the entry exists (TimeWait has 120s timeout).
|
||||
assert!(ct.len() > 0,
|
||||
"TimeWait state should keep the connection alive for 120s");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
//! Netfilter-style packet filter for Red Bear OS netstack.
|
||||
//!
|
||||
//! This module mirrors the architecture of Linux 7.1's netfilter subsystem
|
||||
//! (`net/netfilter/core.c`, `net/ipv4/netfilter/iptable_filter.c`).
|
||||
//!
|
||||
//! Mapping to Linux 7.1:
|
||||
//! - [`Hook`] mirrors `enum nf_inet_hooks` (`include/uapi/linux/netfilter.h:42`)
|
||||
//! - [`Verdict`] mirrors the `NF_DROP` / `NF_ACCEPT` constants
|
||||
//! (`include/uapi/linux/netfilter.h:11-17`)
|
||||
//! - [`FilterEngine`] mirrors the role of `nf_iterate` + `xt_table`
|
||||
//! - [`FilterRule`] mirrors `ipt_entry` + `ipt_entry_match` + `ipt_entry_target`
|
||||
//! (`net/ipv4/netfilter/ip_tables.h`)
|
||||
//!
|
||||
//! The filter is stateless (no conntrack in this initial revision). A future
|
||||
//! revision will add a conntrack hash table analogous to `nf_conn` in
|
||||
//! `net/netfilter/nf_conntrack_core.c`.
|
||||
|
||||
mod conntrack;
|
||||
mod nat;
|
||||
mod rule;
|
||||
mod table;
|
||||
|
||||
pub use conntrack::{ConnState, ConntrackTable};
|
||||
pub use nat::{NatBinding, NatRule, NatTable, NatType, rewrite_src_ipv4, parse_nat_rule};
|
||||
pub use rule::{FilterRule, MatchResult, Protocol, StateMatch};
|
||||
pub use table::{FilterTable, parse_rule};
|
||||
|
||||
/// The five netfilter hook points (mirrors `enum nf_inet_hooks`).
|
||||
///
|
||||
/// Only `InputLocal` and `OutputLocal` are wired in this revision. The other
|
||||
/// three are defined for future expansion: `PreRouting` / `Forward` /
|
||||
/// `PostRouting` are required for routing/firewalling of transit traffic
|
||||
/// (when Red Bear gains multi-homed forwarding in Phase 6).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Hook {
|
||||
/// `NF_INET_PRE_ROUTING` — packet just arrived from a NIC, before the
|
||||
/// routing decision. (Linux 7.1: `include/uapi/linux/netfilter.h:43`)
|
||||
PreRouting,
|
||||
/// `NF_INET_LOCAL_IN` — packet destined for this host, after the routing
|
||||
/// decision. (Linux 7.1: `include/uapi/linux/netfilter.h:44`)
|
||||
InputLocal,
|
||||
/// `NF_INET_FORWARD` — packet being forwarded between NICs. (Linux 7.1:
|
||||
/// `include/uapi/linux/netfilter.h:45`)
|
||||
Forward,
|
||||
/// `NF_INET_LOCAL_OUT` — packet generated locally, before the routing
|
||||
/// decision. (Linux 7.1: `include/uapi/linux/netfilter.h:46`)
|
||||
OutputLocal,
|
||||
/// `NF_INET_POST_ROUTING` — packet leaving a NIC, after the routing
|
||||
/// decision. (Linux 7.1: `include/uapi/linux/netfilter.h:47`)
|
||||
PostRouting,
|
||||
}
|
||||
|
||||
impl Hook {
|
||||
/// Returns all hook points, in Linux's canonical order (matches
|
||||
/// `enum nf_inet_hooks` ordering).
|
||||
pub const ALL: [Hook; 5] = [
|
||||
Hook::PreRouting,
|
||||
Hook::InputLocal,
|
||||
Hook::Forward,
|
||||
Hook::OutputLocal,
|
||||
Hook::PostRouting,
|
||||
];
|
||||
|
||||
/// Returns the kernel-style numeric id (matches `NF_INET_*` constants).
|
||||
pub const fn as_u32(self) -> u32 {
|
||||
match self {
|
||||
Hook::PreRouting => 0,
|
||||
Hook::InputLocal => 1,
|
||||
Hook::Forward => 2,
|
||||
Hook::OutputLocal => 3,
|
||||
Hook::PostRouting => 4,
|
||||
}
|
||||
}
|
||||
|
||||
/// Inverse of [`Self::as_u32`]. Returns `None` for unknown ids.
|
||||
pub const fn from_u32(id: u32) -> Option<Hook> {
|
||||
match id {
|
||||
0 => Some(Hook::PreRouting),
|
||||
1 => Some(Hook::InputLocal),
|
||||
2 => Some(Hook::Forward),
|
||||
3 => Some(Hook::OutputLocal),
|
||||
4 => Some(Hook::PostRouting),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Short lowercase name used in the scheme interface (e.g. "input",
|
||||
/// "output"). Mirrors the chain name conventions of iptables/nftables.
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Hook::PreRouting => "prerouting",
|
||||
Hook::InputLocal => "input",
|
||||
Hook::Forward => "forward",
|
||||
Hook::OutputLocal => "output",
|
||||
Hook::PostRouting => "postrouting",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The decision returned by the filter engine after evaluating rules.
|
||||
///
|
||||
/// Mirrors the verdict constants in `include/uapi/linux/netfilter.h`:
|
||||
/// - [`Accept`] = `NF_ACCEPT` (= 1)
|
||||
/// - [`Drop`] = `NF_DROP` (= 0)
|
||||
///
|
||||
/// Linux's `NF_QUEUE` (= 3) and `NF_STOLEN` (= 2) are not supported in this
|
||||
/// revision (no userspace packet queue and no socket ownership transfer).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Verdict {
|
||||
/// `NF_ACCEPT` — continue normal processing.
|
||||
Accept,
|
||||
/// `NF_DROP` — silently discard the packet.
|
||||
Drop,
|
||||
/// Log the packet details and continue evaluating the next rule.
|
||||
/// Mirrors iptables `-j LOG --log-prefix "..."`.
|
||||
Log,
|
||||
/// Send ICMP Destination Unreachable (port) back to the source.
|
||||
/// Mirrors iptables `-j REJECT --reject-with icmp-port-unreachable`.
|
||||
Reject,
|
||||
}
|
||||
|
||||
impl Verdict {
|
||||
pub const fn as_u32(self) -> u32 {
|
||||
match self {
|
||||
Verdict::Accept => 1,
|
||||
Verdict::Drop => 0,
|
||||
Verdict::Log => 2,
|
||||
Verdict::Reject => 3,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_u32(id: u32) -> Option<Verdict> {
|
||||
match id {
|
||||
1 => Some(Verdict::Accept),
|
||||
0 => Some(Verdict::Drop),
|
||||
2 => Some(Verdict::Log),
|
||||
3 => Some(Verdict::Reject),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Verdict::Accept => "ACCEPT",
|
||||
Verdict::Drop => "DROP",
|
||||
Verdict::Log => "LOG",
|
||||
Verdict::Reject => "REJECT",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The context passed to the filter engine for evaluation.
|
||||
///
|
||||
/// Mirrors `struct nf_hook_state` (`include/linux/netfilter.h:78`):
|
||||
/// - `hook` ↔ `state->hook`
|
||||
/// - `in_dev` ↔ `state->in`
|
||||
/// - `out_dev` ↔ `state->out`
|
||||
/// - `protocol` ↔ the L4 protocol number (extracted from the IP header)
|
||||
///
|
||||
/// `skb` (the buffer) is split into its derived fields here for ergonomic
|
||||
/// matching without forcing each rule to re-parse the packet.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PacketContext<'a> {
|
||||
pub hook: Hook,
|
||||
pub in_dev: Option<alloc::rc::Rc<str>>,
|
||||
pub out_dev: Option<alloc::rc::Rc<str>>,
|
||||
pub src_addr: smoltcp::wire::IpAddress,
|
||||
pub dst_addr: smoltcp::wire::IpAddress,
|
||||
pub protocol: u8,
|
||||
pub src_port: Option<u16>,
|
||||
pub dst_port: Option<u16>,
|
||||
pub packet: &'a [u8],
|
||||
}
|
||||
|
||||
extern crate alloc;
|
||||
@@ -0,0 +1,454 @@
|
||||
//! Network Address Translation — mirrors Linux 7.1's `nf_nat` subsystem.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/netfilter/nf_nat_core.c` — `nf_nat_packet()`, `nf_nat_setup_info()`
|
||||
//! - `include/net/netfilter/nf_nat.h` — `struct nf_conn_nat`
|
||||
//! - `net/netfilter/nf_nat_proto_tcp.c` — TCP checksum adjustment after NAT
|
||||
//! - `net/netfilter/nf_nat_proto_udp.c` — UDP checksum adjustment after NAT
|
||||
//!
|
||||
//! Two NAT types are supported:
|
||||
//! - **SNAT** (Source NAT): changes the source IP of outgoing packets.
|
||||
//! Applied in the OUTPUT/POSTROUTING path. Mirrors `nf_nat_masquerade.c`.
|
||||
//! - **DNAT** (Destination NAT): changes the destination IP of incoming
|
||||
//! packets. Applied in the INPUT/PREROUTING path. Used for port forwarding.
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use smoltcp::wire::{IpAddress, Ipv4Address, Ipv6Address};
|
||||
|
||||
use super::{Hook, PacketContext};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NatType {
|
||||
Snat,
|
||||
Dnat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NatRule {
|
||||
pub id: u32,
|
||||
pub nat_type: NatType,
|
||||
pub hook: Hook,
|
||||
pub src_match: Option<IpAddress>,
|
||||
pub dst_match: Option<IpAddress>,
|
||||
pub trans_addr: IpAddress,
|
||||
pub trans_port: Option<u16>,
|
||||
pub match_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NatBinding {
|
||||
pub orig_src: IpAddress,
|
||||
pub trans_src: IpAddress,
|
||||
pub orig_dst: IpAddress,
|
||||
pub trans_dst: IpAddress,
|
||||
pub orig_sport: u16,
|
||||
pub trans_sport: u16,
|
||||
pub orig_dport: u16,
|
||||
pub trans_dport: u16,
|
||||
}
|
||||
|
||||
impl Default for NatBinding {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
orig_src: IpAddress::Ipv4(Ipv4Address::UNSPECIFIED),
|
||||
trans_src: IpAddress::Ipv4(Ipv4Address::UNSPECIFIED),
|
||||
orig_dst: IpAddress::Ipv4(Ipv4Address::UNSPECIFIED),
|
||||
trans_dst: IpAddress::Ipv4(Ipv4Address::UNSPECIFIED),
|
||||
orig_sport: 0,
|
||||
trans_sport: 0,
|
||||
orig_dport: 0,
|
||||
trans_dport: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NatTable {
|
||||
pub rules: Vec<NatRule>,
|
||||
next_id: u32,
|
||||
ephemeral_port: u16,
|
||||
}
|
||||
|
||||
impl NatTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
rules: Vec::new(),
|
||||
next_id: 1,
|
||||
ephemeral_port: 40000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, mut rule: NatRule) -> u32 {
|
||||
rule.id = self.next_id;
|
||||
self.next_id = self.next_id.saturating_add(1);
|
||||
let id = rule.id;
|
||||
self.rules.push(rule);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn lookup_snat(
|
||||
&self,
|
||||
hook: Hook,
|
||||
src: IpAddress,
|
||||
_dst: IpAddress,
|
||||
) -> Option<(IpAddress, Option<u16>)> {
|
||||
for rule in self.rules.iter().filter(|r| {
|
||||
r.nat_type == NatType::Snat && r.hook == hook
|
||||
}) {
|
||||
if let Some(m) = rule.src_match {
|
||||
if m != src {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Some((rule.trans_addr, rule.trans_port));
|
||||
}
|
||||
if hook == Hook::OutputLocal || hook == Hook::PostRouting {
|
||||
for rule in self.rules.iter().filter(|r| {
|
||||
r.nat_type == NatType::Snat && r.src_match.is_none()
|
||||
}) {
|
||||
return Some((rule.trans_addr, rule.trans_port));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn lookup_dnat(
|
||||
&self,
|
||||
hook: Hook,
|
||||
_src: IpAddress,
|
||||
dst: IpAddress,
|
||||
) -> Option<(IpAddress, Option<u16>)> {
|
||||
for rule in self.rules.iter().filter(|r| {
|
||||
r.nat_type == NatType::Dnat && r.hook == hook
|
||||
}) {
|
||||
if let Some(m) = rule.dst_match {
|
||||
if m != dst {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Some((rule.trans_addr, rule.trans_port));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn alloc_ephemeral_port(&mut self) -> u16 {
|
||||
let port = self.ephemeral_port;
|
||||
self.ephemeral_port = if self.ephemeral_port >= 65535 {
|
||||
40000
|
||||
} else {
|
||||
self.ephemeral_port.saturating_add(1)
|
||||
};
|
||||
port
|
||||
}
|
||||
|
||||
pub fn format(&self) -> String {
|
||||
let mut out = String::from("Nat table:\n");
|
||||
for rule in &self.rules {
|
||||
out.push_str(&alloc::format!(
|
||||
" [{:>3}] {:?} {:?} -> {} port={:?} matches={}\n",
|
||||
rule.id,
|
||||
rule.nat_type,
|
||||
rule.hook,
|
||||
rule.trans_addr,
|
||||
rule.trans_port,
|
||||
rule.match_count,
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, id: u32) -> bool {
|
||||
if let Some(idx) = self.rules.iter().position(|r| r.id == id) {
|
||||
self.rules.remove(idx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rewrites the source IP address in an IPv4 packet. Returns `true` on
|
||||
/// success. Mirrors `nf_nat_ipv4_manip_pkt()` in
|
||||
/// `net/ipv4/netfilter/nf_nat_l3proto_ipv4.c`.
|
||||
pub fn rewrite_src_ipv4(packet: &mut [u8], new_src: Ipv4Address) -> bool {
|
||||
if packet.len() < 20 {
|
||||
return false;
|
||||
}
|
||||
let mut ipv4 = smoltcp::wire::Ipv4Packet::new_unchecked(packet);
|
||||
ipv4.set_src_addr(new_src);
|
||||
ipv4.fill_checksum();
|
||||
true
|
||||
}
|
||||
|
||||
/// Rewrites the destination IP address in an IPv4 packet.
|
||||
pub fn rewrite_dst_ipv4(packet: &mut [u8], new_dst: Ipv4Address) -> bool {
|
||||
if packet.len() < 20 {
|
||||
return false;
|
||||
}
|
||||
let mut ipv4 = smoltcp::wire::Ipv4Packet::new_unchecked(packet);
|
||||
ipv4.set_dst_addr(new_dst);
|
||||
ipv4.fill_checksum();
|
||||
true
|
||||
}
|
||||
|
||||
/// Rewrites the TCP/UDP port in the transport header and recomputes the
|
||||
/// checksum. Mirrors `nf_nat_proto_tcp.c` (`tcp_manip_pkt`) and
|
||||
/// `nf_nat_proto_udp.c` (`udp_manip_pkt`).
|
||||
pub fn rewrite_port_ipv4(
|
||||
packet: &mut [u8],
|
||||
old_port: u16,
|
||||
new_port: u16,
|
||||
old_addr: IpAddress,
|
||||
new_addr: IpAddress,
|
||||
is_src: bool,
|
||||
protocol: u8,
|
||||
) {
|
||||
let ip_header_len: usize = 20;
|
||||
if packet.len() < ip_header_len + 4 {
|
||||
return;
|
||||
}
|
||||
let transport = &mut packet[ip_header_len..];
|
||||
let (port_offset, addr_old, addr_new) = if is_src {
|
||||
(0, old_addr, new_addr)
|
||||
} else {
|
||||
(2, old_addr, new_addr)
|
||||
};
|
||||
|
||||
transport[port_offset] = (new_port >> 8) as u8;
|
||||
transport[port_offset + 1] = (new_port & 0xff) as u8;
|
||||
|
||||
if protocol == 17 {
|
||||
let old_csum = u16::from_be_bytes([transport[6], transport[7]]);
|
||||
if old_csum != 0 {
|
||||
transport[6] = 0;
|
||||
transport[7] = 0;
|
||||
}
|
||||
} else {
|
||||
transport[16] = 0;
|
||||
transport[17] = 0;
|
||||
}
|
||||
|
||||
recompute_transport_checksum(packet, protocol, addr_old, addr_new);
|
||||
}
|
||||
|
||||
fn recompute_transport_checksum(
|
||||
packet: &mut [u8],
|
||||
protocol: u8,
|
||||
_old_addr: IpAddress,
|
||||
_new_addr: IpAddress,
|
||||
) {
|
||||
if packet.len() < 20 {
|
||||
return;
|
||||
}
|
||||
let src_bytes: [u8; 4] = packet[12..16].try_into().unwrap_or([0; 4]);
|
||||
let dst_bytes: [u8; 4] = packet[16..20].try_into().unwrap_or([0; 4]);
|
||||
let src_addr = Ipv4Address::new(src_bytes[0], src_bytes[1], src_bytes[2], src_bytes[3]);
|
||||
let dst_addr = Ipv4Address::new(dst_bytes[0], dst_bytes[1], dst_bytes[2], dst_bytes[3]);
|
||||
|
||||
let transport = &mut packet[20..];
|
||||
|
||||
match protocol {
|
||||
6 => {
|
||||
if transport.len() >= 20 {
|
||||
let mut tcp = smoltcp::wire::TcpPacket::new_unchecked(transport);
|
||||
tcp.fill_checksum(&IpAddress::Ipv4(src_addr), &IpAddress::Ipv4(dst_addr));
|
||||
}
|
||||
}
|
||||
17 => {
|
||||
if transport.len() >= 8 {
|
||||
let mut udp = smoltcp::wire::UdpPacket::new_unchecked(transport);
|
||||
udp.fill_checksum(&IpAddress::Ipv4(src_addr), &IpAddress::Ipv4(dst_addr));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_nat_rule(
|
||||
line: &str,
|
||||
) -> core::result::Result<NatRule, NatParseError> {
|
||||
let mut tokens = line.split_whitespace();
|
||||
|
||||
let nat_type_str = tokens.next().ok_or(NatParseError::MissingField("nat type"))?;
|
||||
let nat_type = match nat_type_str.to_uppercase().as_str() {
|
||||
"SNAT" => NatType::Snat,
|
||||
"DNAT" => NatType::Dnat,
|
||||
_ => return Err(NatParseError::BadNatType(nat_type_str.to_string())),
|
||||
};
|
||||
|
||||
let hook_str = tokens.next().ok_or(NatParseError::MissingField("hook"))?;
|
||||
let hook = match hook_str.to_lowercase().as_str() {
|
||||
"prerouting" => Hook::PreRouting,
|
||||
"input" => Hook::InputLocal,
|
||||
"forward" => Hook::Forward,
|
||||
"output" => Hook::OutputLocal,
|
||||
"postrouting" => Hook::PostRouting,
|
||||
_ => return Err(NatParseError::BadHook(hook_str.to_string())),
|
||||
};
|
||||
|
||||
let to_str = tokens.next();
|
||||
if to_str != Some("to") {
|
||||
return Err(NatParseError::MissingField("to"));
|
||||
}
|
||||
|
||||
let addr_str = tokens.next().ok_or(NatParseError::MissingField("address"))?;
|
||||
let trans_addr = if let Ok(v4) = Ipv4Address::from_str(addr_str) {
|
||||
IpAddress::Ipv4(v4)
|
||||
} else if let Ok(v6) = Ipv6Address::from_str(addr_str) {
|
||||
IpAddress::Ipv6(v6)
|
||||
} else {
|
||||
return Err(NatParseError::BadAddress(addr_str.to_string()));
|
||||
};
|
||||
|
||||
let mut trans_port = None;
|
||||
let mut src_match = None;
|
||||
let mut dst_match = None;
|
||||
|
||||
while let Some(token) = tokens.next() {
|
||||
match token {
|
||||
"port" => {
|
||||
let p = tokens.next().ok_or(NatParseError::MissingField("port value"))?;
|
||||
trans_port = Some(p.parse().map_err(|_| NatParseError::BadPort(p.to_string()))?);
|
||||
}
|
||||
"match-src" => {
|
||||
let a = tokens.next().ok_or(NatParseError::MissingField("match-src value"))?;
|
||||
src_match = Some(parse_addr(a)?);
|
||||
}
|
||||
"match-dst" => {
|
||||
let a = tokens.next().ok_or(NatParseError::MissingField("match-dst value"))?;
|
||||
dst_match = Some(parse_addr(a)?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(NatRule {
|
||||
id: 0,
|
||||
nat_type,
|
||||
hook,
|
||||
src_match,
|
||||
dst_match,
|
||||
trans_addr,
|
||||
trans_port,
|
||||
match_count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_addr(s: &str) -> core::result::Result<IpAddress, NatParseError> {
|
||||
if let Ok(v4) = Ipv4Address::from_str(s) {
|
||||
Ok(IpAddress::Ipv4(v4))
|
||||
} else if let Ok(v6) = Ipv6Address::from_str(s) {
|
||||
Ok(IpAddress::Ipv6(v6))
|
||||
} else {
|
||||
Err(NatParseError::BadAddress(s.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
use core::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum NatParseError {
|
||||
MissingField(&'static str),
|
||||
BadNatType(String),
|
||||
BadHook(String),
|
||||
BadAddress(String),
|
||||
BadPort(String),
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_ipv4_udp_packet(src: [u8; 4], dst: [u8; 4]) -> Vec<u8> {
|
||||
let mut p = vec![0u8; 28];
|
||||
p[0] = 0x45; p[1] = 0x00;
|
||||
p[2] = 0x00; p[3] = 0x1c;
|
||||
p[6] = 0x00; p[7] = 0x00;
|
||||
p[8] = 64;
|
||||
p[9] = 17;
|
||||
p[10] = 0x00; p[11] = 0x00;
|
||||
p[12..16].copy_from_slice(&src);
|
||||
p[16..20].copy_from_slice(&dst);
|
||||
p[20] = 0x12; p[21] = 0x34;
|
||||
p[22] = 0x56; p[23] = 0x78;
|
||||
p[24] = 0x00; p[25] = 0x08;
|
||||
p[26] = 0x00; p[27] = 0x00;
|
||||
p
|
||||
}
|
||||
|
||||
fn read_ipv4(p: &[u8], offset: usize) -> Ipv4Address {
|
||||
Ipv4Address::new(p[offset], p[offset+1], p[offset+2], p[offset+3])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_src_changes_source_address() {
|
||||
let mut p = make_ipv4_udp_packet([10, 0, 0, 1], [192, 168, 1, 1]);
|
||||
let new_src = Ipv4Address::new(192, 168, 99, 99);
|
||||
assert!(rewrite_src_ipv4(&mut p, new_src));
|
||||
assert_eq!(read_ipv4(&p, 12), new_src, "Source IP must be rewritten");
|
||||
assert_eq!(read_ipv4(&p, 16), Ipv4Address::new(192, 168, 1, 1),
|
||||
"Destination IP must be unchanged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_dst_changes_destination_address() {
|
||||
let mut p = make_ipv4_udp_packet([10, 0, 0, 1], [192, 168, 1, 1]);
|
||||
let new_dst = Ipv4Address::new(8, 8, 8, 8);
|
||||
assert!(rewrite_dst_ipv4(&mut p, new_dst));
|
||||
assert_eq!(read_ipv4(&p, 16), new_dst);
|
||||
assert_eq!(read_ipv4(&p, 12), Ipv4Address::new(10, 0, 0, 1),
|
||||
"Source IP must be unchanged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_short_packet_returns_false() {
|
||||
let mut p = vec![0u8; 10];
|
||||
assert!(!rewrite_src_ipv4(&mut p, Ipv4Address::new(1, 2, 3, 4)));
|
||||
assert!(!rewrite_dst_ipv4(&mut p, Ipv4Address::new(1, 2, 3, 4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_recomputes_checksum_when_previously_nonzero() {
|
||||
let mut p = make_ipv4_udp_packet([10, 0, 0, 1], [192, 168, 1, 1]);
|
||||
// Compute a real checksum first
|
||||
if let Ok(mut ipv4) = smoltcp::wire::Ipv4Packet::new_checked(&mut p) {
|
||||
ipv4.fill_checksum();
|
||||
}
|
||||
let original_csum = u16::from_be_bytes([p[10], p[11]]);
|
||||
assert_ne!(original_csum, 0, "Pre-condition: checksum should be non-zero");
|
||||
let _ = rewrite_src_ipv4(&mut p, Ipv4Address::new(10, 0, 0, 2));
|
||||
let new_csum = u16::from_be_bytes([p[10], p[11]]);
|
||||
assert_ne!(new_csum, original_csum,
|
||||
"Checksum must be updated after source rewrite");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nat_table_add_assigns_unique_ids() {
|
||||
let mut t = NatTable::new();
|
||||
let make_rule = || NatRule {
|
||||
id: 0,
|
||||
nat_type: NatType::Snat,
|
||||
hook: Hook::OutputLocal,
|
||||
src_match: None,
|
||||
dst_match: None,
|
||||
trans_addr: IpAddress::v4(192, 168, 1, 100),
|
||||
trans_port: None,
|
||||
match_count: 0,
|
||||
};
|
||||
let id1 = t.add(make_rule());
|
||||
let id2 = t.add(make_rule());
|
||||
assert_ne!(id1, id2, "Each rule gets a unique ID");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nat_ephemeral_port_allocates_unique() {
|
||||
let mut t = NatTable::new();
|
||||
let p1 = t.alloc_ephemeral_port();
|
||||
let p2 = t.alloc_ephemeral_port();
|
||||
assert_ne!(p1, p2, "Ephemeral ports must be unique across calls");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
//! Filter rule representation and packet matching.
|
||||
//!
|
||||
//! Mirrors the Linux 7.1 `ipt_entry` structure and the `xt_match` matching
|
||||
//! framework:
|
||||
//! - Linux `ipt_entry` lives in `net/ipv4/netfilter/ip_tables.h`
|
||||
//! - Linux `xt_match` lives in `net/netfilter/x_tables.h`
|
||||
//! - Linux `xt_action_param` (match context) lives in
|
||||
//! `include/linux/netfilter/x_tables.h`
|
||||
//!
|
||||
//! In this revision the rule supports a fixed set of match fields that
|
||||
//! cover the most common iptables/xt_matches: `src`/`dst` address+CIDR,
|
||||
//! `protocol`, `sport`/`dport` (TCP/UDP only), and the `in`/`out`
|
||||
//! interface. This is intentionally narrower than the full xtables match
|
||||
//! set — extensions like `iprange`, `mac`, `conntrack`, `recent`, etc.
|
||||
//! will be added in follow-up revisions as separate match kinds.
|
||||
|
||||
use alloc::rc::Rc;
|
||||
use smoltcp::wire::{IpAddress, Ipv4Address, Ipv6Address};
|
||||
|
||||
use super::{ConnState, Hook, PacketContext, Verdict};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StateMatch {
|
||||
New,
|
||||
Established,
|
||||
Related,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// The IP protocol number used in a rule. Values mirror the IANA assigned
|
||||
/// numbers (`include/uapi/linux/in.h`):
|
||||
/// - 1 = `IPPROTO_ICMP`
|
||||
/// - 6 = `IPPROTO_TCP`
|
||||
/// - 17 = `IPPROTO_UDP`
|
||||
/// - 58 = `IPPROTO_ICMPV6`
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Protocol(pub u8);
|
||||
|
||||
impl Protocol {
|
||||
pub const ANY: Option<Protocol> = None;
|
||||
pub const ICMP: Protocol = Protocol(1);
|
||||
pub const TCP: Protocol = Protocol(6);
|
||||
pub const UDP: Protocol = Protocol(17);
|
||||
pub const ICMP6: Protocol = Protocol(58);
|
||||
}
|
||||
|
||||
/// Outcome of matching a single rule against a packet.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MatchResult {
|
||||
/// Every field in the rule matched. The rule's verdict applies.
|
||||
Match,
|
||||
/// At least one field did not match. Continue to the next rule.
|
||||
NoMatch,
|
||||
}
|
||||
|
||||
/// A single filter rule. The fields mirror the iptables CLI options:
|
||||
///
|
||||
/// | Rust field | iptables equivalent |
|
||||
/// |----------------------|----------------------------------------------|
|
||||
/// | `src_addr`/`src_cidr`| `--source` / `--src-range` (with cidr_len) |
|
||||
/// | `dst_addr`/`dst_cidr`| `--destination` / `--dst-range` |
|
||||
/// | `protocol` | `--protocol` |
|
||||
/// | `src_port` | `--source-port` (TCP/UDP only) |
|
||||
/// | `dst_port` | `--destination-port` |
|
||||
/// | `in_dev` | `--in-interface` |
|
||||
/// | `out_dev` | `--out-interface` |
|
||||
/// | `verdict` | `--jump` (ACCEPT or DROP) |
|
||||
///
|
||||
/// `None` in any of these fields means "match any" (a wildcard), mirroring
|
||||
/// iptables behaviour where omitting `--source` matches every source.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FilterRule {
|
||||
pub id: u32,
|
||||
pub hook: Hook,
|
||||
pub src_addr: Option<IpAddress>,
|
||||
pub src_prefix_len: u8,
|
||||
pub dst_addr: Option<IpAddress>,
|
||||
pub dst_prefix_len: u8,
|
||||
pub protocol: Option<Protocol>,
|
||||
pub src_port: Option<u16>,
|
||||
pub dst_port: Option<u16>,
|
||||
pub in_dev: Option<Rc<str>>,
|
||||
pub out_dev: Option<Rc<str>>,
|
||||
pub state_match: Option<StateMatch>,
|
||||
pub verdict: Verdict,
|
||||
pub match_count: u64,
|
||||
}
|
||||
|
||||
impl FilterRule {
|
||||
/// Returns true if this rule applies to the given hook point. Mirrors
|
||||
/// `ipt_entry->comefrom` semantics: a rule attached to `Hook::InputLocal`
|
||||
/// only fires when the engine evaluates the INPUT chain.
|
||||
pub fn applies_to(&self, hook: Hook) -> bool {
|
||||
self.hook == hook
|
||||
}
|
||||
|
||||
/// Evaluates this rule against a packet. Mirrors
|
||||
/// `xt_action_param`-based matching in `x_tables.c` (`xt_check_match`).
|
||||
pub fn matches(&self, ctx: &PacketContext<'_>) -> MatchResult {
|
||||
if !self.applies_to(ctx.hook) {
|
||||
return MatchResult::NoMatch;
|
||||
}
|
||||
if let Some(src) = self.src_addr {
|
||||
if !addr_in_cidr(src, self.src_prefix_len, ctx.src_addr) {
|
||||
return MatchResult::NoMatch;
|
||||
}
|
||||
}
|
||||
if let Some(dst) = self.dst_addr {
|
||||
if !addr_in_cidr(dst, self.dst_prefix_len, ctx.dst_addr) {
|
||||
return MatchResult::NoMatch;
|
||||
}
|
||||
}
|
||||
if let Some(proto) = self.protocol {
|
||||
if proto.0 != ctx.protocol {
|
||||
return MatchResult::NoMatch;
|
||||
}
|
||||
}
|
||||
if let Some(sport) = self.src_port {
|
||||
if ctx.src_port != Some(sport) {
|
||||
return MatchResult::NoMatch;
|
||||
}
|
||||
}
|
||||
if let Some(dport) = self.dst_port {
|
||||
if ctx.dst_port != Some(dport) {
|
||||
return MatchResult::NoMatch;
|
||||
}
|
||||
}
|
||||
if let Some(in_dev) = &self.in_dev {
|
||||
match &ctx.in_dev {
|
||||
Some(ctx_dev) if ctx_dev.as_ref() == in_dev.as_ref() => {}
|
||||
_ => return MatchResult::NoMatch,
|
||||
}
|
||||
}
|
||||
if let Some(out_dev) = &self.out_dev {
|
||||
match &ctx.out_dev {
|
||||
Some(ctx_dev) if ctx_dev.as_ref() == out_dev.as_ref() => {}
|
||||
_ => return MatchResult::NoMatch,
|
||||
}
|
||||
}
|
||||
MatchResult::Match
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `addr` falls inside the prefix `network/prefix_len`.
|
||||
/// Mirrors `ip_masked_match` (`net/ipv4/netfilter/ipt_addr.c`) — IPv4
|
||||
/// uses the simple 32-bit mask, IPv6 uses the 128-bit bitwise mask.
|
||||
fn addr_in_cidr(network: IpAddress, prefix_len: u8, addr: IpAddress) -> bool {
|
||||
match (network, addr) {
|
||||
(IpAddress::Ipv4(net), IpAddress::Ipv4(a)) => {
|
||||
if prefix_len == 0 {
|
||||
return true;
|
||||
}
|
||||
if prefix_len > 32 {
|
||||
return false;
|
||||
}
|
||||
let mask: u32 = if prefix_len == 32 {
|
||||
u32::MAX
|
||||
} else {
|
||||
!((1u32 << (32 - prefix_len)) - 1)
|
||||
};
|
||||
(u32::from(net) & mask) == (u32::from(a) & mask)
|
||||
}
|
||||
(IpAddress::Ipv6(net), IpAddress::Ipv6(a)) => {
|
||||
if prefix_len > 128 {
|
||||
return false;
|
||||
}
|
||||
let net_bytes = net.octets();
|
||||
let a_bytes = a.octets();
|
||||
let full_bytes = (prefix_len / 8) as usize;
|
||||
let remainder = prefix_len % 8;
|
||||
if net_bytes[..full_bytes] != a_bytes[..full_bytes] {
|
||||
return false;
|
||||
}
|
||||
if remainder > 0 && full_bytes < 16 {
|
||||
let mask = 0xff << (8 - remainder);
|
||||
(net_bytes[full_bytes] & mask) == (a_bytes[full_bytes] & mask)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
(IpAddress::Ipv4(_), IpAddress::Ipv6(_)) | (IpAddress::Ipv6(_), IpAddress::Ipv4(_)) => {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern crate alloc;
|
||||
@@ -0,0 +1,525 @@
|
||||
//! Filter table: a collection of rules and the per-chain default policies.
|
||||
//!
|
||||
//! Mirrors Linux 7.1's `xt_table` (`include/linux/netfilter/x_tables.h`):
|
||||
//! - `valid_hooks` (bitmask) ↔ `FilterTable::enabled_hooks` (set)
|
||||
//! - `entries` (rule blob) ↔ `FilterTable::rules`
|
||||
//! - Per-hook default policy ↔ `FilterTable::default_policy`
|
||||
//!
|
||||
//! The parser accepts an iptables-style textual rule. The grammar is a
|
||||
//! deliberate subset of iptables that covers the most common cases and
|
||||
//! stays readable:
|
||||
//!
|
||||
//! ```text
|
||||
//! ACTION CHAIN [-i IFACE] [-o IFACE] [-p PROTO] [-s ADDR[/LEN]] [-d ADDR[/LEN]]
|
||||
//! [--sport PORT] [--dport PORT]
|
||||
//! ```
|
||||
//!
|
||||
//! Examples:
|
||||
//! - `ACCEPT input -p tcp --dport 80`
|
||||
//! - `DROP input -s 10.0.0.0/8`
|
||||
//! - `ACCEPT output -d 192.168.1.0/24 --sport 1024`
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use core::fmt;
|
||||
use core::str::FromStr;
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{IpAddress, Ipv4Address, Ipv6Address};
|
||||
|
||||
use super::conntrack::{ConnState, ConntrackTable};
|
||||
use super::nat::{NatRule, NatTable};
|
||||
use super::rule::{FilterRule, Protocol, StateMatch};
|
||||
use super::{Hook, PacketContext, Verdict};
|
||||
|
||||
/// A table of filter rules plus per-chain default policies.
|
||||
///
|
||||
/// In iptables terminology this is one "table" (the `filter` table).
|
||||
/// We currently expose exactly one table; future extensions can add
|
||||
/// `nat`, `mangle`, `raw` tables following the same model.
|
||||
pub struct FilterTable {
|
||||
pub rules: Vec<FilterRule>,
|
||||
pub default_policy: BTreeMap<Hook, Verdict>,
|
||||
pub next_id: u32,
|
||||
pub conntrack: Option<ConntrackTable>,
|
||||
pub nat_table: NatTable,
|
||||
pub chain_counters: BTreeMap<Hook, (u64, u64)>,
|
||||
pub log_buffer: Vec<String>,
|
||||
}
|
||||
|
||||
impl FilterTable {
|
||||
pub fn new() -> Self {
|
||||
let mut default_policy = BTreeMap::new();
|
||||
for &hook in &Hook::ALL {
|
||||
default_policy.insert(hook, Verdict::Accept);
|
||||
}
|
||||
Self {
|
||||
rules: Vec::new(),
|
||||
default_policy,
|
||||
next_id: 1,
|
||||
conntrack: Some(ConntrackTable::new()),
|
||||
nat_table: NatTable::new(),
|
||||
chain_counters: BTreeMap::new(),
|
||||
log_buffer: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the rules attached to `ctx.hook` in order, returning the
|
||||
/// first matching verdict. If no rule matches, the chain's default
|
||||
/// policy applies. Mirrors `nf_iterate` + `ipt_do_table` in
|
||||
/// `net/ipv4/netfilter/ip_tables.c`.
|
||||
pub fn evaluate(&mut self, ctx: &PacketContext<'_>, now: Instant) -> Verdict {
|
||||
let conn_state = self.conntrack.as_mut().map(|ct| ct.track(ctx, now));
|
||||
if conn_state == Some(ConnState::OverLimit) {
|
||||
return Verdict::Drop;
|
||||
}
|
||||
let counter = self.chain_counters.entry(ctx.hook).or_insert((0, 0));
|
||||
counter.0 = counter.0.saturating_add(1);
|
||||
counter.1 = counter.1.saturating_add(ctx.packet.len() as u64);
|
||||
|
||||
let mut final_verdict: Option<Verdict> = None;
|
||||
for rule in self.rules.iter_mut() {
|
||||
if rule.state_match.is_some() {
|
||||
match (rule.state_match, conn_state) {
|
||||
(Some(StateMatch::Established), Some(ConnState::Established)) => {}
|
||||
(Some(StateMatch::New), Some(ConnState::New)) => {}
|
||||
(Some(StateMatch::Invalid), _) => continue,
|
||||
(Some(StateMatch::Related), _) => continue,
|
||||
(Some(_), _) => continue,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if rule.applies_to(ctx.hook) && rule.matches(ctx) == super::rule::MatchResult::Match {
|
||||
rule.match_count = rule.match_count.saturating_add(1);
|
||||
match rule.verdict {
|
||||
Verdict::Log => {
|
||||
let msg = alloc::format!(
|
||||
"{} IN={} OUT={} SRC={} DST={} PROTO={} SPORT={} DPORT={}",
|
||||
rule.id,
|
||||
ctx.in_dev.as_deref().unwrap_or("-"),
|
||||
ctx.out_dev.as_deref().unwrap_or("-"),
|
||||
ctx.src_addr,
|
||||
ctx.dst_addr,
|
||||
ctx.protocol,
|
||||
ctx.src_port.map(|p| p.to_string()).unwrap_or_else(|| "-".into()),
|
||||
ctx.dst_port.map(|p| p.to_string()).unwrap_or_else(|| "-".into()),
|
||||
);
|
||||
if self.log_buffer.len() >= 100 {
|
||||
self.log_buffer.remove(0);
|
||||
}
|
||||
self.log_buffer.push(msg);
|
||||
continue;
|
||||
}
|
||||
Verdict::Reject => {
|
||||
final_verdict = Some(Verdict::Reject);
|
||||
break;
|
||||
}
|
||||
v => {
|
||||
final_verdict = Some(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
final_verdict.unwrap_or_else(|| {
|
||||
self.default_policy
|
||||
.get(&ctx.hook)
|
||||
.copied()
|
||||
.unwrap_or(Verdict::Accept)
|
||||
})
|
||||
}
|
||||
|
||||
/// Inserts a rule. The rule's `id` is overwritten with a fresh id from
|
||||
/// `next_id`.
|
||||
pub fn add(&mut self, mut rule: FilterRule) -> u32 {
|
||||
rule.id = self.next_id;
|
||||
self.next_id = self.next_id.saturating_add(1);
|
||||
let id = rule.id;
|
||||
self.rules.push(rule);
|
||||
id
|
||||
}
|
||||
|
||||
/// Removes the rule with the given id. Returns `true` if a rule was
|
||||
/// removed.
|
||||
pub fn remove(&mut self, id: u32) -> bool {
|
||||
if let Some(idx) = self.rules.iter().position(|r| r.id == id) {
|
||||
self.rules.remove(idx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the entire table as a human-readable text dump. Mirrors
|
||||
/// `iptables -L -n -v` output format.
|
||||
pub fn format(&self) -> String {
|
||||
let mut out = String::new();
|
||||
for &hook in &Hook::ALL {
|
||||
let policy = self
|
||||
.default_policy
|
||||
.get(&hook)
|
||||
.copied()
|
||||
.unwrap_or(Verdict::Accept);
|
||||
out.push_str(&alloc::format!(
|
||||
"Chain {} (policy {})\n",
|
||||
hook.name().to_uppercase(),
|
||||
policy.name()
|
||||
));
|
||||
for rule in self.rules.iter().filter(|r| r.hook == hook) {
|
||||
out.push_str(" ");
|
||||
out.push_str(&format_rule(rule));
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Compact per-chain summary for netcfg summary node.
|
||||
/// Returns lines like:
|
||||
/// input: pkts=12 bytes=5678 policy=ACCEPT
|
||||
pub fn chain_summary(&self) -> String {
|
||||
let mut out = String::new();
|
||||
for &hook in &Hook::ALL {
|
||||
let (pkts, bytes) = self.chain_counters.get(&hook).copied().unwrap_or((0, 0));
|
||||
let policy = self.default_policy.get(&hook).copied().unwrap_or(Verdict::Accept);
|
||||
out.push_str(&alloc::format!(
|
||||
"{}: pkts={} bytes={} policy={}\n",
|
||||
hook.name(),
|
||||
pkts,
|
||||
bytes,
|
||||
policy.name()
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn set_default_policy(&mut self, hook: Hook, verdict: Verdict) {
|
||||
self.default_policy.insert(hook, verdict);
|
||||
}
|
||||
|
||||
/// Clear per-chain counters and per-rule match counts.
|
||||
/// Rules themselves are preserved. Use this to restart metrics
|
||||
/// without losing configuration.
|
||||
pub fn reset_counters(&mut self) {
|
||||
for counter in self.chain_counters.values_mut() {
|
||||
*counter = (0, 0);
|
||||
}
|
||||
for rule in &mut self.rules {
|
||||
rule.match_count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_rule(rule: &FilterRule) -> String {
|
||||
let mut out = alloc::format!("{} [{:>4}]", rule.verdict.name(), rule.id);
|
||||
if let Some(iface) = &rule.in_dev {
|
||||
out.push_str(&alloc::format!(" in={}", iface));
|
||||
}
|
||||
if let Some(iface) = &rule.out_dev {
|
||||
out.push_str(&alloc::format!(" out={}", iface));
|
||||
}
|
||||
if let Some(src) = rule.src_addr {
|
||||
out.push_str(&alloc::format!(" src={}/{}", src, rule.src_prefix_len));
|
||||
}
|
||||
if let Some(dst) = rule.dst_addr {
|
||||
out.push_str(&alloc::format!(" dst={}/{}", dst, rule.dst_prefix_len));
|
||||
}
|
||||
if let Some(proto) = rule.protocol {
|
||||
out.push_str(&alloc::format!(" proto={}", proto.0));
|
||||
}
|
||||
if let Some(sport) = rule.src_port {
|
||||
out.push_str(&alloc::format!(" sport={}", sport));
|
||||
}
|
||||
if let Some(dport) = rule.dst_port {
|
||||
out.push_str(&alloc::format!(" dport={}", dport));
|
||||
}
|
||||
out.push_str(&alloc::format!(" matches={}", rule.match_count));
|
||||
out
|
||||
}
|
||||
|
||||
/// Errors returned by the rule parser. Each variant includes enough
|
||||
/// context to point the user at the offending token (the index into the
|
||||
/// input line where parsing failed).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ParseError {
|
||||
/// Generic parse error with a human-readable message.
|
||||
Msg(&'static str),
|
||||
/// The input did not contain a recognizable chain name.
|
||||
UnknownHook(String),
|
||||
/// The action keyword was not ACCEPT or DROP.
|
||||
UnknownVerdict(String),
|
||||
/// An address failed to parse.
|
||||
BadAddress(String),
|
||||
/// A port number failed to parse.
|
||||
BadPort(String),
|
||||
/// A flag was recognized but its value was malformed.
|
||||
BadFlagValue { flag: &'static str, value: String },
|
||||
/// The protocol name was not tcp/udp/icmp/icmp6 or a number.
|
||||
UnknownProtocol(String),
|
||||
/// The interface name was missing or empty.
|
||||
MissingInterface,
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ParseError::Msg(m) => f.write_str(m),
|
||||
ParseError::UnknownHook(s) => write!(f, "unknown chain: {}", s),
|
||||
ParseError::UnknownVerdict(s) => write!(f, "unknown verdict: {}", s),
|
||||
ParseError::BadAddress(s) => write!(f, "bad address: {}", s),
|
||||
ParseError::BadPort(s) => write!(f, "bad port: {}", s),
|
||||
ParseError::BadFlagValue { flag, value } => {
|
||||
write!(f, "bad value for {}: {}", flag, value)
|
||||
}
|
||||
ParseError::UnknownProtocol(s) => write!(f, "unknown protocol: {}", s),
|
||||
ParseError::MissingInterface => f.write_str("missing interface name"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses one iptables-style textual rule into a [`FilterRule`]. The
|
||||
/// grammar is documented at the top of this file.
|
||||
pub fn parse_rule(line: &str) -> core::result::Result<FilterRule, ParseError> {
|
||||
let mut tokens = line.split_whitespace();
|
||||
let verdict_token = tokens.next().ok_or(ParseError::Msg("missing verdict"))?;
|
||||
let verdict = match verdict_token.to_uppercase().as_str() {
|
||||
"ACCEPT" => Verdict::Accept,
|
||||
"DROP" => Verdict::Drop,
|
||||
"LOG" => Verdict::Log,
|
||||
"REJECT" => Verdict::Reject,
|
||||
_ => return Err(ParseError::UnknownVerdict(verdict_token.to_string())),
|
||||
};
|
||||
|
||||
let chain_token = tokens.next().ok_or(ParseError::Msg("missing chain"))?;
|
||||
let hook = match chain_token.to_lowercase().as_str() {
|
||||
"prerouting" => Hook::PreRouting,
|
||||
"input" => Hook::InputLocal,
|
||||
"forward" => Hook::Forward,
|
||||
"output" => Hook::OutputLocal,
|
||||
"postrouting" => Hook::PostRouting,
|
||||
_ => return Err(ParseError::UnknownHook(chain_token.to_string())),
|
||||
};
|
||||
|
||||
let mut rule = FilterRule {
|
||||
id: 0,
|
||||
hook,
|
||||
src_addr: None,
|
||||
src_prefix_len: 0,
|
||||
dst_addr: None,
|
||||
dst_prefix_len: 0,
|
||||
protocol: None,
|
||||
src_port: None,
|
||||
dst_port: None,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
state_match: None,
|
||||
verdict,
|
||||
match_count: 0,
|
||||
};
|
||||
|
||||
while let Some(flag) = tokens.next() {
|
||||
match flag {
|
||||
"-i" => {
|
||||
let name = tokens.next().ok_or(ParseError::MissingInterface)?;
|
||||
rule.in_dev = Some(Rc::from(name));
|
||||
}
|
||||
"-o" => {
|
||||
let name = tokens.next().ok_or(ParseError::MissingInterface)?;
|
||||
rule.out_dev = Some(Rc::from(name));
|
||||
}
|
||||
"-s" => {
|
||||
let value = tokens.next().ok_or(ParseError::Msg("missing -s value"))?;
|
||||
let (addr, prefix) = parse_addr_with_prefix(value)?;
|
||||
rule.src_addr = Some(addr);
|
||||
rule.src_prefix_len = prefix;
|
||||
}
|
||||
"-d" => {
|
||||
let value = tokens.next().ok_or(ParseError::Msg("missing -d value"))?;
|
||||
let (addr, prefix) = parse_addr_with_prefix(value)?;
|
||||
rule.dst_addr = Some(addr);
|
||||
rule.dst_prefix_len = prefix;
|
||||
}
|
||||
"-p" => {
|
||||
let value = tokens.next().ok_or(ParseError::Msg("missing -p value"))?;
|
||||
let proto = parse_protocol(value)?;
|
||||
rule.protocol = Some(proto);
|
||||
}
|
||||
"--sport" | "--source-port" => {
|
||||
let value = tokens.next().ok_or(ParseError::Msg("missing --sport value"))?;
|
||||
rule.src_port = Some(parse_port(value)?);
|
||||
}
|
||||
"--dport" | "--destination-port" => {
|
||||
let value = tokens.next().ok_or(ParseError::Msg("missing --dport value"))?;
|
||||
rule.dst_port = Some(parse_port(value)?);
|
||||
}
|
||||
"state" => {
|
||||
let value = tokens.next().ok_or(ParseError::Msg("missing state value"))?;
|
||||
rule.state_match = Some(match value {
|
||||
"new" => StateMatch::New,
|
||||
"established" => StateMatch::Established,
|
||||
"related" => StateMatch::Related,
|
||||
"invalid" => StateMatch::Invalid,
|
||||
_ => return Err(ParseError::Msg("unknown state value")),
|
||||
});
|
||||
}
|
||||
_ => return Err(ParseError::Msg("unknown flag")),
|
||||
}
|
||||
}
|
||||
|
||||
if rule.protocol.is_some() && (rule.src_port.is_some() || rule.dst_port.is_some()) {
|
||||
if let Some(proto) = rule.protocol {
|
||||
if proto != Protocol::TCP && proto != Protocol::UDP && proto != Protocol::ICMP6 {
|
||||
return Err(ParseError::Msg("port specified for non-transport protocol"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(rule)
|
||||
}
|
||||
|
||||
fn parse_addr_with_prefix(value: &str) -> core::result::Result<(IpAddress, u8), ParseError> {
|
||||
let (addr_str, prefix_len) = match value.split_once('/') {
|
||||
Some((a, p)) => (a, p.parse::<u8>().map_err(|_| ParseError::BadAddress(value.to_string()))?),
|
||||
None => (value, 0),
|
||||
};
|
||||
if let Ok(v4) = Ipv4Address::from_str(addr_str) {
|
||||
let prefix = if prefix_len == 0 { 32 } else { prefix_len };
|
||||
Ok((IpAddress::Ipv4(v4), prefix))
|
||||
} else if let Ok(v6) = Ipv6Address::from_str(addr_str) {
|
||||
let prefix = if prefix_len == 0 { 128 } else { prefix_len };
|
||||
Ok((IpAddress::Ipv6(v6), prefix))
|
||||
} else {
|
||||
Err(ParseError::BadAddress(value.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_protocol(value: &str) -> core::result::Result<Protocol, ParseError> {
|
||||
let lower = value.to_lowercase();
|
||||
let num: Option<u8> = lower.parse().ok();
|
||||
match (lower.as_str(), num) {
|
||||
("tcp", _) => Ok(Protocol::TCP),
|
||||
("udp", _) => Ok(Protocol::UDP),
|
||||
("icmp", _) => Ok(Protocol::ICMP),
|
||||
("icmp6", _) | ("icmpv6", _) => Ok(Protocol::ICMP6),
|
||||
(_, Some(n)) => Ok(Protocol(n)),
|
||||
_ => Err(ParseError::UnknownProtocol(value.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_port(value: &str) -> core::result::Result<u16, ParseError> {
|
||||
// Accept single port (e.g. "80") but reject port ranges (e.g.
|
||||
// "1024:65535"). The FilterRule struct uses a single u16 field
|
||||
// and does not support ranges. Silently accepting the first
|
||||
// number would make --sport 1024:65535 match only port 1024,
|
||||
// which is both wrong and misleading.
|
||||
if value.contains(':') {
|
||||
return Err(ParseError::Msg("port ranges not supported (use a single port number)"));
|
||||
}
|
||||
value
|
||||
.parse::<u16>()
|
||||
.map_err(|_| ParseError::BadPort(value.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use alloc::rc::Rc;
|
||||
use smoltcp::wire::IpAddress;
|
||||
use smoltcp::time::Instant;
|
||||
|
||||
fn make_ctx(hook: Hook, src: IpAddress, dst: IpAddress) -> PacketContext<'static> {
|
||||
PacketContext {
|
||||
hook,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: src,
|
||||
dst_addr: dst,
|
||||
protocol: 6,
|
||||
src_port: Some(1234),
|
||||
dst_port: Some(80),
|
||||
packet: &[],
|
||||
}
|
||||
}
|
||||
|
||||
fn make_drop_rule(hook: Hook) -> FilterRule {
|
||||
FilterRule {
|
||||
id: 1,
|
||||
hook,
|
||||
src_addr: None,
|
||||
src_prefix_len: 0,
|
||||
dst_addr: None,
|
||||
dst_prefix_len: 0,
|
||||
protocol: None,
|
||||
src_port: None,
|
||||
dst_port: None,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
state_match: None,
|
||||
verdict: Verdict::Drop,
|
||||
match_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_accept_rule(hook: Hook) -> FilterRule {
|
||||
let mut r = make_drop_rule(hook);
|
||||
r.id = 2;
|
||||
r.verdict = Verdict::Accept;
|
||||
r
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drop_rule_actually_drops() {
|
||||
let mut t = FilterTable::new();
|
||||
t.rules.push(make_drop_rule(Hook::InputLocal));
|
||||
let ctx = make_ctx(Hook::InputLocal, IpAddress::v4(10, 0, 0, 1), IpAddress::v4(192, 168, 1, 1));
|
||||
assert_eq!(t.evaluate(&ctx, Instant::from_millis(0)), Verdict::Drop,
|
||||
"DROP rule must drop the packet (regression test for R33 bug fix)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accept_rule_actually_accepts() {
|
||||
let mut t = FilterTable::new();
|
||||
t.rules.push(make_accept_rule(Hook::InputLocal));
|
||||
let ctx = make_ctx(Hook::InputLocal, IpAddress::v4(10, 0, 0, 1), IpAddress::v4(192, 168, 1, 1));
|
||||
assert_eq!(t.evaluate(&ctx, Instant::from_millis(0)), Verdict::Accept,
|
||||
"ACCEPT rule must accept the packet (regression test)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rule_matching_other_hook_does_not_apply() {
|
||||
let mut t = FilterTable::new();
|
||||
t.rules.push(make_drop_rule(Hook::OutputLocal));
|
||||
let ctx = make_ctx(Hook::InputLocal, IpAddress::v4(10, 0, 0, 1), IpAddress::v4(192, 168, 1, 1));
|
||||
assert_eq!(t.evaluate(&ctx, Instant::from_millis(0)), Verdict::Accept,
|
||||
"Rule for OUTPUT must not affect INPUT chain — should fall through to default policy");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_policy_applied_when_no_rules() {
|
||||
let mut t = FilterTable::new();
|
||||
t.set_default_policy(Hook::InputLocal, Verdict::Drop);
|
||||
let ctx = make_ctx(Hook::InputLocal, IpAddress::v4(10, 0, 0, 1), IpAddress::v4(192, 168, 1, 1));
|
||||
assert_eq!(t.evaluate(&ctx, Instant::from_millis(0)), Verdict::Drop,
|
||||
"Default policy applies when no matching rule");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_counters_clears_metrics_keeps_rules() {
|
||||
let mut t = FilterTable::new();
|
||||
t.rules.push(make_drop_rule(Hook::InputLocal));
|
||||
let ctx = make_ctx(Hook::InputLocal, IpAddress::v4(10, 0, 0, 1), IpAddress::v4(192, 168, 1, 1));
|
||||
let _ = t.evaluate(&ctx, Instant::from_millis(0));
|
||||
assert_eq!(t.rules[0].match_count, 1);
|
||||
let (_p, _b) = t.chain_counters.get(&Hook::InputLocal).copied().unwrap_or((0, 0));
|
||||
t.reset_counters();
|
||||
assert_eq!(t.rules[0].match_count, 0,
|
||||
"reset_counters must clear rule.match_count");
|
||||
assert_eq!(t.chain_counters.get(&Hook::InputLocal).copied().unwrap_or((0, 0)), (0, 0),
|
||||
"reset_counters must clear chain_counters");
|
||||
assert_eq!(t.rules.len(), 1,
|
||||
"reset_counters must preserve rules");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
//! ICMP error message generation — mirrors Linux 7.1's `icmp_send`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/ipv4/icmp.c:802` — `__icmp_send()` — builds ICMPv4 error messages
|
||||
//! - `net/ipv6/icmp.c:icmpv6_send()` — ICMPv6 error messages
|
||||
//! - `include/uapi/linux/icmp.h` — ICMP type/code constants
|
||||
//!
|
||||
//! ICMP error messages contain: ICMP header + original IP header + 8 bytes
|
||||
//! of original transport payload (RFC 792 §3.1, RFC 4443 §2.4).
|
||||
|
||||
use smoltcp::wire::{
|
||||
Icmpv4Packet, Icmpv4Repr, Icmpv6Packet, Icmpv6Repr, IpAddress, Ipv4Address, Ipv4Packet,
|
||||
Ipv4Repr, Ipv6Address, Ipv6Packet, Ipv6Repr,
|
||||
};
|
||||
|
||||
/// Builds an ICMPv4 Destination Unreachable error message (Type 3).
|
||||
/// Code 3 = Port Unreachable (mirrors `ICMP_PORT_UNREACH` in icmp.c).
|
||||
pub fn build_icmpv4_port_unreachable(
|
||||
original_packet: &[u8],
|
||||
) -> Option<Vec<u8>> {
|
||||
let ipv4 = Ipv4Packet::new_checked(original_packet).ok()?;
|
||||
let src = ipv4.src_addr();
|
||||
let dst = ipv4.dst_addr();
|
||||
|
||||
let ip_header_len = ipv4.header_len() as usize;
|
||||
let total_len = original_packet.len().min(ip_header_len + 8);
|
||||
|
||||
let mut payload = alloc::vec![0u8; 4 + total_len];
|
||||
payload[0] = 0;
|
||||
payload[1] = 0;
|
||||
payload[2] = 0;
|
||||
payload[3] = 0;
|
||||
payload[4..4 + total_len].copy_from_slice(&original_packet[..total_len]);
|
||||
|
||||
let icmp_repr = Icmpv4Repr::DstUnreachable {
|
||||
reason: smoltcp::wire::Icmpv4DstUnreachable::PortUnreachable,
|
||||
header: Ipv4Repr {
|
||||
src_addr: dst,
|
||||
dst_addr: src,
|
||||
next_header: smoltcp::wire::IpProtocol::Icmp,
|
||||
hop_limit: 64,
|
||||
payload_len: 4 + total_len,
|
||||
},
|
||||
data: &payload,
|
||||
};
|
||||
|
||||
let mut buf = alloc::vec![0u8; icmp_repr.buffer_len()];
|
||||
let mut pkt = Icmpv4Packet::new_unchecked(&mut buf);
|
||||
icmp_repr.emit(&mut pkt, &smoltcp::phy::ChecksumCapabilities::ignored());
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
/// Builds an ICMPv4 Time Exceeded error message (Type 11, Code 0).
|
||||
/// Mirrors `ICMP_TIME_EXCEEDED` / `ICMP_EXC_TTL` in `icmp.c:1168`.
|
||||
pub fn build_icmpv4_time_exceeded(
|
||||
original_packet: &[u8],
|
||||
) -> Option<Vec<u8>> {
|
||||
let ipv4 = Ipv4Packet::new_checked(original_packet).ok()?;
|
||||
let src = ipv4.src_addr();
|
||||
let dst = ipv4.dst_addr();
|
||||
|
||||
let ip_header_len = ipv4.header_len() as usize;
|
||||
let total_len = original_packet.len().min(ip_header_len + 8);
|
||||
|
||||
let mut payload = alloc::vec![0u8; 4 + total_len];
|
||||
payload[4..4 + total_len].copy_from_slice(&original_packet[..total_len]);
|
||||
|
||||
let icmp_repr = Icmpv4Repr::TimeExceeded {
|
||||
reason: smoltcp::wire::Icmpv4TimeExceeded::TtlExpired,
|
||||
header: Ipv4Repr {
|
||||
src_addr: dst,
|
||||
dst_addr: src,
|
||||
next_header: smoltcp::wire::IpProtocol::Icmp,
|
||||
hop_limit: 64,
|
||||
payload_len: 4 + total_len,
|
||||
},
|
||||
data: &payload,
|
||||
};
|
||||
|
||||
let mut buf = alloc::vec![0u8; icmp_repr.buffer_len()];
|
||||
let mut pkt = Icmpv4Packet::new_unchecked(&mut buf);
|
||||
icmp_repr.emit(&mut pkt, &smoltcp::phy::ChecksumCapabilities::ignored());
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
/// Builds an ICMPv6 Destination Unreachable error (Type 1, Code 4).
|
||||
pub fn build_icmpv6_port_unreachable(
|
||||
original_packet: &[u8],
|
||||
) -> Option<Vec<u8>> {
|
||||
let ipv6 = Ipv6Packet::new_checked(original_packet).ok()?;
|
||||
let src = ipv6.src_addr();
|
||||
let _dst = ipv6.dst_addr();
|
||||
|
||||
let total_len = original_packet.len().min(48);
|
||||
let mut data = alloc::vec![0u8; 4 + total_len];
|
||||
data[4..4 + total_len].copy_from_slice(&original_packet[..total_len]);
|
||||
|
||||
let icmp_repr = Icmpv6Repr::DstUnreachable {
|
||||
reason: smoltcp::wire::Icmpv6DstUnreachable::PortUnreachable,
|
||||
header: Ipv6Repr {
|
||||
src_addr: _dst,
|
||||
dst_addr: src,
|
||||
next_header: smoltcp::wire::IpProtocol::Icmpv6,
|
||||
hop_limit: 64,
|
||||
payload_len: 4 + total_len,
|
||||
},
|
||||
data: &data,
|
||||
};
|
||||
|
||||
let mut buf = alloc::vec![0u8; icmp_repr.buffer_len()];
|
||||
let mut pkt = Icmpv6Packet::new_unchecked(&mut buf);
|
||||
icmp_repr.emit(&_dst, &src, &mut pkt, &smoltcp::phy::ChecksumCapabilities::ignored());
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_standard_packet(src: [u8; 4], dst: [u8; 4]) -> Vec<u8> {
|
||||
// 20-byte standard IPv4 header + 8-byte UDP header.
|
||||
let mut p = vec![0u8; 28];
|
||||
p[0] = 0x45; // version 4, IHL 5
|
||||
p[2] = 0; p[3] = 28;
|
||||
p[8] = 64;
|
||||
p[9] = 17; // UDP
|
||||
p[10] = 0;
|
||||
p[11] = 0;
|
||||
p[12..16].copy_from_slice(&src);
|
||||
p[16..20].copy_from_slice(&dst);
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icmpv4_short_packet_returns_none() {
|
||||
let p = vec![0u8; 10];
|
||||
assert!(build_icmpv4_port_unreachable(&p).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icmpv4_preserves_destination_address() {
|
||||
// Regression: the previous code computed IHL as
|
||||
// (ipv4.version() & 0x0f) * 4 = 4*4 = 16 (WRONG).
|
||||
// smoltcp's version() returns 4 (the version number), not the
|
||||
// combined version/IHL byte. The correct IHL for a standard
|
||||
// 20-byte header is 5. With the bug, the ICMP error would
|
||||
// include only 16+8=24 bytes of original, truncating the
|
||||
// destination IP. With the fix, all 20+8=28 bytes included.
|
||||
let original = make_standard_packet([10, 0, 0, 1], [192, 168, 1, 1]);
|
||||
let icmp = build_icmpv4_port_unreachable(&original).expect("should build");
|
||||
// ICMP DstUnreachable layout:
|
||||
// bytes 0-3: type(1) + code(1) + checksum(2)
|
||||
// bytes 4-7: unused(4)
|
||||
// bytes 8-31: ICMP-wrapped IP header (src=orig_dst, dst=orig_src)
|
||||
// bytes 32+: original IP packet (and L4 data)
|
||||
// The original IP's dst IP first byte is at offset 32+16=48.
|
||||
assert_eq!(icmp[48], 192, "dst[0] must be 192, was {}", icmp[48]);
|
||||
assert_eq!(icmp[49], 168);
|
||||
assert_eq!(icmp[50], 1);
|
||||
assert_eq!(icmp[51], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icmpv4_with_ip_options_includes_extended_header() {
|
||||
// IHL=6 means 24-byte header (20 + 4 bytes options).
|
||||
// The bug would compute IHL=4 (16 bytes) and truncate.
|
||||
let mut p = vec![0u8; 32];
|
||||
p[0] = 0x46; // version 4, IHL 6
|
||||
p[2] = 0; p[3] = 32;
|
||||
p[8] = 64;
|
||||
p[9] = 17;
|
||||
p[10] = 0;
|
||||
p[11] = 0;
|
||||
p[12] = 10; p[13] = 0; p[14] = 0; p[15] = 1; // src
|
||||
p[16] = 192; p[17] = 168; p[18] = 1; p[19] = 1; // dst
|
||||
let icmp = build_icmpv4_port_unreachable(&p).expect("should build");
|
||||
// Source IP of the original packet is at offset 32+12=44.
|
||||
// (See comment above for the full ICMP layout.)
|
||||
assert_eq!(icmp[44], 10, "src[0] must be 10 with IHL=6");
|
||||
assert_eq!(icmp[45], 0);
|
||||
assert_eq!(icmp[46], 0);
|
||||
assert_eq!(icmp[47], 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
//! Bonding (Link Aggregation) — mirrors Linux 7.1's `drivers/net/bonding/`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `drivers/net/bonding/bond_main.c:1589` — `bond_should_deliver` (frame handling)
|
||||
//! - `drivers/net/bonding/bond_main.c:328` — mode selection (round-robin vs active-backup)
|
||||
//! - `drivers/net/bonding/bond_3ad.c` — LACP (not yet implemented)
|
||||
//!
|
||||
//! Two modes are supported:
|
||||
//! - **ActiveBackup**: one slave is active, others standby. If active fails,
|
||||
//! the next standby takes over. Mirrors `BOND_MODE_ACTIVEBACKUP`.
|
||||
//! - **RoundRobin**: packets are distributed across slaves in sequence.
|
||||
//! Mirrors `BOND_MODE_ROUNDROBIN`.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr};
|
||||
|
||||
use super::LinkDevice;
|
||||
use super::Stats;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BondMode {
|
||||
ActiveBackup,
|
||||
RoundRobin,
|
||||
}
|
||||
|
||||
struct Slave {
|
||||
dev: Box<dyn LinkDevice>,
|
||||
up: bool,
|
||||
}
|
||||
|
||||
pub struct BondDevice {
|
||||
name: Rc<str>,
|
||||
slaves: RefCell<Vec<Slave>>,
|
||||
active_slave: RefCell<usize>,
|
||||
mode: BondMode,
|
||||
rr_counter: RefCell<usize>,
|
||||
recv_buffer: Vec<u8>,
|
||||
ip_address: Option<IpCidr>,
|
||||
}
|
||||
|
||||
impl BondDevice {
|
||||
pub fn new(name: &str, mode: BondMode) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
slaves: RefCell::new(Vec::new()),
|
||||
active_slave: RefCell::new(0),
|
||||
mode,
|
||||
rr_counter: RefCell::new(0),
|
||||
recv_buffer: Vec::with_capacity(1500),
|
||||
ip_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_slave<T: LinkDevice + 'static>(&self, dev: T) {
|
||||
self.slaves.borrow_mut().push(Slave {
|
||||
dev: Box::new(dev),
|
||||
up: true,
|
||||
});
|
||||
}
|
||||
|
||||
fn select_slave(&self) -> Option<usize> {
|
||||
let slaves = self.slaves.borrow();
|
||||
match self.mode {
|
||||
BondMode::ActiveBackup => {
|
||||
let active = *self.active_slave.borrow();
|
||||
if active < slaves.len() && slaves[active].up {
|
||||
Some(active)
|
||||
} else {
|
||||
for (idx, slave) in slaves.iter().enumerate() {
|
||||
if slave.up {
|
||||
*self.active_slave.borrow_mut() = idx;
|
||||
return Some(idx);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
BondMode::RoundRobin => {
|
||||
let count = slaves.len();
|
||||
if count == 0 {
|
||||
return None;
|
||||
}
|
||||
let idx = *self.rr_counter.borrow() % count;
|
||||
*self.rr_counter.borrow_mut() = idx.wrapping_add(1);
|
||||
if !slaves[idx].up {
|
||||
for (i, slave) in slaves.iter().enumerate() {
|
||||
if slave.up {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
Some(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for BondDevice {
|
||||
fn send(&mut self, next_hop: IpAddress, packet: &[u8], now: Instant) {
|
||||
if let Some(idx) = self.select_slave() {
|
||||
let mut slaves = self.slaves.borrow_mut();
|
||||
if let Some(slave) = slaves.get_mut(idx) {
|
||||
slave.dev.send(next_hop, packet, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn recv(&mut self, now: Instant) -> Option<&[u8]> {
|
||||
let mut slaves = self.slaves.borrow_mut();
|
||||
for slave in slaves.iter_mut() {
|
||||
if !slave.up {
|
||||
continue;
|
||||
}
|
||||
if let Some(buf) = slave.dev.recv(now) {
|
||||
self.recv_buffer.clear();
|
||||
self.recv_buffer.extend_from_slice(buf);
|
||||
return Some(&self.recv_buffer);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn name(&self) -> &Rc<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn can_recv(&self) -> bool {
|
||||
self.slaves.borrow().iter().any(|s| s.up && s.dev.can_recv())
|
||||
}
|
||||
|
||||
fn mac_address(&self) -> Option<EthernetAddress> {
|
||||
self.slaves.borrow().first().and_then(|s| s.dev.mac_address())
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, addr: EthernetAddress) {
|
||||
for slave in self.slaves.borrow_mut().iter_mut() {
|
||||
slave.dev.set_mac_address(addr);
|
||||
}
|
||||
}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
|
||||
fn link_state(&self) -> &'static str {
|
||||
let active = *self.active_slave.borrow();
|
||||
let slaves = self.slaves.borrow();
|
||||
if slaves.is_empty() { return "down"; }
|
||||
if active < slaves.len() && slaves[active].up { "up" } else { "degraded" }
|
||||
}
|
||||
|
||||
fn arp_table(&self) -> String {
|
||||
let slaves = self.slaves.borrow();
|
||||
let active = *self.active_slave.borrow();
|
||||
let mut out = format!("Bond mode: {:?}, active slave: {}\n",
|
||||
self.mode, if active < slaves.len() { slaves[active].dev.name().as_ref() } else { "none" });
|
||||
for (i, s) in slaves.iter().enumerate() {
|
||||
out.push_str(&format!(" slave {}: {} ({})\n", i, s.dev.name(), if s.up { "up" } else { "down" }));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn statistics(&self) -> Stats {
|
||||
let mut total = Stats::default();
|
||||
for s in self.slaves.borrow().iter() {
|
||||
let stats = s.dev.statistics();
|
||||
total.rx_bytes += stats.rx_bytes;
|
||||
total.rx_packets += stats.rx_packets;
|
||||
total.tx_bytes += stats.tx_bytes;
|
||||
total.tx_packets += stats.tx_packets;
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
fn arp_stats(&self) -> String {
|
||||
let mut out = String::new();
|
||||
let slaves = self.slaves.borrow();
|
||||
for (i, s) in slaves.iter().enumerate() {
|
||||
let stats = s.dev.arp_stats();
|
||||
if !stats.is_empty() {
|
||||
out.push_str(&format!("slave{}: {}", i, stats));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
//! 802.1D MAC Learning Bridge — mirrors Linux 7.1's `net/bridge/`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/bridge/br.c` — bridge core (`br_add_bridge`, `br_del_bridge`)
|
||||
//! - `net/bridge/br_fdb.c` — forwarding database (MAC learning + aging)
|
||||
//! - `net/bridge/br_forward.c` — frame forwarding logic
|
||||
//! - `net/bridge/br_input.c` — ingress frame handling
|
||||
//! - `net/bridge/br_device.c` — bridge as a `net_device`
|
||||
//!
|
||||
//! The bridge composes multiple link-layer devices and forwards Ethernet
|
||||
//! frames between them based on a dynamically-learned MAC→port table.
|
||||
//! Unknown destinations are flooded to all other ports, mirroring Linux's
|
||||
//! `br_flood()` in `br_forward.c`.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::time::{Duration, Instant};
|
||||
use smoltcp::wire::{
|
||||
EthernetAddress, EthernetFrame, EthernetProtocol, EthernetRepr, IpAddress, IpCidr,
|
||||
};
|
||||
|
||||
use super::stp::{self, BPDU_MAC, PortState, StpState};
|
||||
use super::LinkDevice;
|
||||
use super::Stats;
|
||||
|
||||
const MAC_AGE_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
|
||||
struct MacEntry {
|
||||
port: usize,
|
||||
last_seen: Instant,
|
||||
}
|
||||
|
||||
pub struct BridgeDevice {
|
||||
name: Rc<str>,
|
||||
ports: RefCell<Vec<Box<dyn LinkDevice>>>,
|
||||
mac_table: RefCell<BTreeMap<EthernetAddress, MacEntry>>,
|
||||
stp: RefCell<Option<StpState>>,
|
||||
recv_buffer: Vec<u8>,
|
||||
ip_address: Option<IpCidr>,
|
||||
}
|
||||
|
||||
impl BridgeDevice {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
ports: RefCell::new(Vec::new()),
|
||||
mac_table: RefCell::new(BTreeMap::new()),
|
||||
stp: RefCell::new(None),
|
||||
recv_buffer: Vec::with_capacity(1500),
|
||||
ip_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_port<T: LinkDevice + 'static>(&self, dev: T) {
|
||||
let mac = dev.mac_address();
|
||||
self.ports.borrow_mut().push(Box::new(dev));
|
||||
if let Some(stp) = self.stp.borrow_mut().as_mut() {
|
||||
stp.port_states.push(PortState::Forwarding);
|
||||
}
|
||||
let _ = mac;
|
||||
}
|
||||
|
||||
/// Enable STP with the given bridge priority and MAC.
|
||||
/// Must be called after all ports are added.
|
||||
pub fn enable_stp(&self, priority: u16, bridge_mac: EthernetAddress) {
|
||||
let port_count = self.ports.borrow().len();
|
||||
*self.stp.borrow_mut() = Some(StpState::new(priority, bridge_mac, port_count));
|
||||
}
|
||||
|
||||
fn learn(&self, mac: EthernetAddress, port: usize, now: Instant) {
|
||||
if !mac.is_unicast() {
|
||||
return;
|
||||
}
|
||||
self.mac_table.borrow_mut().insert(
|
||||
mac,
|
||||
MacEntry {
|
||||
port,
|
||||
last_seen: now,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn age_entries(&self, now: Instant) {
|
||||
self.mac_table
|
||||
.borrow_mut()
|
||||
.retain(|_, e| now < e.last_seen + MAC_AGE_TIMEOUT);
|
||||
}
|
||||
|
||||
fn lookup(&self, mac: EthernetAddress) -> Option<usize> {
|
||||
self.mac_table.borrow().get(&mac).map(|e| e.port)
|
||||
}
|
||||
|
||||
fn flood(&self, packet: &[u8], now: Instant, except_port: Option<usize>) {
|
||||
for (idx, port) in self.ports.borrow_mut().iter_mut().enumerate() {
|
||||
if Some(idx) == except_port {
|
||||
continue;
|
||||
}
|
||||
if self.stp.borrow().as_ref().is_some_and(|s| s.is_blocked(idx)) {
|
||||
continue;
|
||||
}
|
||||
port.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), packet, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for BridgeDevice {
|
||||
fn send(&mut self, _next_hop: IpAddress, packet: &[u8], now: Instant) {
|
||||
if packet.len() < 14 {
|
||||
return;
|
||||
}
|
||||
let frame = EthernetFrame::new_unchecked(packet);
|
||||
let Ok(repr) = EthernetRepr::parse(&frame) else {
|
||||
return;
|
||||
};
|
||||
let dst_mac = repr.dst_addr;
|
||||
if repr.dst_addr.is_broadcast() || repr.dst_addr.is_multicast() {
|
||||
self.flood(packet, now, None);
|
||||
} else if let Some(port_idx) = self.lookup(dst_mac) {
|
||||
if self.stp.borrow().as_ref().is_some_and(|s| s.is_blocked(port_idx)) {
|
||||
return;
|
||||
}
|
||||
if let Some(port) = self.ports.borrow_mut().get_mut(port_idx) {
|
||||
port.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), packet, now);
|
||||
}
|
||||
} else {
|
||||
self.flood(packet, now, None);
|
||||
}
|
||||
}
|
||||
|
||||
fn recv(&mut self, now: Instant) -> Option<&[u8]> {
|
||||
self.age_entries(now);
|
||||
|
||||
// STP hello timer — send periodic BPDUs if we're the root bridge
|
||||
if self.stp.borrow_mut().as_mut().is_some_and(|s| s.send_hello(now)) {
|
||||
let bpdu = {
|
||||
let stp = self.stp.borrow();
|
||||
stp.as_ref().map(|s| s.build_bpdu())
|
||||
};
|
||||
if let Some(bpdu) = bpdu {
|
||||
for port in self.ports.borrow_mut().iter_mut() {
|
||||
port.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), &bpdu, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut received: Option<(usize, Vec<u8>, EthernetAddress, EthernetProtocol)> = None;
|
||||
{
|
||||
let mut ports = self.ports.borrow_mut();
|
||||
for (port_idx, port) in ports.iter_mut().enumerate() {
|
||||
if let Some(buf) = port.recv(now) {
|
||||
let frame = EthernetFrame::new_unchecked(buf);
|
||||
let Ok(repr) = EthernetRepr::parse(&frame) else {
|
||||
continue;
|
||||
};
|
||||
self.learn(repr.src_addr, port_idx, now);
|
||||
received = Some((port_idx, buf.to_vec(), repr.dst_addr, repr.ethertype));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((port_idx, packet, dst_mac, ethertype)) = received {
|
||||
// BPDU: process via STP, don't forward
|
||||
if dst_mac == BPDU_MAC {
|
||||
let response = self.stp.borrow_mut().as_mut()
|
||||
.and_then(|s| s.process_bpdu(port_idx, &packet, now));
|
||||
if let Some(rsp) = response {
|
||||
if let Some(port) = self.ports.borrow_mut().get_mut(port_idx) {
|
||||
port.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), &rsp, now);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if ethertype == EthernetProtocol::Arp
|
||||
|| ethertype == EthernetProtocol::Ipv4
|
||||
|| ethertype == EthernetProtocol::Ipv6
|
||||
{
|
||||
if dst_mac.is_broadcast() || dst_mac.is_multicast() {
|
||||
self.recv_buffer = packet.clone();
|
||||
self.flood(&self.recv_buffer, now, Some(port_idx));
|
||||
return Some(&self.recv_buffer);
|
||||
} else if let Some(dst_port_idx) = self.lookup(dst_mac) {
|
||||
if dst_port_idx != port_idx
|
||||
&& !self.stp.borrow().as_ref().is_some_and(|s| s.is_blocked(dst_port_idx))
|
||||
{
|
||||
let mut ports = self.ports.borrow_mut();
|
||||
if let Some(target) = ports.get_mut(dst_port_idx) {
|
||||
target.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), &packet, now);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
self.recv_buffer = packet;
|
||||
return Some(&self.recv_buffer);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn name(&self) -> &Rc<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn can_recv(&self) -> bool {
|
||||
self.ports.borrow().iter().any(|p| p.can_recv())
|
||||
}
|
||||
|
||||
fn mac_address(&self) -> Option<EthernetAddress> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, _addr: EthernetAddress) {}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
|
||||
fn arp_table(&self) -> String {
|
||||
let mut out = String::from("Bridge FDB:\n");
|
||||
for (mac, entry) in self.mac_table.borrow().iter() {
|
||||
out.push_str(&format!(" {} port={} last_seen={}\n", mac, entry.port, entry.last_seen));
|
||||
}
|
||||
if self.mac_table.borrow().is_empty() {
|
||||
out.push_str(" (empty)\n");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn link_state(&self) -> &'static str {
|
||||
if self.ports.borrow().is_empty() { "down" } else { "up" }
|
||||
}
|
||||
|
||||
fn statistics(&self) -> Stats {
|
||||
let mut total = Stats::default();
|
||||
for port in self.ports.borrow().iter() {
|
||||
let s = port.statistics();
|
||||
total.rx_bytes += s.rx_bytes;
|
||||
total.rx_packets += s.rx_packets;
|
||||
total.tx_bytes += s.tx_bytes;
|
||||
total.tx_packets += s.tx_packets;
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
fn arp_stats(&self) -> String {
|
||||
let mut out = String::new();
|
||||
for (i, port) in self.ports.borrow().iter().enumerate() {
|
||||
let s = port.arp_stats();
|
||||
if !s.is_empty() {
|
||||
out.push_str(&format!("port{}: {}", i, s));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smoltcp::wire::EthernetAddress;
|
||||
|
||||
fn mac(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8) -> EthernetAddress {
|
||||
EthernetAddress([a, b, c, d, e, f])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn learn_stores_unicast_mapping() {
|
||||
let b = BridgeDevice::new("br0");
|
||||
let m = mac(0x00, 0x11, 0x22, 0x33, 0x44, 0x55);
|
||||
b.learn(m, 2, Instant::from_secs(0));
|
||||
assert_eq!(b.lookup(m), Some(2),
|
||||
"Learned MAC must be found on port 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn learn_ignores_multicast() {
|
||||
let b = BridgeDevice::new("br0");
|
||||
let multicast = EthernetAddress::BROADCAST;
|
||||
b.learn(multicast, 1, Instant::from_secs(0));
|
||||
assert_eq!(b.lookup(multicast), None,
|
||||
"Multicast MACs must not be learned");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn learn_replaces_existing_entry_on_new_port() {
|
||||
let b = BridgeDevice::new("br0");
|
||||
let m = mac(0x00, 0x11, 0x22, 0x33, 0x44, 0x55);
|
||||
b.learn(m, 1, Instant::from_secs(0));
|
||||
assert_eq!(b.lookup(m), Some(1));
|
||||
b.learn(m, 2, Instant::from_secs(10));
|
||||
assert_eq!(b.lookup(m), Some(2),
|
||||
"MAC move from port 1 to port 2 must update FDB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn age_entries_removes_expired_macs() {
|
||||
let b = BridgeDevice::new("br0");
|
||||
let m1 = mac(0x00, 0x11, 0x22, 0x33, 0x44, 0x55);
|
||||
let m2 = mac(0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE);
|
||||
b.learn(m1, 1, Instant::from_secs(0));
|
||||
b.learn(m2, 2, Instant::from_secs(MAC_AGE_TIMEOUT.secs() as i64)); // Age boundary
|
||||
// Age just past the 300s timeout
|
||||
b.age_entries(Instant::from_secs(MAC_AGE_TIMEOUT.secs() as i64 + 1));
|
||||
assert_eq!(b.lookup(m1), None, "m1 (learned at 0) must be aged out");
|
||||
assert_eq!(b.lookup(m2), Some(2),
|
||||
"m2 (learned at 300s) must still be valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_returns_none_for_unknown_mac() {
|
||||
let b = BridgeDevice::new("br0");
|
||||
assert_eq!(b.lookup(mac(0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01)), None);
|
||||
}
|
||||
}
|
||||
+627
-42
@@ -4,14 +4,19 @@ use std::fs::File;
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::phy::ChecksumCapabilities;
|
||||
use smoltcp::storage::PacketMetadata;
|
||||
use smoltcp::time::{Duration, Instant};
|
||||
use smoltcp::wire::{
|
||||
ArpOperation, ArpPacket, ArpRepr, EthernetAddress, EthernetFrame, EthernetProtocol,
|
||||
EthernetRepr, IpAddress, IpCidr, Ipv4Address, Ipv4Cidr,
|
||||
EthernetRepr, Icmpv6Packet, Icmpv6Repr, IpAddress, IpCidr, IpProtocol, Ipv4Address,
|
||||
Ipv6Address, Ipv6Cidr, Ipv6Packet, NdiscNeighborFlags, NdiscPrefixInfoFlags, NdiscRepr,
|
||||
RawHardwareAddress,
|
||||
};
|
||||
|
||||
use super::LinkDevice;
|
||||
use crate::link::qdisc::QdiscConfig;
|
||||
use super::{LinkDevice, Stats};
|
||||
use crate::slaac;
|
||||
|
||||
struct Neighbor {
|
||||
hardware_address: EthernetAddress,
|
||||
@@ -29,20 +34,66 @@ enum ArpState {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum NdpState {
|
||||
#[default]
|
||||
Discovered,
|
||||
Discovering {
|
||||
target: Ipv6Address,
|
||||
tries: u32,
|
||||
silent_until: Instant,
|
||||
},
|
||||
}
|
||||
|
||||
type PacketBuffer = smoltcp::storage::PacketBuffer<'static, IpAddress>;
|
||||
|
||||
const EMPTY_MAC: EthernetAddress = EthernetAddress([0; 6]);
|
||||
const NDP_MAX_TRIES: u32 = 3;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum SlaacState {
|
||||
#[default]
|
||||
Idle,
|
||||
LinkLocalOnly,
|
||||
RsSent {
|
||||
silent_until: Instant,
|
||||
},
|
||||
Configured,
|
||||
}
|
||||
|
||||
fn solicited_node_multicast(target: &Ipv6Address) -> Ipv6Address {
|
||||
let o = target.octets();
|
||||
Ipv6Address::new(
|
||||
0xff02, 0, 0, 0, 0, 1, 0xff00 | (o[13] as u16), ((o[14] as u16) << 8) | (o[15] as u16),
|
||||
)
|
||||
}
|
||||
|
||||
fn multicast_mac(addr: &Ipv6Address) -> EthernetAddress {
|
||||
let o = addr.octets();
|
||||
EthernetAddress([0x33, 0x33, o[12], o[13], o[14], o[15]])
|
||||
}
|
||||
|
||||
pub struct EthernetLink {
|
||||
name: Rc<str>,
|
||||
neighbor_cache: BTreeMap<IpAddress, Neighbor>,
|
||||
arp_state: ArpState,
|
||||
ndp_state: NdpState,
|
||||
slaac_state: SlaacState,
|
||||
waiting_packets: PacketBuffer,
|
||||
input_buffer: Vec<u8>,
|
||||
output_buffer: Vec<u8>,
|
||||
network_file: File,
|
||||
hardware_address: Option<EthernetAddress>,
|
||||
ip_address: Option<Ipv4Cidr>,
|
||||
ip_address: Option<IpCidr>,
|
||||
qdisc_config: QdiscConfig,
|
||||
mtu: usize,
|
||||
enabled: bool,
|
||||
promiscuous: bool,
|
||||
stats: Stats,
|
||||
arp_requests: u64,
|
||||
arp_replies: u64,
|
||||
arp_cache_hits: u64,
|
||||
arp_cache_misses: u64,
|
||||
}
|
||||
|
||||
impl EthernetLink {
|
||||
@@ -53,6 +104,7 @@ impl EthernetLink {
|
||||
|
||||
const NEIGHBOR_LIVE_TIME: Duration = Duration::from_secs(60);
|
||||
const ARP_SILENCE_TIME: Duration = Duration::from_secs(1);
|
||||
const NDP_SILENCE_TIME: Duration = Duration::from_secs(1);
|
||||
|
||||
pub fn new(name: &str, network_file: File) -> Self {
|
||||
let waiting_packets = PacketBuffer::new(
|
||||
@@ -69,7 +121,18 @@ impl EthernetLink {
|
||||
input_buffer: vec![0u8; Self::MTU],
|
||||
output_buffer: Vec::with_capacity(Self::MTU),
|
||||
arp_state: Default::default(),
|
||||
ndp_state: Default::default(),
|
||||
slaac_state: Default::default(),
|
||||
qdisc_config: QdiscConfig::default(),
|
||||
mtu: Self::MTU,
|
||||
enabled: true,
|
||||
promiscuous: false,
|
||||
stats: Stats::default(),
|
||||
neighbor_cache: Default::default(),
|
||||
arp_requests: 0,
|
||||
arp_replies: 0,
|
||||
arp_cache_hits: 0,
|
||||
arp_cache_misses: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +158,7 @@ impl EthernetLink {
|
||||
f(frame.payload_mut());
|
||||
|
||||
if let Err(_) = self.network_file.write_all(&self.output_buffer) {
|
||||
self.stats.tx_errors += 1;
|
||||
error!(
|
||||
"Dropped outboud packet on {} (failed to write to network file)",
|
||||
self.name
|
||||
@@ -107,13 +171,14 @@ impl EthernetLink {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(ip_addr) = self.ip_address else {
|
||||
let Some(IpCidr::Ipv4(ip_addr)) = self.ip_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(repr) = ArpPacket::new_checked(packet).and_then(|packet| ArpRepr::parse(&packet))
|
||||
else {
|
||||
debug!("Dropped incomming arp packet on {} (Malformed)", self.name);
|
||||
self.stats.rx_errors += 1;
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -156,6 +221,7 @@ impl EthernetLink {
|
||||
expires_at: now + Self::NEIGHBOR_LIVE_TIME,
|
||||
},
|
||||
);
|
||||
self.arp_replies += 1;
|
||||
|
||||
if let ArpOperation::Request = operation {
|
||||
let response = ArpRepr::EthernetIpv4 {
|
||||
@@ -194,6 +260,10 @@ impl EthernetLink {
|
||||
self.send_arp(now);
|
||||
break;
|
||||
}
|
||||
Ok((IpAddress::Ipv6(_), _)) => {
|
||||
let _ = waiting_packets.dequeue();
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
self.arp_state = ArpState::Discovered;
|
||||
break;
|
||||
@@ -204,7 +274,7 @@ impl EthernetLink {
|
||||
self.send_to(
|
||||
mac,
|
||||
packet.len(),
|
||||
|buf| buf.copy_from_slice(packet),
|
||||
|buf| buf.copy_from_slice(&packet),
|
||||
EthernetProtocol::Ipv4,
|
||||
);
|
||||
}
|
||||
@@ -227,6 +297,10 @@ impl EthernetLink {
|
||||
|
||||
return;
|
||||
}
|
||||
Ok((IpAddress::Ipv6(_), _)) => {
|
||||
let _ = self.waiting_packets.dequeue();
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
self.arp_state = ArpState::Discovered;
|
||||
return;
|
||||
@@ -234,6 +308,7 @@ impl EthernetLink {
|
||||
}
|
||||
|
||||
let _ = self.waiting_packets.dequeue();
|
||||
self.stats.rx_dropped += 1;
|
||||
debug!(
|
||||
"Dropped packet on {} because neighbor was not found",
|
||||
self.name
|
||||
@@ -241,8 +316,76 @@ impl EthernetLink {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_waiting_packets_v6(&mut self, ip: Ipv6Address, mac: EthernetAddress) {
|
||||
let mut waiting_packets =
|
||||
std::mem::replace(&mut self.waiting_packets, PacketBuffer::new(vec![], vec![]));
|
||||
loop {
|
||||
match waiting_packets.peek() {
|
||||
Ok((IpAddress::Ipv6(dst), _)) if *dst == ip => {}
|
||||
Ok((IpAddress::Ipv6(dst), _)) => {
|
||||
self.ndp_state = NdpState::Discovering {
|
||||
target: *dst,
|
||||
tries: 0,
|
||||
silent_until: Instant::ZERO,
|
||||
};
|
||||
self.send_ndp_solicit(Instant::ZERO);
|
||||
break;
|
||||
}
|
||||
Ok((IpAddress::Ipv4(_), _)) => {
|
||||
let _ = waiting_packets.dequeue();
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
self.ndp_state = NdpState::Discovered;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let (_, packet) = waiting_packets.dequeue().unwrap();
|
||||
self.send_to(
|
||||
mac,
|
||||
packet.len(),
|
||||
|buf| buf.copy_from_slice(&packet),
|
||||
EthernetProtocol::Ipv6,
|
||||
);
|
||||
}
|
||||
|
||||
self.waiting_packets = waiting_packets;
|
||||
}
|
||||
|
||||
fn drop_waiting_packets_v6(&mut self, ip: Ipv6Address) {
|
||||
loop {
|
||||
match self.waiting_packets.peek() {
|
||||
Ok((IpAddress::Ipv6(dst), _)) if *dst == ip => {}
|
||||
Ok((IpAddress::Ipv6(dst), _)) => {
|
||||
self.ndp_state = NdpState::Discovering {
|
||||
target: *dst,
|
||||
tries: 0,
|
||||
silent_until: Instant::ZERO,
|
||||
};
|
||||
self.send_ndp_solicit(Instant::ZERO);
|
||||
return;
|
||||
}
|
||||
Ok((IpAddress::Ipv4(_), _)) => {
|
||||
let _ = self.waiting_packets.dequeue();
|
||||
self.stats.rx_dropped += 1;
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
self.ndp_state = NdpState::Discovered;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.waiting_packets.dequeue();
|
||||
self.stats.rx_dropped += 1;
|
||||
debug!("Dropped packet on {} because IPv6 neighbor was not found", self.name);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_missing_neighbor(&mut self, next_hop: IpAddress, packet: &[u8], now: Instant) {
|
||||
let Ok(buf) = self.waiting_packets.enqueue(packet.len(), next_hop) else {
|
||||
self.stats.rx_dropped += 1;
|
||||
warn!(
|
||||
"Dropped packet on {} because waiting queue was full",
|
||||
self.name
|
||||
@@ -251,24 +394,37 @@ impl EthernetLink {
|
||||
};
|
||||
buf.copy_from_slice(packet);
|
||||
|
||||
let IpAddress::Ipv4(next_hop) = next_hop;
|
||||
if let ArpState::Discovered = self.arp_state {
|
||||
self.arp_state = ArpState::Discovering {
|
||||
target: next_hop,
|
||||
tries: 0,
|
||||
silent_until: Instant::ZERO,
|
||||
};
|
||||
|
||||
self.send_arp(now)
|
||||
match next_hop {
|
||||
IpAddress::Ipv4(v4) => {
|
||||
if let ArpState::Discovered = self.arp_state {
|
||||
self.arp_state = ArpState::Discovering {
|
||||
target: v4,
|
||||
tries: 0,
|
||||
silent_until: Instant::ZERO,
|
||||
};
|
||||
self.send_arp(now)
|
||||
}
|
||||
}
|
||||
IpAddress::Ipv6(v6) => {
|
||||
if let NdpState::Discovered = self.ndp_state {
|
||||
self.ndp_state = NdpState::Discovering {
|
||||
target: v6,
|
||||
tries: 0,
|
||||
silent_until: Instant::ZERO,
|
||||
};
|
||||
self.send_ndp_solicit(now)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_arp(&mut self, now: Instant) {
|
||||
self.arp_requests += 1;
|
||||
let Some(hardware_address) = self.hardware_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(ip_address) = self.ip_address else {
|
||||
let Some(IpCidr::Ipv4(ip_address)) = self.ip_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -303,38 +459,321 @@ impl EthernetLink {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_ndp_solicit(&mut self, now: Instant) {
|
||||
let Some(hardware_address) = self.hardware_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(IpCidr::Ipv6(_)) = self.ip_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Extract target. Do NOT destructure tries/silent_until by value
|
||||
// — the recursive call into drop_waiting_packets_v6 can modify
|
||||
// self.ndp_state and a later write-back would clobber that
|
||||
// update. We read tries/silent_until from the live state right
|
||||
// before we write it back.
|
||||
let target = match self.ndp_state {
|
||||
NdpState::Discovered => return,
|
||||
NdpState::Discovering { target, .. } => target,
|
||||
};
|
||||
|
||||
{
|
||||
let (tries, silent_until) = match self.ndp_state {
|
||||
NdpState::Discovering { tries, silent_until, .. } => (tries, silent_until),
|
||||
NdpState::Discovered => return,
|
||||
};
|
||||
if silent_until > now {
|
||||
return;
|
||||
}
|
||||
if tries >= NDP_MAX_TRIES {
|
||||
self.drop_waiting_packets_v6(target);
|
||||
self.ndp_state = NdpState::Discovered;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let snmc = solicited_node_multicast(&target);
|
||||
let dst_mac = multicast_mac(&snmc);
|
||||
let ndisc = NdiscRepr::NeighborSolicit {
|
||||
target_addr: target,
|
||||
lladdr: Some(RawHardwareAddress::from(hardware_address)),
|
||||
};
|
||||
let icmp = Icmpv6Repr::Ndisc(ndisc);
|
||||
let mut buf = [0u8; 128];
|
||||
let mut icmp_pkt = Icmpv6Packet::new_unchecked(&mut buf);
|
||||
let src_addr = Ipv6Address::new(0, 0, 0, 0, 0, 0, 0, 0);
|
||||
let dst_addr = snmc;
|
||||
icmp.emit(&src_addr, &dst_addr, &mut icmp_pkt, &ChecksumCapabilities::ignored());
|
||||
let icmp_len = icmp.buffer_len();
|
||||
|
||||
// Read the current state (which may have been updated by a
|
||||
// recursive call into drop_waiting_packets_v6) and update
|
||||
// incrementally. This avoids overwriting a recursive update.
|
||||
let (mut tries, mut silent_until) = match self.ndp_state {
|
||||
NdpState::Discovering { tries, silent_until, .. } => (tries, silent_until),
|
||||
NdpState::Discovered => return,
|
||||
};
|
||||
tries += 1;
|
||||
silent_until = now + Self::NDP_SILENCE_TIME;
|
||||
self.ndp_state = NdpState::Discovering {
|
||||
target,
|
||||
tries,
|
||||
silent_until,
|
||||
};
|
||||
|
||||
self.send_to(
|
||||
dst_mac,
|
||||
icmp_len,
|
||||
|out| out[..icmp_len].copy_from_slice(&buf[..icmp_len]),
|
||||
EthernetProtocol::Ipv6,
|
||||
);
|
||||
}
|
||||
|
||||
fn send_router_solicitation(&mut self, now: Instant) {
|
||||
let Some(hardware_address) = self.hardware_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(IpCidr::Ipv6(our_addr)) = self.ip_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let addr_octets = our_addr.address().octets();
|
||||
if addr_octets[0] != 0xfe || addr_octets[1] & 0xc0 != 0x80 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let SlaacState::RsSent { silent_until } = self.slaac_state {
|
||||
if silent_until > now {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let dst_mac = EthernetAddress([0x33, 0x33, 0, 0, 0, 2]);
|
||||
let ndisc = NdiscRepr::RouterSolicit {
|
||||
lladdr: Some(RawHardwareAddress::from(hardware_address)),
|
||||
};
|
||||
let icmp = Icmpv6Repr::Ndisc(ndisc);
|
||||
let mut buf = [0u8; 128];
|
||||
let mut icmp_pkt = Icmpv6Packet::new_unchecked(&mut buf);
|
||||
icmp.emit(
|
||||
&our_addr.address(),
|
||||
&slaac::ALL_ROUTERS_MULTICAST,
|
||||
&mut icmp_pkt,
|
||||
&ChecksumCapabilities::ignored(),
|
||||
);
|
||||
let icmp_len = icmp.buffer_len();
|
||||
|
||||
self.slaac_state = SlaacState::RsSent {
|
||||
silent_until: now + Self::NDP_SILENCE_TIME,
|
||||
};
|
||||
|
||||
self.send_to(
|
||||
dst_mac,
|
||||
icmp_len,
|
||||
|out| out[..icmp_len].copy_from_slice(&buf[..icmp_len]),
|
||||
EthernetProtocol::Ipv6,
|
||||
);
|
||||
}
|
||||
|
||||
fn process_ndp(&mut self, packet: &[u8], now: Instant) {
|
||||
let Some(hardware_address) = self.hardware_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(IpCidr::Ipv6(our_addr)) = self.ip_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(ipv6_pkt) = Ipv6Packet::new_checked(packet) else {
|
||||
return;
|
||||
};
|
||||
if ipv6_pkt.next_header() != IpProtocol::Icmpv6 {
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(icmp_pkt) = Icmpv6Packet::new_checked(ipv6_pkt.payload()) else {
|
||||
return;
|
||||
};
|
||||
let Ok(icmp_repr) = Icmpv6Repr::parse(
|
||||
&our_addr.address(),
|
||||
&ipv6_pkt.src_addr(),
|
||||
&icmp_pkt,
|
||||
&ChecksumCapabilities::ignored(),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Icmpv6Repr::Ndisc(ndisc) = icmp_repr else {
|
||||
return;
|
||||
};
|
||||
|
||||
match ndisc {
|
||||
NdiscRepr::NeighborSolicit { target_addr, lladdr } => {
|
||||
if target_addr != our_addr.address() {
|
||||
return;
|
||||
}
|
||||
let src_addr = ipv6_pkt.src_addr();
|
||||
let Some(src_mac) = lladdr.map(|a| EthernetAddress::from_bytes(a.as_bytes()))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.neighbor_cache.insert(
|
||||
IpAddress::Ipv6(src_addr),
|
||||
Neighbor {
|
||||
hardware_address: src_mac,
|
||||
expires_at: now + Self::NEIGHBOR_LIVE_TIME,
|
||||
},
|
||||
);
|
||||
|
||||
let response = NdiscRepr::NeighborAdvert {
|
||||
flags: NdiscNeighborFlags::SOLICITED | NdiscNeighborFlags::OVERRIDE,
|
||||
target_addr: our_addr.address(),
|
||||
lladdr: Some(RawHardwareAddress::from(hardware_address)),
|
||||
};
|
||||
let icmp_resp = Icmpv6Repr::Ndisc(response);
|
||||
let mut buf = [0u8; 128];
|
||||
let mut icmp_pkt = Icmpv6Packet::new_unchecked(&mut buf);
|
||||
icmp_resp.emit(
|
||||
&our_addr.address(),
|
||||
&src_addr,
|
||||
&mut icmp_pkt,
|
||||
&ChecksumCapabilities::ignored(),
|
||||
);
|
||||
let resp_len = icmp_resp.buffer_len();
|
||||
|
||||
self.send_to(
|
||||
src_mac,
|
||||
resp_len,
|
||||
|out| out[..resp_len].copy_from_slice(&buf[..resp_len]),
|
||||
EthernetProtocol::Ipv6,
|
||||
);
|
||||
}
|
||||
NdiscRepr::NeighborAdvert { target_addr, lladdr, .. } => {
|
||||
let Some(mac) = lladdr.map(|a| EthernetAddress::from_bytes(a.as_bytes())) else {
|
||||
return;
|
||||
};
|
||||
self.neighbor_cache.insert(
|
||||
IpAddress::Ipv6(target_addr),
|
||||
Neighbor {
|
||||
hardware_address: mac,
|
||||
expires_at: now + Self::NEIGHBOR_LIVE_TIME,
|
||||
},
|
||||
);
|
||||
self.check_waiting_packets_v6(target_addr, mac);
|
||||
}
|
||||
NdiscRepr::RouterSolicit { lladdr } => {
|
||||
if let Some(ll) = lladdr {
|
||||
self.neighbor_cache.insert(
|
||||
IpAddress::Ipv6(ipv6_pkt.src_addr()),
|
||||
Neighbor {
|
||||
hardware_address: EthernetAddress::from_bytes(ll.as_bytes()),
|
||||
expires_at: now + Self::NEIGHBOR_LIVE_TIME,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
NdiscRepr::RouterAdvert {
|
||||
hop_limit,
|
||||
router_lifetime,
|
||||
reachable_time,
|
||||
retrans_time,
|
||||
lladdr,
|
||||
mtu,
|
||||
prefix_info,
|
||||
..
|
||||
} => {
|
||||
self.slaac_state = SlaacState::Configured;
|
||||
if let Some(ll) = lladdr {
|
||||
self.neighbor_cache.insert(
|
||||
IpAddress::Ipv6(ipv6_pkt.src_addr()),
|
||||
Neighbor {
|
||||
hardware_address: EthernetAddress::from_bytes(ll.as_bytes()),
|
||||
expires_at: now + Self::NEIGHBOR_LIVE_TIME,
|
||||
},
|
||||
);
|
||||
}
|
||||
if let Some(pi) = prefix_info {
|
||||
if pi.flags.contains(NdiscPrefixInfoFlags::ADDRCONF) {
|
||||
let valid_lft = pi.valid_lifetime;
|
||||
if valid_lft > Duration::ZERO {
|
||||
let slaac_addr = slaac::form_slaac_addr(
|
||||
Ipv6Cidr::new(pi.prefix, pi.prefix_len),
|
||||
hardware_address,
|
||||
);
|
||||
self.ip_address = Some(IpCidr::Ipv6(Ipv6Cidr::new(
|
||||
slaac_addr,
|
||||
pi.prefix_len,
|
||||
)));
|
||||
info!(
|
||||
"SLAAC: configured {} on {} (via RA from {})",
|
||||
slaac_addr,
|
||||
self.name,
|
||||
ipv6_pkt.src_addr()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for EthernetLink {
|
||||
fn send(&mut self, next_hop: IpAddress, packet: &[u8], now: Instant) {
|
||||
let local_broadcast = match self.ip_address.and_then(|cidr| cidr.broadcast()) {
|
||||
Some(addr) => IpAddress::Ipv4(addr) == next_hop,
|
||||
None => false,
|
||||
let eth_proto = if !packet.is_empty() && packet[0] >> 4 == 6 {
|
||||
EthernetProtocol::Ipv6
|
||||
} else {
|
||||
EthernetProtocol::Ipv4
|
||||
};
|
||||
|
||||
let owned = self.qdisc_config.enqueue_and_dequeue(packet.to_vec(), now);
|
||||
let Some(packet) = owned else {
|
||||
self.stats.tx_dropped += 1;
|
||||
return;
|
||||
};
|
||||
self.stats.tx_packets += 1;
|
||||
self.stats.tx_bytes += packet.len() as u64;
|
||||
|
||||
let local_broadcast = match self.ip_address {
|
||||
Some(IpCidr::Ipv4(cidr)) => cidr
|
||||
.broadcast()
|
||||
.map(|addr| IpAddress::Ipv4(addr) == next_hop)
|
||||
.unwrap_or(false),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if local_broadcast || next_hop.is_broadcast() {
|
||||
self.send_to(
|
||||
EthernetAddress::BROADCAST,
|
||||
packet.len(),
|
||||
|buf| buf.copy_from_slice(packet),
|
||||
EthernetProtocol::Ipv4,
|
||||
|buf| buf.copy_from_slice(&packet),
|
||||
eth_proto,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
match self.neighbor_cache.entry(next_hop) {
|
||||
Entry::Vacant(_) => self.handle_missing_neighbor(next_hop, packet, now),
|
||||
Entry::Vacant(_) => {
|
||||
self.arp_cache_misses += 1;
|
||||
self.handle_missing_neighbor(next_hop, &packet, now)
|
||||
}
|
||||
Entry::Occupied(e) => {
|
||||
if e.get().expires_at < now {
|
||||
e.remove();
|
||||
self.handle_missing_neighbor(next_hop, packet, now)
|
||||
self.arp_cache_misses += 1;
|
||||
self.handle_missing_neighbor(next_hop, &packet, now)
|
||||
} else {
|
||||
self.arp_cache_hits += 1;
|
||||
let mac = e.get().hardware_address;
|
||||
self.send_to(
|
||||
mac,
|
||||
packet.len(),
|
||||
|buf| buf.copy_from_slice(packet),
|
||||
EthernetProtocol::Ipv4,
|
||||
|buf| buf.copy_from_slice(&packet),
|
||||
eth_proto,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -352,33 +791,59 @@ impl LinkDevice for EthernetLink {
|
||||
if e.kind() != ErrorKind::WouldBlock {
|
||||
error!("Failed to read ethernet device on link {}", self.name);
|
||||
} else {
|
||||
// No packet to read but we check if we have arp to send
|
||||
// No packet to read but we check if we have neighbor discovery to send
|
||||
self.send_arp(now);
|
||||
self.send_ndp_solicit(now);
|
||||
self.send_router_solicitation(now);
|
||||
}
|
||||
self.input_buffer = input_buffer;
|
||||
return None;
|
||||
}
|
||||
let packet = EthernetFrame::new_unchecked(&input_buffer[..]);
|
||||
let Ok(repr) = EthernetRepr::parse(&packet) else {
|
||||
debug!("Dropped incomming frame on {} (Malformed)", self.name);
|
||||
continue;
|
||||
|
||||
let (is_for_us, ethertype) = {
|
||||
let packet = EthernetFrame::new_unchecked(&input_buffer[..]);
|
||||
let Ok(repr) = EthernetRepr::parse(&packet) else {
|
||||
debug!("Dropped incomming frame on {} (Malformed)", self.name);
|
||||
self.stats.rx_errors += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let is_for_us = repr.dst_addr.is_broadcast()
|
||||
|| repr.dst_addr == EMPTY_MAC
|
||||
|| repr.dst_addr == hardware_address;
|
||||
(is_for_us, repr.ethertype)
|
||||
};
|
||||
|
||||
// We let EMPTY_MAC pass because somehow this is the mac used when net=redir is used
|
||||
if !repr.dst_addr.is_broadcast()
|
||||
&& repr.dst_addr != EMPTY_MAC
|
||||
&& repr.dst_addr != hardware_address
|
||||
{
|
||||
// Drop packets which are not for us
|
||||
if !is_for_us && !self.promiscuous {
|
||||
continue;
|
||||
}
|
||||
|
||||
match repr.ethertype {
|
||||
EthernetProtocol::Ipv4 => {
|
||||
match ethertype {
|
||||
EthernetProtocol::Ipv4 | EthernetProtocol::Ipv6 => {
|
||||
let payload_start = EthernetFrame::<&[u8]>::header_len();
|
||||
let payload_len = input_buffer.len().saturating_sub(payload_start);
|
||||
let next_header_byte = if payload_len >= 8 {
|
||||
input_buffer[payload_start + 6]
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let is_ndp = ethertype == EthernetProtocol::Ipv6
|
||||
&& payload_len >= 8
|
||||
&& next_header_byte == u8::from(IpProtocol::Icmpv6);
|
||||
if is_ndp {
|
||||
let ndp_packet = input_buffer[payload_start..].to_vec();
|
||||
self.process_ndp(&ndp_packet, now);
|
||||
continue;
|
||||
}
|
||||
self.input_buffer = input_buffer;
|
||||
return Some(EthernetFrame::new_unchecked(&self.input_buffer[..]).payload());
|
||||
self.stats.rx_packets += 1;
|
||||
self.stats.rx_bytes += self.input_buffer[payload_start..].len() as u64;
|
||||
return Some(&self.input_buffer[payload_start..]);
|
||||
}
|
||||
EthernetProtocol::Arp => {
|
||||
let packet = EthernetFrame::new_unchecked(&input_buffer[..]);
|
||||
self.process_arp(packet.payload(), now);
|
||||
}
|
||||
EthernetProtocol::Arp => self.process_arp(packet.payload(), now),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
@@ -398,15 +863,135 @@ impl LinkDevice for EthernetLink {
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, addr: EthernetAddress) {
|
||||
self.hardware_address = Some(addr)
|
||||
self.hardware_address = Some(addr);
|
||||
if self.ip_address.is_none() {
|
||||
let link_local = slaac::form_link_local(addr);
|
||||
self.ip_address = Some(IpCidr::Ipv6(link_local));
|
||||
info!("SLAAC: configured link-local {} on {}", link_local.address(), self.name);
|
||||
}
|
||||
}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
Some(IpCidr::Ipv4(self.ip_address?))
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
let IpCidr::Ipv4(addr) = addr;
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
|
||||
fn arp_table(&self) -> String {
|
||||
let mut out = String::new();
|
||||
for (ip, neighbor) in &self.neighbor_cache {
|
||||
out.push_str(&format!("{} {}\n", ip, neighbor.hardware_address));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn flush_arp(&mut self) {
|
||||
self.neighbor_cache.clear();
|
||||
}
|
||||
|
||||
fn arp_stats(&self) -> String {
|
||||
format!(
|
||||
"requests={} replies={} hits={} misses={} entries={}\n",
|
||||
self.arp_requests,
|
||||
self.arp_replies,
|
||||
self.arp_cache_hits,
|
||||
self.arp_cache_misses,
|
||||
self.neighbor_cache.len(),
|
||||
)
|
||||
}
|
||||
|
||||
fn statistics(&self) -> Stats {
|
||||
self.stats
|
||||
}
|
||||
|
||||
fn link_state(&self) -> &'static str {
|
||||
if !self.enabled {
|
||||
"down"
|
||||
} else if self.hardware_address.is_some() {
|
||||
"up"
|
||||
} else {
|
||||
"down"
|
||||
}
|
||||
}
|
||||
|
||||
fn qdisc_info(&self) -> String {
|
||||
match &self.qdisc_config {
|
||||
QdiscConfig::None => "none\n".to_string(),
|
||||
QdiscConfig::TokenBucket(tb) => {
|
||||
format!("token_bucket rate={} burst={} tokens={}\n", tb.rate(), tb.burst(), tb.tokens())
|
||||
}
|
||||
QdiscConfig::PriorityQueue(pq) => {
|
||||
format!("priority_queue len={} max={}\n", pq.len(), pq.max_len())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_qdisc(&mut self, kind: &str) -> Result<(), String> {
|
||||
match kind.trim() {
|
||||
"none" => {
|
||||
self.qdisc_config = QdiscConfig::None;
|
||||
Ok(())
|
||||
}
|
||||
"token_bucket" | "tbf" => {
|
||||
self.qdisc_config = QdiscConfig::TokenBucket(
|
||||
crate::link::qdisc::TokenBucket::new(1_000_000, 1500)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
"priority_queue" | "pfifo_fast" => {
|
||||
self.qdisc_config = QdiscConfig::PriorityQueue(
|
||||
crate::link::qdisc::PriorityQueue::new(1000)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(format!("unknown qdisc type: {}\n", kind)),
|
||||
}
|
||||
}
|
||||
|
||||
fn mtu(&self) -> usize {
|
||||
self.mtu
|
||||
}
|
||||
|
||||
fn set_mtu(&mut self, mtu: usize) {
|
||||
self.mtu = mtu.clamp(576, 9000);
|
||||
}
|
||||
|
||||
fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
|
||||
fn add_static_neighbor(&mut self, ip: IpAddress, mac: [u8; 6]) -> Result<(), String> {
|
||||
let hw_addr = EthernetAddress(mac);
|
||||
if !hw_addr.is_unicast() {
|
||||
return Err(format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x} is not a unicast MAC\n",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]));
|
||||
}
|
||||
self.neighbor_cache.insert(
|
||||
ip,
|
||||
Neighbor {
|
||||
hardware_address: hw_addr,
|
||||
expires_at: Instant::from_millis(i64::MAX / 2),
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_neighbor(&mut self, ip: IpAddress) -> bool {
|
||||
self.neighbor_cache.remove(&ip).is_some()
|
||||
}
|
||||
|
||||
fn is_promiscuous(&self) -> bool {
|
||||
self.promiscuous
|
||||
}
|
||||
|
||||
fn set_promiscuous(&mut self, enabled: bool) {
|
||||
self.promiscuous = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
//! GRE Tunnel (Generic Routing Encapsulation) — mirrors Linux 7.1's
|
||||
//! `net/ipv4/ip_gre.c`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/ipv4/ip_gre.c:601` — `ipgre_tunnel_xmit()` — GRE header prepend
|
||||
//! - `net/ipv4/ip_gre.c:430` — `ipgre_rcv()` — GRE decapsulation
|
||||
//! - `include/uapi/linux/if_tunnel.h` — GRE header format
|
||||
//! - `include/net/gre.h` — GRE protocol constants
|
||||
//!
|
||||
//! GRE encapsulates an inner packet in an outer IP header + GRE header
|
||||
//! (4+ bytes) for tunneling through an IP network. Supports optional
|
||||
//! GRE key for multiplexing multiple tunnels over the same endpoints.
|
||||
//!
|
||||
//! GRE Header (minimum 4 bytes):
|
||||
//! [Flags 2B][Protocol 2B]
|
||||
//! Flags: C=checksum, K=key, S=sequence
|
||||
//! Protocol: 0x0800 = IPv4, 0x86DD = IPv6
|
||||
//!
|
||||
//! With GRE key (8 bytes):
|
||||
//! [Flags 2B][Protocol 2B][Key 4B]
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{
|
||||
EthernetAddress, IpAddress, IpCidr, Ipv4Address, Ipv4Packet, Ipv4Repr, IpProtocol,
|
||||
};
|
||||
|
||||
use super::LinkDevice;
|
||||
|
||||
const GRE_PROTO_IPV4: u16 = 0x0800;
|
||||
const GRE_FLAG_KEY: u16 = 0x2000;
|
||||
|
||||
pub struct GreDevice {
|
||||
name: Rc<str>,
|
||||
parent_name: Rc<str>,
|
||||
local_ip: Ipv4Address,
|
||||
remote_ip: Ipv4Address,
|
||||
gre_key: Option<u32>,
|
||||
gre_header: [u8; 8],
|
||||
gre_header_len: usize,
|
||||
send_buffer: Vec<u8>,
|
||||
recv_buffer: Vec<u8>,
|
||||
recv_queue: VecDeque<Vec<u8>>,
|
||||
ip_address: Option<IpCidr>,
|
||||
}
|
||||
|
||||
impl GreDevice {
|
||||
pub fn new(
|
||||
name: &str,
|
||||
parent_name: &str,
|
||||
local_ip: Ipv4Address,
|
||||
remote_ip: Ipv4Address,
|
||||
gre_key: Option<u32>,
|
||||
) -> Self {
|
||||
let mut header = [0u8; 8];
|
||||
let header_len = if let Some(key) = gre_key {
|
||||
header[0] = (GRE_FLAG_KEY >> 8) as u8;
|
||||
header[1] = (GRE_FLAG_KEY & 0xff) as u8;
|
||||
header[2] = (GRE_PROTO_IPV4 >> 8) as u8;
|
||||
header[3] = (GRE_PROTO_IPV4 & 0xff) as u8;
|
||||
header[4] = (key >> 24) as u8;
|
||||
header[5] = ((key >> 16) & 0xff) as u8;
|
||||
header[6] = ((key >> 8) & 0xff) as u8;
|
||||
header[7] = (key & 0xff) as u8;
|
||||
8
|
||||
} else {
|
||||
header[2] = (GRE_PROTO_IPV4 >> 8) as u8;
|
||||
header[3] = (GRE_PROTO_IPV4 & 0xff) as u8;
|
||||
4
|
||||
};
|
||||
|
||||
Self {
|
||||
name: name.into(),
|
||||
parent_name: parent_name.into(),
|
||||
local_ip,
|
||||
remote_ip,
|
||||
gre_key,
|
||||
gre_header: header,
|
||||
gre_header_len: header_len,
|
||||
send_buffer: Vec::with_capacity(1500),
|
||||
recv_buffer: Vec::with_capacity(1500),
|
||||
recv_queue: VecDeque::new(),
|
||||
ip_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_received(&mut self, packet: Vec<u8>) {
|
||||
self.recv_queue.push_back(packet);
|
||||
}
|
||||
|
||||
fn build_encapsulated(&mut self, inner_packet: &[u8]) -> &[u8] {
|
||||
self.send_buffer.clear();
|
||||
let outer_header = Ipv4Repr {
|
||||
src_addr: self.local_ip,
|
||||
dst_addr: self.remote_ip,
|
||||
next_header: IpProtocol::from(47u8),
|
||||
payload_len: self.gre_header_len + inner_packet.len(),
|
||||
hop_limit: 64,
|
||||
};
|
||||
let ip_hdr_len = outer_header.buffer_len();
|
||||
let total_len = ip_hdr_len + self.gre_header_len + inner_packet.len();
|
||||
self.send_buffer.resize(total_len, 0);
|
||||
let mut ip_pkt = Ipv4Packet::new_unchecked(&mut self.send_buffer);
|
||||
outer_header.emit(&mut ip_pkt, &smoltcp::phy::ChecksumCapabilities::ignored());
|
||||
let gre_start = ip_hdr_len;
|
||||
self.send_buffer[gre_start..gre_start + self.gre_header_len]
|
||||
.copy_from_slice(&self.gre_header[..self.gre_header_len]);
|
||||
self.send_buffer[gre_start + self.gre_header_len..]
|
||||
.copy_from_slice(inner_packet);
|
||||
&self.send_buffer
|
||||
}
|
||||
|
||||
fn matches_endpoint(&self, outer_packet: &[u8]) -> bool {
|
||||
if outer_packet.len() < 24 {
|
||||
return false;
|
||||
}
|
||||
let Ok(ipv4) = Ipv4Packet::new_checked(outer_packet) else {
|
||||
return false;
|
||||
};
|
||||
if u8::from(ipv4.next_header()) != 47 || ipv4.dst_addr() != self.local_ip {
|
||||
return false;
|
||||
}
|
||||
let ip_header_len = 20;
|
||||
let gre = &outer_packet[ip_header_len..];
|
||||
if gre.len() < 4 {
|
||||
return false;
|
||||
}
|
||||
let flags = u16::from_be_bytes([gre[0], gre[1]]);
|
||||
let proto = u16::from_be_bytes([gre[2], gre[3]]);
|
||||
if proto != GRE_PROTO_IPV4 {
|
||||
return false;
|
||||
}
|
||||
if let Some(expected_key) = self.gre_key {
|
||||
if flags & GRE_FLAG_KEY == 0 || gre.len() < 8 {
|
||||
return false;
|
||||
}
|
||||
let actual_key = u32::from_be_bytes([gre[4], gre[5], gre[6], gre[7]]);
|
||||
if actual_key != expected_key {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for GreDevice {
|
||||
fn send(&mut self, _next_hop: IpAddress, packet: &[u8], _now: Instant) {
|
||||
// No parent device reference — drop rather than self-loop.
|
||||
log::debug!("gre: dropping {} byte frame (no parent device)", packet.len());
|
||||
}
|
||||
|
||||
fn recv(&mut self, _now: Instant) -> Option<&[u8]> {
|
||||
let packet = self.recv_queue.pop_front()?;
|
||||
if !self.matches_endpoint(&packet) {
|
||||
return None;
|
||||
}
|
||||
let ip_header_len = 20;
|
||||
let gre_header_len = if self.gre_key.is_some() { 8 } else { 4 };
|
||||
let payload_start = ip_header_len + gre_header_len;
|
||||
let payload = &packet[payload_start..];
|
||||
self.recv_buffer.clear();
|
||||
self.recv_buffer.extend_from_slice(payload);
|
||||
Some(&self.recv_buffer)
|
||||
}
|
||||
|
||||
fn name(&self) -> &Rc<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn can_recv(&self) -> bool {
|
||||
!self.recv_queue.is_empty()
|
||||
}
|
||||
|
||||
fn mac_address(&self) -> Option<EthernetAddress> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, _addr: EthernetAddress) {}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
//! IPIP Tunnel (IP-in-IP) — mirrors Linux 7.1's `net/ipv4/ipip.c`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/ipv4/ipip.c:184` — `ipip_tunnel_xmit()` — build outer header, transmit
|
||||
//! - `net/ipv4/ip_tunnel.c:326` — `ip_tunnel_rcv()` — receive and decapsulate
|
||||
//! - `include/uapi/linux/in.h:30` — `IPPROTO_IPIP = 4`
|
||||
//!
|
||||
//! The IPIP tunnel wraps an inner IP packet in an outer IP header, routing
|
||||
//! it through a parent link-layer device to a remote endpoint. On reception,
|
||||
//! the outer header is stripped and the inner packet is delivered to the
|
||||
//! network stack.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{
|
||||
EthernetAddress, IpAddress, IpCidr, Ipv4Address, Ipv4Packet, Ipv4Repr, IpProtocol,
|
||||
};
|
||||
|
||||
use super::LinkDevice;
|
||||
|
||||
const IPIP_PROTO: u8 = 4;
|
||||
|
||||
pub struct IpipDevice {
|
||||
name: Rc<str>,
|
||||
parent_name: Rc<str>,
|
||||
local_ip: Ipv4Address,
|
||||
remote_ip: Ipv4Address,
|
||||
send_buffer: Vec<u8>,
|
||||
recv_buffer: Vec<u8>,
|
||||
recv_queue: VecDeque<Vec<u8>>,
|
||||
ip_address: Option<IpCidr>,
|
||||
}
|
||||
|
||||
impl IpipDevice {
|
||||
pub fn new(name: &str, parent_name: &str, local_ip: Ipv4Address, remote_ip: Ipv4Address) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
parent_name: parent_name.into(),
|
||||
local_ip,
|
||||
remote_ip,
|
||||
send_buffer: Vec::with_capacity(1500),
|
||||
recv_buffer: Vec::with_capacity(1500),
|
||||
recv_queue: VecDeque::new(),
|
||||
ip_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_received(&mut self, packet: Vec<u8>) {
|
||||
self.recv_queue.push_back(packet);
|
||||
}
|
||||
|
||||
fn build_outer_header(&mut self, inner_packet: &[u8]) -> &[u8] {
|
||||
self.send_buffer.clear();
|
||||
let outer_header = Ipv4Repr {
|
||||
src_addr: self.local_ip,
|
||||
dst_addr: self.remote_ip,
|
||||
next_header: IpProtocol::from(IPIP_PROTO),
|
||||
payload_len: inner_packet.len(),
|
||||
hop_limit: 64,
|
||||
};
|
||||
let header_len = outer_header.buffer_len();
|
||||
self.send_buffer.resize(header_len + inner_packet.len(), 0);
|
||||
let mut ip_pkt = Ipv4Packet::new_unchecked(&mut self.send_buffer);
|
||||
outer_header.emit(&mut ip_pkt, &smoltcp::phy::ChecksumCapabilities::ignored());
|
||||
self.send_buffer[header_len..].copy_from_slice(inner_packet);
|
||||
&self.send_buffer
|
||||
}
|
||||
|
||||
fn matches_endpoint(&self, outer_packet: &[u8]) -> bool {
|
||||
if outer_packet.len() < 20 {
|
||||
return false;
|
||||
}
|
||||
let Ok(ipv4) = Ipv4Packet::new_checked(outer_packet) else {
|
||||
return false;
|
||||
};
|
||||
u8::from(ipv4.next_header()) == IPIP_PROTO && ipv4.dst_addr() == self.local_ip
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for IpipDevice {
|
||||
fn send(&mut self, _next_hop: IpAddress, packet: &[u8], _now: Instant) {
|
||||
// No parent device reference — drop rather than self-loop.
|
||||
log::debug!("ipip: dropping {} byte frame (no parent device)", packet.len());
|
||||
}
|
||||
|
||||
fn recv(&mut self, _now: Instant) -> Option<&[u8]> {
|
||||
let packet = self.recv_queue.pop_front()?;
|
||||
if !self.matches_endpoint(&packet) {
|
||||
return None;
|
||||
}
|
||||
let Ok(ipv4) = Ipv4Packet::new_checked(&packet) else {
|
||||
return None;
|
||||
};
|
||||
let header_len = 20;
|
||||
let payload = &packet[header_len..];
|
||||
self.recv_buffer.clear();
|
||||
self.recv_buffer.extend_from_slice(payload);
|
||||
Some(&self.recv_buffer)
|
||||
}
|
||||
|
||||
fn name(&self) -> &Rc<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn can_recv(&self) -> bool {
|
||||
!self.recv_queue.is_empty()
|
||||
}
|
||||
|
||||
fn mac_address(&self) -> Option<EthernetAddress> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, _addr: EthernetAddress) {}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,5 @@ impl LinkDevice for LoopbackDevice {
|
||||
Some("127.0.0.1/8".parse().unwrap())
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, _addr: smoltcp::wire::IpCidr) {
|
||||
todo!()
|
||||
}
|
||||
fn set_ip_address(&mut self, _addr: smoltcp::wire::IpCidr) {}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
pub mod bond;
|
||||
pub mod bridge;
|
||||
pub mod ethernet;
|
||||
pub mod gre;
|
||||
pub mod ipip;
|
||||
pub mod loopback;
|
||||
pub mod qdisc;
|
||||
pub mod stp;
|
||||
pub mod tun;
|
||||
pub mod vlan;
|
||||
pub mod vxlan;
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -29,6 +38,70 @@ pub trait LinkDevice {
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr>;
|
||||
fn set_ip_address(&mut self, addr: IpCidr);
|
||||
|
||||
fn arp_table(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn flush_arp(&mut self) {}
|
||||
|
||||
fn arp_stats(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn qdisc_info(&self) -> String {
|
||||
"none\n".to_string()
|
||||
}
|
||||
|
||||
fn set_qdisc(&mut self, _kind: &str) -> Result<(), String> {
|
||||
Err("qdisc configuration not supported on this device\n".to_string())
|
||||
}
|
||||
|
||||
fn add_static_neighbor(&mut self, _ip: IpAddress, _mac: [u8; 6]) -> Result<(), String> {
|
||||
Err("static ARP entry not supported on this device\n".to_string())
|
||||
}
|
||||
|
||||
fn remove_neighbor(&mut self, _ip: IpAddress) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_promiscuous(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_promiscuous(&mut self, _enabled: bool) {}
|
||||
|
||||
fn mtu(&self) -> usize {
|
||||
1500
|
||||
}
|
||||
|
||||
fn set_mtu(&mut self, _mtu: usize) {}
|
||||
|
||||
fn is_enabled(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, _enabled: bool) {}
|
||||
|
||||
fn link_state(&self) -> &'static str {
|
||||
"unknown"
|
||||
}
|
||||
|
||||
fn statistics(&self) -> Stats {
|
||||
Stats::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct Stats {
|
||||
pub rx_bytes: u64,
|
||||
pub rx_packets: u64,
|
||||
pub tx_bytes: u64,
|
||||
pub tx_packets: u64,
|
||||
pub rx_errors: u64,
|
||||
pub tx_errors: u64,
|
||||
pub rx_dropped: u64,
|
||||
pub tx_dropped: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
//! Traffic Control (qdisc) — mirrors Linux 7.1's `net/sched/`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/sched/sch_tbf.c` — Token Bucket Filter (`tbf_enqueue`, `tbf_dequeue`)
|
||||
//! - `net/sched/sch_prio.c` — Priority queue (`prio_enqueue`, `prio_dequeue`)
|
||||
//! - `net/sched/sch_api.c` — Qdisc registration and API
|
||||
//!
|
||||
//! Two qdiscs are implemented:
|
||||
//! - **TokenBucket**: rate-limits outgoing packets using a token bucket.
|
||||
//! Packets exceeding the rate are dropped. Mirrors `TBF`.
|
||||
//! - **PriorityQueue**: three FIFO bands prioritized by IP TOS field.
|
||||
//! Mirrors `pfifo_fast` (the default Linux qdisc).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use smoltcp::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TokenBucket {
|
||||
rate: u64,
|
||||
burst: u64,
|
||||
tokens: u64,
|
||||
last_update: Instant,
|
||||
}
|
||||
|
||||
impl TokenBucket {
|
||||
pub fn new(rate_bps: u64, burst_bytes: u64) -> Self {
|
||||
Self { rate: rate_bps, burst: burst_bytes.max(1500), tokens: burst_bytes.max(1500), last_update: Instant::from_millis(0) }
|
||||
}
|
||||
pub fn rate(&self) -> u64 { self.rate }
|
||||
pub fn burst(&self) -> u64 { self.burst }
|
||||
pub fn tokens(&self) -> u64 { self.tokens }
|
||||
|
||||
pub fn set_rate(&mut self, rate_bps: u64) {
|
||||
self.rate = rate_bps;
|
||||
}
|
||||
|
||||
pub fn set_burst(&mut self, burst_bytes: u64) {
|
||||
self.burst = burst_bytes.max(1500);
|
||||
if self.tokens > self.burst {
|
||||
self.tokens = self.burst;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume(&mut self, bytes: u64, now: Instant) -> bool {
|
||||
let elapsed = now - self.last_update;
|
||||
if elapsed > Duration::ZERO {
|
||||
// Saturating mul: rate (bytes/s) * elapsed (ms) / 8000.
|
||||
// saturating_mul prevents overflow on long elapsed durations.
|
||||
let token_add = self.rate
|
||||
.saturating_mul(elapsed.total_millis() as u64)
|
||||
/ 8000;
|
||||
self.tokens = (self.tokens + token_add).min(self.burst);
|
||||
self.last_update = now;
|
||||
}
|
||||
if self.tokens >= bytes {
|
||||
self.tokens -= bytes;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PriorityQueue {
|
||||
bands: [VecDeque<Vec<u8>>; 3],
|
||||
max_len: usize,
|
||||
}
|
||||
|
||||
impl PriorityQueue {
|
||||
pub fn new(max_len: usize) -> Self { Self { bands: [VecDeque::new(), VecDeque::new(), VecDeque::new()], max_len } }
|
||||
pub fn max_len(&self) -> usize { self.max_len }
|
||||
|
||||
fn classify(packet: &[u8]) -> usize {
|
||||
if packet.len() < 2 || packet[0] >> 4 != 4 {
|
||||
return 1;
|
||||
}
|
||||
let tos = if packet.len() > 1 { packet[1] } else { 0 };
|
||||
let precedence = tos >> 5;
|
||||
if precedence >= 6 {
|
||||
0
|
||||
} else if precedence >= 4 {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enqueue(&mut self, packet: Vec<u8>) -> bool {
|
||||
let band = Self::classify(&packet);
|
||||
if self.bands[band].len() >= self.max_len {
|
||||
return false;
|
||||
}
|
||||
self.bands[band].push_back(packet);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn dequeue(&mut self) -> Option<Vec<u8>> {
|
||||
for band in 0..3 {
|
||||
if let Some(packet) = self.bands[band].pop_front() {
|
||||
return Some(packet);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.bands[0].len() + self.bands[1].len() + self.bands[2].len()
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) -> Vec<Vec<u8>> {
|
||||
let mut result = Vec::new();
|
||||
while let Some(pkt) = self.dequeue() {
|
||||
result.push(pkt);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum QdiscConfig {
|
||||
None,
|
||||
TokenBucket(TokenBucket),
|
||||
PriorityQueue(PriorityQueue),
|
||||
}
|
||||
|
||||
impl Default for QdiscConfig {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl QdiscConfig {
|
||||
pub fn enqueue_and_dequeue(
|
||||
&mut self,
|
||||
packet: Vec<u8>,
|
||||
now: Instant,
|
||||
) -> Option<Vec<u8>> {
|
||||
match self {
|
||||
QdiscConfig::None => Some(packet),
|
||||
QdiscConfig::TokenBucket(tb) => {
|
||||
if tb.consume(packet.len() as u64, now) {
|
||||
Some(packet)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
QdiscConfig::PriorityQueue(pq) => {
|
||||
pq.enqueue(packet);
|
||||
pq.dequeue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
//! Spanning Tree Protocol (IEEE 802.1D) — mirrors Linux 7.1's `net/bridge/`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/bridge/br_stp.c` — STP state machine (`br_set_state`, `br_become_root_bridge`)
|
||||
//! - `net/bridge/br_stp_bpdu.c` — BPDU handling (`br_stp_rcv`, `br_send_config_bpdu`)
|
||||
//! - `net/bridge/br_stp_timer.c` — timers (`br_hello_timer_expired`, `br_tcn_timer_expired`)
|
||||
//! - `net/bridge/br_private_stp.h` — port roles/states
|
||||
//!
|
||||
//! Prevents Ethernet loops by selectively blocking ports. Uses BPDU messages
|
||||
//! (multicast to 01:80:c2:00:00:00) to elect a root bridge and compute the
|
||||
//! spanning tree. Ports are either Forwarding or Blocking (simplified from the
|
||||
//! full 802.1D state machine: Blocking→Listening→Learning→Forwarding).
|
||||
|
||||
use smoltcp::time::{Duration, Instant};
|
||||
use smoltcp::wire::EthernetAddress;
|
||||
|
||||
pub const BPDU_MAC: EthernetAddress = EthernetAddress([0x01, 0x80, 0xc2, 0x00, 0x00, 0x00]);
|
||||
pub const DEFAULT_PRIORITY: u16 = 32768;
|
||||
pub const DEFAULT_HELLO: Duration = Duration::from_secs(2);
|
||||
pub const DEFAULT_MAX_AGE: Duration = Duration::from_secs(20);
|
||||
pub const DEFAULT_FORWARD_DELAY: Duration = Duration::from_secs(15);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PortState {
|
||||
Blocking,
|
||||
Forwarding,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StpState {
|
||||
pub bridge_priority: u16,
|
||||
pub bridge_mac: EthernetAddress,
|
||||
pub root_id: u64,
|
||||
pub root_path_cost: u32,
|
||||
pub root_port: Option<usize>,
|
||||
pub hello_timer: Instant,
|
||||
pub port_states: Vec<PortState>,
|
||||
}
|
||||
|
||||
impl StpState {
|
||||
pub fn new(priority: u16, mac: EthernetAddress, port_count: usize) -> Self {
|
||||
let root_id = ((priority as u64) << 48) | mac_to_u64(mac);
|
||||
Self {
|
||||
bridge_priority: priority,
|
||||
bridge_mac: mac,
|
||||
root_id,
|
||||
root_path_cost: 0,
|
||||
root_port: None,
|
||||
hello_timer: Instant::from_millis(0),
|
||||
port_states: vec![PortState::Forwarding; port_count],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bridge_id(&self) -> u64 {
|
||||
((self.bridge_priority as u64) << 48) | mac_to_u64(self.bridge_mac)
|
||||
}
|
||||
|
||||
pub fn send_hello(&mut self, now: Instant) -> bool {
|
||||
if now < self.hello_timer + DEFAULT_HELLO {
|
||||
return false;
|
||||
}
|
||||
self.hello_timer = now;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn process_bpdu(
|
||||
&mut self,
|
||||
port_idx: usize,
|
||||
data: &[u8],
|
||||
now: Instant,
|
||||
) -> Option<Vec<u8>> {
|
||||
let bpdu = BpduMessage::parse(data)?;
|
||||
|
||||
if bpdu.root_id < self.root_id {
|
||||
self.root_id = bpdu.root_id;
|
||||
self.root_path_cost = bpdu.root_path_cost + self.port_cost();
|
||||
self.root_port = Some(port_idx);
|
||||
return Some(self.build_bpdu());
|
||||
}
|
||||
|
||||
if bpdu.root_id == self.root_id {
|
||||
if port_idx == self.root_port.unwrap_or(usize::MAX) {
|
||||
self.root_path_cost = bpdu.root_path_cost + self.port_cost();
|
||||
} else if bpdu.root_path_cost < self.root_path_cost {
|
||||
self.port_states[port_idx] = PortState::Blocking;
|
||||
}
|
||||
}
|
||||
|
||||
if self.bridge_id() < bpdu.bridge_id && bpdu.root_id == self.root_id {
|
||||
self.root_id = self.bridge_id();
|
||||
self.root_path_cost = 0;
|
||||
self.root_port = None;
|
||||
for state in &mut self.port_states {
|
||||
*state = PortState::Forwarding;
|
||||
}
|
||||
return Some(self.build_bpdu());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn is_blocked(&self, port_idx: usize) -> bool {
|
||||
port_idx < self.port_states.len() && self.port_states[port_idx] == PortState::Blocking
|
||||
}
|
||||
|
||||
fn port_cost(&self) -> u32 {
|
||||
4
|
||||
}
|
||||
|
||||
pub fn build_bpdu(&self) -> Vec<u8> {
|
||||
let mut buf = vec![0u8; 35];
|
||||
buf[0..2].copy_from_slice(&[0x00, 0x00]);
|
||||
buf[2] = 0x00;
|
||||
buf[3] = 0x00;
|
||||
buf[4] = 0x00;
|
||||
buf[5..13].copy_from_slice(&self.root_id.to_be_bytes());
|
||||
buf[13..17].copy_from_slice(&self.root_path_cost.to_be_bytes());
|
||||
buf[17..25].copy_from_slice(&self.bridge_id().to_be_bytes());
|
||||
buf[25..27].copy_from_slice(&0u16.to_be_bytes());
|
||||
buf[27..29].copy_from_slice(&0u16.to_be_bytes());
|
||||
// IEEE 802.1D timer fields are in units of 1/256 second.
|
||||
// 1 second = 256 ticks. So seconds * 256 = ticks.
|
||||
let to_ticks = |d: Duration| -> u16 {
|
||||
// total_millis returns i64; convert to u16 ticks (1s = 256 ticks).
|
||||
// Clamp to u16::MAX to avoid overflow.
|
||||
((d.total_millis() as u64).wrapping_mul(256).wrapping_div(1000) as u16)
|
||||
};
|
||||
buf[29..31].copy_from_slice(&to_ticks(DEFAULT_MAX_AGE).to_be_bytes());
|
||||
buf[31..33].copy_from_slice(&to_ticks(DEFAULT_HELLO).to_be_bytes());
|
||||
buf[33..35].copy_from_slice(&to_ticks(DEFAULT_FORWARD_DELAY).to_be_bytes());
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
struct BpduMessage {
|
||||
root_id: u64,
|
||||
root_path_cost: u32,
|
||||
bridge_id: u64,
|
||||
}
|
||||
|
||||
impl BpduMessage {
|
||||
fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < 35 || data[0..2] != [0x00, 0x00] || data[2] != 0x00 {
|
||||
return None;
|
||||
}
|
||||
let root_id = u64::from_be_bytes(data[5..13].try_into().ok()?);
|
||||
let root_path_cost = u32::from_be_bytes(data[13..17].try_into().ok()?);
|
||||
let bridge_id = u64::from_be_bytes(data[17..25].try_into().ok()?);
|
||||
Some(Self { root_id, root_path_cost, bridge_id })
|
||||
}
|
||||
}
|
||||
|
||||
fn mac_to_u64(mac: EthernetAddress) -> u64 {
|
||||
let b = mac.as_bytes();
|
||||
((b[0] as u64) << 40) | ((b[1] as u64) << 32) | ((b[2] as u64) << 24)
|
||||
| ((b[3] as u64) << 16) | ((b[4] as u64) << 8) | (b[5] as u64)
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_minimal_bpdu() -> Vec<u8> {
|
||||
// Minimal config BPDU: protocol=0x0000, version=0, type=0, flags=0,
|
||||
// root_id(8) + cost(4) + bridge_id(8) + port(2) + age(2) + max_age(2) +
|
||||
// hello(2) + fwd(2) = 35 bytes.
|
||||
let mut p = vec![0u8; 35];
|
||||
p[0] = 0x00; p[1] = 0x00; // protocol id
|
||||
p[2] = 0x00; // version
|
||||
p[3] = 0x00; // type = config
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bpdu_minimal_parses() {
|
||||
let p = make_minimal_bpdu();
|
||||
let m = BpduMessage::parse(&p);
|
||||
assert!(m.is_some(), "Valid config BPDU should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bpdu_short_returns_none() {
|
||||
let p = vec![0u8; 10];
|
||||
assert!(BpduMessage::parse(&p).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bpdu_wrong_protocol_returns_none() {
|
||||
let mut p = make_minimal_bpdu();
|
||||
p[0] = 0x80;
|
||||
assert!(BpduMessage::parse(&p).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_bpdu_does_not_panic() {
|
||||
// Regression test: build_bpdu used to panic because
|
||||
// Duration::total_millis() returns i64 (8 bytes) but the
|
||||
// destination buffer was only 2 bytes.
|
||||
let stp = StpState::new(32768, EthernetAddress([0,0,0,0,0,0]), 1);
|
||||
let buf = stp.build_bpdu();
|
||||
// Must produce 35 bytes
|
||||
assert_eq!(buf.len(), 35);
|
||||
// And must be a valid BPDU
|
||||
assert_eq!(&buf[0..2], &[0x00, 0x00]);
|
||||
assert_eq!(buf[2], 0x00); // version
|
||||
assert_eq!(buf[3], 0x00); // type
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//! TUN virtual network device — mirrors Linux 7.1's `drivers/net/tun.c`.
|
||||
//!
|
||||
//! The TUN device operates at layer 3 (IP), providing a virtual network
|
||||
//! interface that exchanges raw IP packets with a userspace program via
|
||||
//! the `scheme:tun` interface. This enables VPN software, custom routing
|
||||
//! daemons, and container networking.
|
||||
//!
|
||||
//! Linux reference: `tun_net_xmit()` (kernel→userspace) and
|
||||
//! `tun_get_user()` (userspace→kernel) in `drivers/net/tun.c`.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr};
|
||||
|
||||
use super::LinkDevice;
|
||||
use super::Stats;
|
||||
|
||||
pub type PacketQueue = Rc<RefCell<VecDeque<Vec<u8>>>>;
|
||||
|
||||
pub struct TunDevice {
|
||||
name: Rc<str>,
|
||||
rx_queue: PacketQueue,
|
||||
tx_queue: PacketQueue,
|
||||
recv_buffer: Vec<u8>,
|
||||
ip_address: Option<IpCidr>,
|
||||
stats: Stats,
|
||||
}
|
||||
|
||||
impl TunDevice {
|
||||
pub fn new(name: &str, rx: PacketQueue, tx: PacketQueue) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
rx_queue: rx,
|
||||
tx_queue: tx,
|
||||
recv_buffer: Vec::new(),
|
||||
ip_address: None,
|
||||
stats: Stats::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for TunDevice {
|
||||
fn send(&mut self, _next_hop: IpAddress, packet: &[u8], _now: Instant) {
|
||||
self.stats.tx_bytes += packet.len() as u64;
|
||||
self.stats.tx_packets += 1;
|
||||
self.tx_queue.borrow_mut().push_back(packet.to_vec());
|
||||
}
|
||||
|
||||
fn recv(&mut self, _now: Instant) -> Option<&[u8]> {
|
||||
self.recv_buffer = self.rx_queue.borrow_mut().pop_front()?;
|
||||
self.stats.rx_bytes += self.recv_buffer.len() as u64;
|
||||
self.stats.rx_packets += 1;
|
||||
Some(&self.recv_buffer)
|
||||
}
|
||||
|
||||
fn name(&self) -> &Rc<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn can_recv(&self) -> bool {
|
||||
!self.rx_queue.borrow().is_empty()
|
||||
}
|
||||
|
||||
fn mac_address(&self) -> Option<EthernetAddress> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, _addr: EthernetAddress) {}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
|
||||
fn statistics(&self) -> Stats {
|
||||
self.stats
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
//! 802.1Q VLAN device — mirrors Linux 7.1's `net/8021q/`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/8021q/vlan_dev.c` — `vlan_dev_hard_start_xmit()` (send path)
|
||||
//! - `include/linux/if_vlan.h:411` — `__vlan_insert_tag()` (tag insertion)
|
||||
//! - `include/uapi/linux/if_ether.h:44` — `ETH_P_8021Q = 0x8100`
|
||||
//!
|
||||
//! The VLAN device wraps a parent link-layer device and inserts/strips
|
||||
//! the 4-byte 802.1Q tag (TPID 0x8100 + TCI) before/after the Ethernet
|
||||
//! header on each frame.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{
|
||||
EthernetAddress, EthernetFrame, EthernetProtocol, IpAddress, IpCidr,
|
||||
};
|
||||
|
||||
use super::LinkDevice;
|
||||
|
||||
const TPID_8021Q: u16 = 0x8100;
|
||||
|
||||
pub struct VlanDevice {
|
||||
name: Rc<str>,
|
||||
parent_name: Rc<str>,
|
||||
vlan_id: u16,
|
||||
priority: u8,
|
||||
tag: [u8; 4],
|
||||
send_buffer: Vec<u8>,
|
||||
recv_buffer: Vec<u8>,
|
||||
recv_queue: VecDeque<Vec<u8>>,
|
||||
mac_address: Option<EthernetAddress>,
|
||||
ip_address: Option<IpCidr>,
|
||||
}
|
||||
|
||||
impl VlanDevice {
|
||||
pub fn new(name: &str, parent_name: &str, vlan_id: u16) -> Self {
|
||||
let tci: u16 = vlan_id & 0x0fff;
|
||||
let tag: [u8; 4] = [
|
||||
(TPID_8021Q >> 8) as u8,
|
||||
(TPID_8021Q & 0xff) as u8,
|
||||
(tci >> 8) as u8,
|
||||
(tci & 0xff) as u8,
|
||||
];
|
||||
Self {
|
||||
name: name.into(),
|
||||
parent_name: parent_name.into(),
|
||||
vlan_id,
|
||||
priority: 0,
|
||||
tag,
|
||||
send_buffer: Vec::with_capacity(1522),
|
||||
recv_buffer: Vec::with_capacity(1522),
|
||||
recv_queue: VecDeque::new(),
|
||||
mac_address: None,
|
||||
ip_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vlan_id(&self) -> u16 {
|
||||
self.vlan_id
|
||||
}
|
||||
|
||||
pub fn parent_name(&self) -> &str {
|
||||
&self.parent_name
|
||||
}
|
||||
|
||||
pub fn set_priority(&mut self, pcp: u8) {
|
||||
self.priority = pcp & 0x07;
|
||||
let tci: u16 = ((self.priority as u16) << 13) | (self.vlan_id & 0x0fff);
|
||||
self.tag[2] = (tci >> 8) as u8;
|
||||
self.tag[3] = (tci & 0xff) as u8;
|
||||
}
|
||||
|
||||
pub fn push_received(&mut self, packet: Vec<u8>) {
|
||||
self.recv_queue.push_back(packet);
|
||||
}
|
||||
|
||||
fn insert_tag(&mut self, packet: &[u8]) -> &[u8] {
|
||||
self.send_buffer.clear();
|
||||
if packet.len() < 14 {
|
||||
self.send_buffer.extend_from_slice(packet);
|
||||
return &self.send_buffer;
|
||||
}
|
||||
self.send_buffer.extend_from_slice(&packet[..12]);
|
||||
self.send_buffer.extend_from_slice(&self.tag);
|
||||
self.send_buffer.extend_from_slice(&packet[12..]);
|
||||
&self.send_buffer
|
||||
}
|
||||
|
||||
fn strip_tag(&mut self, packet: &[u8]) -> Option<&[u8]> {
|
||||
if packet.len() < 18 {
|
||||
return None;
|
||||
}
|
||||
let tpid = u16::from_be_bytes([packet[12], packet[13]]);
|
||||
if tpid != TPID_8021Q {
|
||||
return None;
|
||||
}
|
||||
self.recv_buffer.clear();
|
||||
self.recv_buffer.extend_from_slice(&packet[..12]);
|
||||
self.recv_buffer.extend_from_slice(&packet[16..]);
|
||||
Some(&self.recv_buffer)
|
||||
}
|
||||
|
||||
pub fn matches_tag(&self, packet: &[u8]) -> bool {
|
||||
if packet.len() < 18 {
|
||||
return false;
|
||||
}
|
||||
let tpid = u16::from_be_bytes([packet[12], packet[13]]);
|
||||
if tpid != TPID_8021Q {
|
||||
return false;
|
||||
}
|
||||
let tci = u16::from_be_bytes([packet[14], packet[15]]);
|
||||
let vid = tci & 0x0fff;
|
||||
vid == self.vlan_id
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for VlanDevice {
|
||||
fn send(&mut self, next_hop: IpAddress, packet: &[u8], _now: Instant) {
|
||||
// The VLAN device has no parent device reference, so we cannot
|
||||
// send the tagged frame out. Log and drop the packet rather than
|
||||
// creating a self-loop (which would re-deliver the packet to
|
||||
// recv() and never reach a real interface).
|
||||
log::debug!("vlan: dropping {} byte frame (no parent device)", packet.len());
|
||||
let _ = next_hop;
|
||||
}
|
||||
|
||||
fn recv(&mut self, _now: Instant) -> Option<&[u8]> {
|
||||
let packet = self.recv_queue.pop_front()?;
|
||||
if packet.len() < 18 {
|
||||
return None;
|
||||
}
|
||||
let tpid = u16::from_be_bytes([packet[12], packet[13]]);
|
||||
if tpid != TPID_8021Q {
|
||||
return None;
|
||||
}
|
||||
self.recv_buffer.clear();
|
||||
self.recv_buffer.extend_from_slice(&packet[..12]);
|
||||
self.recv_buffer.extend_from_slice(&packet[16..]);
|
||||
Some(&self.recv_buffer)
|
||||
}
|
||||
|
||||
fn name(&self) -> &Rc<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn can_recv(&self) -> bool {
|
||||
!self.recv_queue.is_empty()
|
||||
}
|
||||
|
||||
fn mac_address(&self) -> Option<EthernetAddress> {
|
||||
self.mac_address
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, addr: EthernetAddress) {
|
||||
self.mac_address = Some(addr);
|
||||
}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
//! VXLAN (Virtual eXtensible LAN) — mirrors Linux 7.1's `drivers/net/vxlan/`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `drivers/net/vxlan/vxlan_core.c:1855` — `vxlan_xmit()` — encapsulation
|
||||
//! - `drivers/net/vxlan/vxlan_core.c:1366` — `vxlan_rcv()` — decapsulation
|
||||
//! - `include/net/vxlan.h` — VXLAN header format, VNI, default port 4789
|
||||
//!
|
||||
//! VXLAN encapsulates L2 Ethernet frames in UDP for overlay networking.
|
||||
//! Each overlay network is identified by a 24-bit VNI (Virtual Network
|
||||
//! Identifier). The default UDP destination port is 4789 (IANA-assigned).
|
||||
//!
|
||||
//! Packet structure:
|
||||
//! [Outer Eth][Outer IP][UDP:4789][VXLAN 8B][Inner Eth][Inner IP][Payload]
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{
|
||||
EthernetAddress, EthernetFrame, EthernetProtocol, EthernetRepr, IpAddress, IpCidr,
|
||||
Ipv4Address, Ipv4Packet, Ipv4Repr, IpProtocol, UdpPacket, UdpRepr,
|
||||
};
|
||||
|
||||
use super::LinkDevice;
|
||||
|
||||
const VXLAN_PORT: u16 = 4789;
|
||||
const VXLAN_FLAGS: u8 = 0x08;
|
||||
|
||||
pub struct VxlanDevice {
|
||||
name: Rc<str>,
|
||||
parent_name: Rc<str>,
|
||||
local_ip: Ipv4Address,
|
||||
remote_ip: Ipv4Address,
|
||||
vni: u32,
|
||||
vxlan_header: [u8; 8],
|
||||
send_buffer: Vec<u8>,
|
||||
recv_buffer: Vec<u8>,
|
||||
recv_queue: VecDeque<Vec<u8>>,
|
||||
virtual_mac: EthernetAddress,
|
||||
ip_address: Option<IpCidr>,
|
||||
}
|
||||
|
||||
impl VxlanDevice {
|
||||
pub fn new(
|
||||
name: &str,
|
||||
parent_name: &str,
|
||||
local_ip: Ipv4Address,
|
||||
remote_ip: Ipv4Address,
|
||||
vni: u32,
|
||||
) -> Self {
|
||||
let vni_bytes = vni.to_be_bytes();
|
||||
Self {
|
||||
name: name.into(),
|
||||
parent_name: parent_name.into(),
|
||||
local_ip,
|
||||
remote_ip,
|
||||
vni: vni & 0x00ffffff,
|
||||
vxlan_header: [
|
||||
VXLAN_FLAGS, 0, 0, 0,
|
||||
vni_bytes[1], vni_bytes[2], vni_bytes[3], 0,
|
||||
],
|
||||
send_buffer: Vec::with_capacity(1550),
|
||||
recv_buffer: Vec::with_capacity(1550),
|
||||
recv_queue: VecDeque::new(),
|
||||
virtual_mac: EthernetAddress([0x00, 0x00, 0x5e, 0x00, 0x01, 0x01]),
|
||||
ip_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_received(&mut self, packet: Vec<u8>) {
|
||||
self.recv_queue.push_back(packet);
|
||||
}
|
||||
|
||||
fn build_encapsulated(&mut self, inner_packet: &[u8]) -> &[u8] {
|
||||
self.send_buffer.clear();
|
||||
let inner_eth = {
|
||||
let mut buf = [0u8; 14];
|
||||
buf[..6].copy_from_slice(&EthernetAddress::BROADCAST.0);
|
||||
buf[6..12].copy_from_slice(&self.virtual_mac.0);
|
||||
let ethtype = if !inner_packet.is_empty() && inner_packet[0] >> 4 == 6 {
|
||||
EthernetProtocol::Ipv6
|
||||
} else {
|
||||
EthernetProtocol::Ipv4
|
||||
};
|
||||
let proto_bytes: [u8; 2] = match ethtype {
|
||||
EthernetProtocol::Ipv4 => [0x08, 0x00],
|
||||
EthernetProtocol::Ipv6 => [0x86, 0xDD],
|
||||
_ => [0x08, 0x00],
|
||||
};
|
||||
buf[12..14].copy_from_slice(&proto_bytes);
|
||||
buf
|
||||
};
|
||||
let inner_frame_len = inner_eth.len() + inner_packet.len();
|
||||
|
||||
let udp_repr = UdpRepr {
|
||||
src_port: VXLAN_PORT,
|
||||
dst_port: VXLAN_PORT,
|
||||
};
|
||||
let udp_payload_len = 8 + inner_frame_len;
|
||||
let outer_ip_repr = Ipv4Repr {
|
||||
src_addr: self.local_ip,
|
||||
dst_addr: self.remote_ip,
|
||||
next_header: IpProtocol::Udp,
|
||||
payload_len: 8 + udp_payload_len,
|
||||
hop_limit: 64,
|
||||
};
|
||||
|
||||
let total_len = outer_ip_repr.buffer_len() + 8 + 8 + inner_frame_len;
|
||||
self.send_buffer.resize(total_len, 0);
|
||||
let mut ip = Ipv4Packet::new_unchecked(&mut self.send_buffer);
|
||||
outer_ip_repr.emit(&mut ip, &smoltcp::phy::ChecksumCapabilities::ignored());
|
||||
|
||||
let ip_hdr_len = outer_ip_repr.buffer_len();
|
||||
let mut udp = UdpPacket::new_unchecked(&mut self.send_buffer[ip_hdr_len..]);
|
||||
udp_repr.emit(
|
||||
&mut udp,
|
||||
&IpAddress::Ipv4(self.local_ip),
|
||||
&IpAddress::Ipv4(self.remote_ip),
|
||||
udp_payload_len,
|
||||
|buf| {
|
||||
buf[..8].copy_from_slice(&self.vxlan_header);
|
||||
buf[8..8 + inner_eth.len()].copy_from_slice(&inner_eth);
|
||||
buf[8 + inner_eth.len()..].copy_from_slice(inner_packet);
|
||||
},
|
||||
&smoltcp::phy::ChecksumCapabilities::ignored(),
|
||||
);
|
||||
&self.send_buffer
|
||||
}
|
||||
|
||||
fn matches_endpoint(&self, outer_packet: &[u8]) -> bool {
|
||||
if outer_packet.len() < 50 {
|
||||
return false;
|
||||
}
|
||||
let Ok(ipv4) = Ipv4Packet::new_checked(outer_packet) else {
|
||||
return false;
|
||||
};
|
||||
if u8::from(ipv4.next_header()) != 17 || ipv4.dst_addr() != self.local_ip {
|
||||
return false;
|
||||
}
|
||||
let ip_hdr_len = 20;
|
||||
let udp = &outer_packet[ip_hdr_len..];
|
||||
if udp.len() < 18 {
|
||||
return false;
|
||||
}
|
||||
let dst_port = u16::from_be_bytes([udp[2], udp[3]]);
|
||||
if dst_port != VXLAN_PORT {
|
||||
return false;
|
||||
}
|
||||
if udp[8] != VXLAN_FLAGS {
|
||||
return false;
|
||||
}
|
||||
let pkt_vni = u32::from_be_bytes([0, udp[12], udp[13], udp[14]]) & 0x00ffffff;
|
||||
pkt_vni == self.vni
|
||||
}
|
||||
|
||||
fn decapsulate(&mut self, outer_packet: &[u8]) -> Option<&[u8]> {
|
||||
if !self.matches_endpoint(outer_packet) {
|
||||
return None;
|
||||
}
|
||||
let inner_frame_start = 20 + 8 + 8;
|
||||
let inner_frame = &outer_packet[inner_frame_start..];
|
||||
if inner_frame.len() < 14 {
|
||||
return None;
|
||||
}
|
||||
let eth = EthernetFrame::new_unchecked(inner_frame);
|
||||
let Ok(repr) = EthernetRepr::parse(ð) else {
|
||||
return None;
|
||||
};
|
||||
if repr.ethertype != EthernetProtocol::Ipv4 && repr.ethertype != EthernetProtocol::Ipv6 {
|
||||
return None;
|
||||
}
|
||||
self.recv_buffer.clear();
|
||||
self.recv_buffer.extend_from_slice(eth.payload());
|
||||
Some(&self.recv_buffer)
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for VxlanDevice {
|
||||
fn send(&mut self, _next_hop: IpAddress, packet: &[u8], _now: Instant) {
|
||||
// No parent device reference — drop rather than self-loop.
|
||||
log::debug!("vxlan: dropping {} byte frame (no parent device)", packet.len());
|
||||
}
|
||||
|
||||
fn recv(&mut self, _now: Instant) -> Option<&[u8]> {
|
||||
let packet = self.recv_queue.pop_front()?;
|
||||
self.decapsulate(&packet)
|
||||
}
|
||||
|
||||
fn name(&self) -> &Rc<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn can_recv(&self) -> bool {
|
||||
!self.recv_queue.is_empty()
|
||||
}
|
||||
|
||||
fn mac_address(&self) -> Option<EthernetAddress> {
|
||||
Some(self.virtual_mac)
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, addr: EthernetAddress) {
|
||||
self.virtual_mac = addr;
|
||||
}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
}
|
||||
+40
-6
@@ -13,11 +13,15 @@ use smoltcp::wire::EthernetAddress;
|
||||
|
||||
mod buffer_pool;
|
||||
mod error;
|
||||
mod filter;
|
||||
mod icmp_error;
|
||||
mod link;
|
||||
mod logger;
|
||||
mod observer;
|
||||
mod port_set;
|
||||
mod router;
|
||||
mod scheme;
|
||||
mod slaac;
|
||||
|
||||
fn get_network_adapter() -> Result<String> {
|
||||
use std::fs;
|
||||
@@ -82,6 +86,14 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
let netcfg_fd =
|
||||
Socket::nonblock().map_err(|e| anyhow!("failed to open netcfg scheme socket {:?}", e))?;
|
||||
|
||||
trace!("opening netfilter scheme socket");
|
||||
let netfilter_fd =
|
||||
Socket::nonblock().map_err(|e| anyhow!("failed to open netfilter scheme socket: {:?}", e))?;
|
||||
|
||||
trace!("opening tun scheme socket");
|
||||
let tun_fd =
|
||||
Socket::nonblock().map_err(|e| anyhow!("failed to open tun scheme socket: {:?}", e))?;
|
||||
|
||||
let time_path = format!("/scheme/time/{}", syscall::CLOCK_MONOTONIC);
|
||||
let time_fd = Fd::open(&time_path, O_RDWR, 0).context("failed to open /scheme/time")?;
|
||||
|
||||
@@ -94,6 +106,8 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
TcpScheme,
|
||||
IcmpScheme,
|
||||
NetcfgScheme,
|
||||
NetfilterScheme,
|
||||
TunScheme,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +126,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
|
||||
event_queue
|
||||
.subscribe(ip_fd.inner().raw(), EventSource::IpScheme, EventFlags::READ)
|
||||
.context("failed to listen to ip scheme events")?;
|
||||
.map_err(|e| anyhow!("failed to listen to ip scheme events: {:?}", e))?;
|
||||
|
||||
event_queue
|
||||
.subscribe(
|
||||
@@ -120,7 +134,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
EventSource::UdpScheme,
|
||||
EventFlags::READ,
|
||||
)
|
||||
.context("failed to listen to udp scheme events")?;
|
||||
.map_err(|e| anyhow!("failed to listen to udp scheme events: {:?}", e))?;
|
||||
|
||||
event_queue
|
||||
.subscribe(
|
||||
@@ -128,7 +142,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
EventSource::TcpScheme,
|
||||
EventFlags::READ,
|
||||
)
|
||||
.context("failed to listen to tcp scheme events")?;
|
||||
.map_err(|e| anyhow!("failed to listen to tcp scheme events: {:?}", e))?;
|
||||
|
||||
event_queue
|
||||
.subscribe(
|
||||
@@ -136,7 +150,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
EventSource::IcmpScheme,
|
||||
EventFlags::READ,
|
||||
)
|
||||
.context("failed to listen to icmp scheme events")?;
|
||||
.map_err(|e| anyhow!("failed to listen to icmp scheme events: {:?}", e))?;
|
||||
|
||||
event_queue
|
||||
.subscribe(
|
||||
@@ -144,7 +158,23 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
EventSource::NetcfgScheme,
|
||||
EventFlags::READ,
|
||||
)
|
||||
.context("failed to listen to netcfg scheme events")?;
|
||||
.map_err(|e| anyhow!("failed to listen to netcfg scheme events: {:?}", e))?;
|
||||
|
||||
event_queue
|
||||
.subscribe(
|
||||
netfilter_fd.inner().raw(),
|
||||
EventSource::NetfilterScheme,
|
||||
EventFlags::READ,
|
||||
)
|
||||
.map_err(|e| anyhow!("failed to listen to netfilter scheme events: {:?}", e))?;
|
||||
|
||||
event_queue
|
||||
.subscribe(
|
||||
tun_fd.inner().raw(),
|
||||
EventSource::TunScheme,
|
||||
EventFlags::READ,
|
||||
)
|
||||
.map_err(|e| anyhow!("failed to listen to tun scheme events: {:?}", e))?;
|
||||
|
||||
let mut smolnetd = Smolnetd::new(
|
||||
network_fd,
|
||||
@@ -155,6 +185,8 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
icmp_fd,
|
||||
time_fd,
|
||||
netcfg_fd,
|
||||
netfilter_fd,
|
||||
tun_fd,
|
||||
)
|
||||
.context("smolnetd: failed to initialize smolnetd")?;
|
||||
|
||||
@@ -162,7 +194,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
|
||||
let all = {
|
||||
use EventSource::*;
|
||||
[Network, Time, IpScheme, UdpScheme, IcmpScheme, NetcfgScheme].map(Ok)
|
||||
[Network, Time, IpScheme, UdpScheme, TcpScheme, IcmpScheme, NetcfgScheme, NetfilterScheme, TunScheme].map(Ok)
|
||||
};
|
||||
|
||||
for event_res in all
|
||||
@@ -177,6 +209,8 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
EventSource::TcpScheme => smolnetd.on_tcp_scheme_event(),
|
||||
EventSource::IcmpScheme => smolnetd.on_icmp_scheme_event(),
|
||||
EventSource::NetcfgScheme => smolnetd.on_netcfg_scheme_event(),
|
||||
EventSource::NetfilterScheme => smolnetd.on_netfilter_scheme_event(),
|
||||
EventSource::TunScheme => smolnetd.on_tun_scheme_event(),
|
||||
}
|
||||
.map_err(|e| error!("Received packet error: {:?}", e));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
//! Packet observer / capture facility — mirrors Linux 7.1's AF_PACKET + tcpdump.
|
||||
//!
|
||||
//! Provides a ring buffer that captures packets flowing through the network
|
||||
//! stack. Exposed via `/scheme/netcfg/capture` for reading by diagnostic tools.
|
||||
//!
|
||||
//! Usage:
|
||||
//! cat /scheme/netcfg/capture/read → drain captured packets (hex dump)
|
||||
//! cat /scheme/netcfg/capture/count → stats
|
||||
//! echo tcp > /scheme/netcfg/capture/filter → capture only TCP
|
||||
//! echo > /scheme/netcfg/capture/enable → start
|
||||
//! echo > /scheme/netcfg/capture/disable → stop
|
||||
//! echo tcp port 80 > /scheme/netcfg/capture/filter → TCP port 80 only
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
|
||||
const MAX_CAPTURE_PACKETS: usize = 256;
|
||||
const MAX_CAPTURE_BYTES: usize = 65536;
|
||||
|
||||
pub struct Observer {
|
||||
enabled: AtomicBool,
|
||||
buffer: RefCell<Vec<Vec<u8>>>,
|
||||
filter: RefCell<CaptureFilter>,
|
||||
max_packets: usize,
|
||||
total_captured: AtomicU64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CaptureFilter {
|
||||
proto: Option<u8>,
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
||||
impl CaptureFilter {
|
||||
fn new() -> Self {
|
||||
Self { proto: None, port: None }
|
||||
}
|
||||
|
||||
fn matches(&self, packet: &[u8]) -> bool {
|
||||
if self.proto.is_none() && self.port.is_none() {
|
||||
return true;
|
||||
}
|
||||
if packet.len() < 20 {
|
||||
return false; // too short to determine protocol/port
|
||||
}
|
||||
let version = packet[0] >> 4;
|
||||
let (proto, src_port_offset, dst_port_offset) = match version {
|
||||
4 => {
|
||||
if packet.len() < 24 { return true; }
|
||||
let ihl = (packet[0] & 0x0f) as usize * 4;
|
||||
if packet.len() < ihl + 4 { return true; }
|
||||
(packet[9], ihl, ihl + 2)
|
||||
}
|
||||
6 => {
|
||||
if packet.len() < 48 { return true; }
|
||||
(packet[6], 40, 42)
|
||||
}
|
||||
_ => return true,
|
||||
};
|
||||
if let Some(p) = self.proto {
|
||||
if proto != p { return false; }
|
||||
}
|
||||
if let Some(port) = self.port {
|
||||
let sp = u16::from_be_bytes([packet[src_port_offset], packet[src_port_offset + 1]]);
|
||||
let dp = u16::from_be_bytes([packet[dst_port_offset], packet[dst_port_offset + 1]]);
|
||||
if sp != port && dp != port { return false; }
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn from_str(s: &str) -> Self {
|
||||
let mut filter = Self::new();
|
||||
let lower = s.trim().to_lowercase();
|
||||
let parts: Vec<&str> = lower.split_whitespace().collect();
|
||||
let mut i = 0;
|
||||
while i < parts.len() {
|
||||
match parts[i] {
|
||||
"tcp" => filter.proto = Some(6),
|
||||
"udp" => filter.proto = Some(17),
|
||||
"icmp" => filter.proto = Some(1),
|
||||
"port" if i + 1 < parts.len() => {
|
||||
if let Ok(p) = parts[i + 1].parse::<u16>() {
|
||||
filter.port = Some(p);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
filter
|
||||
}
|
||||
}
|
||||
|
||||
pub type ObserverRef = Rc<Observer>;
|
||||
|
||||
impl Observer {
|
||||
pub fn new() -> ObserverRef {
|
||||
Rc::new(Observer {
|
||||
enabled: AtomicBool::new(false),
|
||||
buffer: RefCell::new(Vec::with_capacity(MAX_CAPTURE_PACKETS)),
|
||||
filter: RefCell::new(CaptureFilter::new()),
|
||||
max_packets: MAX_CAPTURE_PACKETS,
|
||||
total_captured: AtomicU64::new(0),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn enable(&self) {
|
||||
self.enabled.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn disable(&self) {
|
||||
self.enabled.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.enabled.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn set_filter(&self, filter_str: &str) {
|
||||
*self.filter.borrow_mut() = CaptureFilter::from_str(filter_str);
|
||||
}
|
||||
|
||||
pub fn filter_str(&self) -> String {
|
||||
let f = self.filter.borrow();
|
||||
let mut s = String::new();
|
||||
if let Some(p) = f.proto {
|
||||
s.push_str(match p { 6 => "tcp", 17 => "udp", 1 => "icmp", _ => "?" });
|
||||
}
|
||||
if let Some(port) = f.port {
|
||||
if !s.is_empty() { s.push(' '); }
|
||||
s.push_str(&format!("port {}", port));
|
||||
}
|
||||
if s.is_empty() { s.push_str("any"); }
|
||||
s
|
||||
}
|
||||
|
||||
pub fn capture(&self, packet: &[u8]) {
|
||||
if !self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
if !self.filter.borrow().matches(packet) {
|
||||
return;
|
||||
}
|
||||
let mut buf = self.buffer.borrow_mut();
|
||||
if buf.len() >= self.max_packets {
|
||||
buf.remove(0);
|
||||
}
|
||||
// Truncate to per-packet size limit so a single large packet
|
||||
// can't exhaust the total capture buffer.
|
||||
let per_packet_limit = MAX_CAPTURE_BYTES / self.max_packets.max(1);
|
||||
let truncated_len = packet.len().min(per_packet_limit);
|
||||
let mut copy = Vec::with_capacity(truncated_len);
|
||||
copy.extend_from_slice(&packet[..truncated_len]);
|
||||
buf.push(copy);
|
||||
self.total_captured.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn drain_hex(&self) -> String {
|
||||
let mut out = String::new();
|
||||
let mut buf = self.buffer.borrow_mut();
|
||||
for packet in buf.drain(..) {
|
||||
out.push_str(&format!("# {} bytes\n", packet.len()));
|
||||
for chunk in packet.chunks(16) {
|
||||
for byte in chunk {
|
||||
out.push_str(&format!("{:02x} ", byte));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.buffer.borrow().len()
|
||||
}
|
||||
|
||||
pub fn total(&self) -> u64 {
|
||||
self.total_captured.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,16 @@ impl PortSet {
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment the reference count for an already-claimed port (SO_REUSEADDR).
|
||||
/// Always succeeds: the caller has indicated SO_REUSEADDR is set, so
|
||||
/// multiple sockets are allowed to bind to the same port. The actual
|
||||
/// collision check (returning EADDRINUSE) is done by `claim_port` first.
|
||||
/// Returns true on success.
|
||||
pub fn claim_port_reuse(&mut self, port: u16) -> bool {
|
||||
self.ports.entry(port).and_modify(|c| *c += 1).or_insert(1);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn acquire_port(&mut self, port: u16) {
|
||||
*self.ports.entry(port).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
+418
-26
@@ -1,13 +1,16 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::phy::{Device, DeviceCapabilities, Medium};
|
||||
use smoltcp::storage::PacketMetadata;
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::IpAddress;
|
||||
use smoltcp::wire::{IpAddress, Ipv4Address, Ipv4Packet, Ipv6Packet};
|
||||
|
||||
use self::route_table::RouteTable;
|
||||
use self::route_table::{RouteTable, RouteType};
|
||||
use crate::filter::{FilterTable, Hook, PacketContext, Verdict};
|
||||
use crate::icmp_error;
|
||||
use crate::link::DeviceList;
|
||||
use crate::observer::ObserverRef;
|
||||
use crate::scheme::Smolnetd;
|
||||
|
||||
pub mod route_table;
|
||||
@@ -19,10 +22,12 @@ pub struct Router {
|
||||
tx_buffer: PacketBuffer,
|
||||
devices: Rc<RefCell<DeviceList>>,
|
||||
route_table: Rc<RefCell<RouteTable>>,
|
||||
pub ip_forward: Rc<Cell<bool>>,
|
||||
observer: ObserverRef,
|
||||
}
|
||||
|
||||
impl Router {
|
||||
pub fn new(devices: Rc<RefCell<DeviceList>>, route_table: Rc<RefCell<RouteTable>>) -> Self {
|
||||
pub fn new(devices: Rc<RefCell<DeviceList>>, route_table: Rc<RefCell<RouteTable>>, observer: ObserverRef) -> Self {
|
||||
let rx_buffer = PacketBuffer::new(
|
||||
vec![PacketMetadata::EMPTY; Smolnetd::SOCKET_BUFFER_SIZE],
|
||||
vec![0u8; Router::MTU * Smolnetd::SOCKET_BUFFER_SIZE],
|
||||
@@ -36,6 +41,8 @@ impl Router {
|
||||
tx_buffer,
|
||||
devices,
|
||||
route_table,
|
||||
ip_forward: Rc::new(Cell::new(true)),
|
||||
observer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +56,197 @@ impl Router {
|
||||
can_recv
|
||||
}
|
||||
|
||||
pub fn filter_input(&mut self, filter_table: &Rc<RefCell<FilterTable>>, now: Instant) {
|
||||
let mut filtered: Vec<Vec<u8>> = Vec::new();
|
||||
while let Ok(((), packet)) = self.rx_buffer.dequeue() {
|
||||
if packet.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut table = filter_table.borrow_mut();
|
||||
let context = infer_context(Hook::InputLocal, None, packet);
|
||||
match table.evaluate(&context, now) {
|
||||
Verdict::Accept => {
|
||||
drop(table);
|
||||
filtered.push(packet.to_vec());
|
||||
}
|
||||
Verdict::Reject => {
|
||||
drop(table);
|
||||
let err_fn = if packet[0] >> 4 == 6 {
|
||||
icmp_error::build_icmpv6_port_unreachable
|
||||
} else {
|
||||
icmp_error::build_icmpv4_port_unreachable
|
||||
};
|
||||
if let Some(err_pkt) = err_fn(packet) {
|
||||
if let Ok(buf) = self.tx_buffer.enqueue(err_pkt.len(), ()) {
|
||||
buf.copy_from_slice(&err_pkt);
|
||||
}
|
||||
}
|
||||
debug!("filter: rejected INPUT packet {} → {}", context.src_addr, context.dst_addr);
|
||||
}
|
||||
Verdict::Drop | Verdict::Log => {
|
||||
drop(table);
|
||||
debug!("filter: dropped INPUT packet {} → {}", context.src_addr, context.dst_addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
for packet in filtered {
|
||||
let Ok(buf) = self.rx_buffer.enqueue(packet.len(), ()) else {
|
||||
break;
|
||||
};
|
||||
buf.copy_from_slice(&packet);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forward_packets(&mut self, filter_table: &Rc<RefCell<FilterTable>>, now: Instant) {
|
||||
if !self.ip_forward.get() {
|
||||
return;
|
||||
}
|
||||
let mut forwarded: Vec<Vec<u8>> = Vec::new();
|
||||
let mut local: Vec<Vec<u8>> = Vec::new();
|
||||
|
||||
while let Ok(((), packet)) = self.rx_buffer.dequeue() {
|
||||
let packet_data: &[u8] = packet;
|
||||
if packet_data.is_empty() || packet_data[0] >> 4 != 4 {
|
||||
local.push(packet_data.to_vec());
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(ipv4) = Ipv4Packet::new_checked(packet_data) else {
|
||||
local.push(packet_data.to_vec());
|
||||
continue;
|
||||
};
|
||||
|
||||
let dst = IpAddress::Ipv4(ipv4.dst_addr());
|
||||
let is_broadcast = ipv4.dst_addr().is_broadcast() || dst.is_multicast();
|
||||
if is_broadcast {
|
||||
local.push(packet.to_vec());
|
||||
continue;
|
||||
}
|
||||
|
||||
let route_info = {
|
||||
let table = self.route_table.borrow();
|
||||
table.lookup_rule(&dst).map(|r| (r.dev.clone(), r.via, r.route_type))
|
||||
};
|
||||
|
||||
let Some((dev_name, _via, route_type)) = route_info else {
|
||||
local.push(packet.to_vec());
|
||||
continue;
|
||||
};
|
||||
|
||||
match route_type {
|
||||
RouteType::Blackhole => continue,
|
||||
RouteType::Unreachable | RouteType::Prohibit => {
|
||||
// Build the ICMPv4 error and queue it for transmit
|
||||
// back to the original sender. Using rx_buffer here
|
||||
// would re-route the error back into the input path
|
||||
// (infinite loop) and never reach the sender.
|
||||
if let Some(error_pkt) = icmp_error::build_icmpv4_port_unreachable(packet) {
|
||||
let _ = self.tx_buffer.enqueue(error_pkt.len(), ())
|
||||
.map(|b| b.copy_from_slice(&error_pkt));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
RouteType::Unicast => {}
|
||||
}
|
||||
let mut buf = packet.to_vec();
|
||||
let context = infer_context(Hook::Forward, Some(dev_name.clone()), &buf);
|
||||
|
||||
{
|
||||
let mut table = filter_table.borrow_mut();
|
||||
if table.evaluate(&context, now) == Verdict::Drop {
|
||||
debug!("filter: dropped FORWARD packet");
|
||||
continue;
|
||||
}
|
||||
if let Some((trans_addr, _)) = table.nat_table.lookup_snat(
|
||||
Hook::Forward,
|
||||
IpAddress::Ipv4(ipv4.src_addr()),
|
||||
dst,
|
||||
) {
|
||||
let IpAddress::Ipv4(new_src) = trans_addr else { continue; };
|
||||
let _ = crate::filter::rewrite_src_ipv4(&mut buf, new_src);
|
||||
}
|
||||
}
|
||||
|
||||
let ttl = ipv4.hop_limit();
|
||||
if ttl <= 1 {
|
||||
debug!("forward: TTL expired");
|
||||
if let Some(error_pkt) = icmp_error::build_icmpv4_time_exceeded(&buf) {
|
||||
if let Ok(buf) = self.tx_buffer.enqueue(error_pkt.len(), ()) {
|
||||
buf.copy_from_slice(&error_pkt);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
buf[8] = ttl - 1;
|
||||
if let Ok(mut pkt) = Ipv4Packet::new_checked(&mut buf) {
|
||||
pkt.fill_checksum();
|
||||
}
|
||||
|
||||
forwarded.push(buf);
|
||||
}
|
||||
|
||||
for packet in &forwarded {
|
||||
self.observer.capture(packet);
|
||||
}
|
||||
for packet in &local {
|
||||
self.observer.capture(packet);
|
||||
}
|
||||
|
||||
for packet in local {
|
||||
let Ok(buf) = self.rx_buffer.enqueue(packet.len(), ()) else {
|
||||
break;
|
||||
};
|
||||
buf.copy_from_slice(&packet);
|
||||
}
|
||||
for packet in forwarded {
|
||||
let Ok(buf) = self.tx_buffer.enqueue(packet.len(), ()) else {
|
||||
break;
|
||||
};
|
||||
buf.copy_from_slice(&packet);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter_output(&self, packet: &[u8], filter_table: &Rc<RefCell<FilterTable>>, now: Instant) -> Verdict {
|
||||
if packet.is_empty() {
|
||||
return Verdict::Accept;
|
||||
}
|
||||
let context = infer_context(Hook::OutputLocal, None, packet);
|
||||
filter_table.borrow_mut().evaluate(&context, now)
|
||||
}
|
||||
|
||||
fn apply_snat(
|
||||
&self,
|
||||
packet: &mut [u8],
|
||||
filter_table: &Rc<RefCell<FilterTable>>,
|
||||
) {
|
||||
if packet.is_empty() {
|
||||
return;
|
||||
}
|
||||
let version = packet[0] >> 4;
|
||||
if version != 4 {
|
||||
return;
|
||||
}
|
||||
let Ok(ipv4) = Ipv4Packet::new_checked(&*packet) else {
|
||||
return;
|
||||
};
|
||||
let src = IpAddress::Ipv4(ipv4.src_addr());
|
||||
let dst = IpAddress::Ipv4(ipv4.dst_addr());
|
||||
let table = filter_table.borrow();
|
||||
let snat = table.nat_table.lookup_snat(
|
||||
crate::filter::Hook::OutputLocal,
|
||||
src,
|
||||
dst,
|
||||
);
|
||||
drop(table);
|
||||
|
||||
if let Some((trans_addr, _trans_port)) = snat {
|
||||
let IpAddress::Ipv4(new_src) = trans_addr else {
|
||||
return;
|
||||
};
|
||||
crate::filter::rewrite_src_ipv4(packet, new_src);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, now: Instant) {
|
||||
for dev in self.devices.borrow_mut().iter_mut() {
|
||||
if self.rx_buffer.is_full() {
|
||||
@@ -72,41 +270,130 @@ impl Router {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch(&mut self, now: Instant) {
|
||||
pub fn dispatch(&mut self, now: Instant, filter_table: &Rc<RefCell<FilterTable>>) {
|
||||
let mut packets: Vec<Vec<u8>> = Vec::new();
|
||||
while let Ok(((), packet)) = self.tx_buffer.dequeue() {
|
||||
if let Ok(mut packet) = smoltcp::wire::Ipv4Packet::new_checked(packet) {
|
||||
let dst_addr = IpAddress::Ipv4(packet.dst_addr());
|
||||
if packet.dst_addr().is_broadcast() {
|
||||
let buf = packet.into_inner();
|
||||
for dev in self.devices.borrow_mut().iter_mut() {
|
||||
dev.send(dst_addr, buf, now)
|
||||
}
|
||||
} else {
|
||||
let route_table = self.route_table.borrow();
|
||||
let Some(rule) = route_table.lookup_rule(&dst_addr) else {
|
||||
warn!("No route found for destination: {}", dst_addr);
|
||||
if !packet.is_empty() {
|
||||
self.observer.capture(packet);
|
||||
packets.push(packet.to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
for packet in packets {
|
||||
if packet.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match packet[0] >> 4 {
|
||||
4 => {
|
||||
let mut packet_buf = packet;
|
||||
let Ok(mut ipv4_pkt) = smoltcp::wire::Ipv4Packet::new_checked(&mut packet_buf)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let dst_addr = IpAddress::Ipv4(ipv4_pkt.dst_addr());
|
||||
let src_addr = ipv4_pkt.src_addr();
|
||||
|
||||
let next_hop = match rule.via {
|
||||
Some(via) => via,
|
||||
None => dst_addr,
|
||||
let (next_hop, src_rule, dev_name, route_type) = {
|
||||
let route_table = self.route_table.borrow();
|
||||
let Some(rule) = route_table.lookup_rule(&dst_addr) else {
|
||||
warn!("No route found for destination: {}", dst_addr);
|
||||
continue;
|
||||
};
|
||||
let next_hop = rule.via.unwrap_or(dst_addr);
|
||||
(next_hop, rule.src, rule.dev.clone(), rule.route_type)
|
||||
};
|
||||
|
||||
match route_type {
|
||||
RouteType::Blackhole => continue,
|
||||
RouteType::Unreachable => {
|
||||
if let Some(error_pkt) = icmp_error::build_icmpv4_port_unreachable(&packet_buf) {
|
||||
let _ = self.tx_buffer.enqueue(error_pkt.len(), ())
|
||||
.map(|b| b.copy_from_slice(&error_pkt));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
RouteType::Prohibit => {
|
||||
if let Some(error_pkt) = icmp_error::build_icmpv4_port_unreachable(&packet_buf) {
|
||||
let _ = self.tx_buffer.enqueue(error_pkt.len(), ())
|
||||
.map(|b| b.copy_from_slice(&error_pkt));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
RouteType::Unicast => {}
|
||||
}
|
||||
|
||||
if ipv4_pkt.dst_addr().is_broadcast() {
|
||||
let buf = ipv4_pkt.into_inner();
|
||||
if self.filter_output(buf, filter_table, now) == Verdict::Drop {
|
||||
debug!("filter: dropped OUTPUT broadcast IPv4 packet");
|
||||
continue;
|
||||
}
|
||||
self.apply_snat(buf, filter_table);
|
||||
for dev in self.devices.borrow_mut().iter_mut() {
|
||||
dev.send(dst_addr, buf, now);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let IpAddress::Ipv4(src) = src_rule {
|
||||
if src != src_addr {
|
||||
ipv4_pkt.set_src_addr(src);
|
||||
ipv4_pkt.fill_checksum();
|
||||
}
|
||||
}
|
||||
|
||||
let buf = ipv4_pkt.into_inner();
|
||||
let mut devices = self.devices.borrow_mut();
|
||||
let Some(dev) = devices.get_mut(&rule.dev) else {
|
||||
warn!("Device {} not found", rule.dev);
|
||||
let Some(dev) = devices.get_mut(&dev_name) else {
|
||||
warn!("Device {} not found", dev_name);
|
||||
// TODO: Remove route if device doesn't exist anymore ?
|
||||
continue;
|
||||
};
|
||||
if self.filter_output(buf, filter_table, now) == Verdict::Drop {
|
||||
debug!("filter: dropped OUTPUT IPv4 packet");
|
||||
continue;
|
||||
}
|
||||
self.apply_snat(buf, filter_table);
|
||||
dev.send(next_hop, buf, now);
|
||||
}
|
||||
6 => {
|
||||
let Ok(ipv6_pkt) = smoltcp::wire::Ipv6Packet::new_checked(&packet) else {
|
||||
continue;
|
||||
};
|
||||
let dst_addr = IpAddress::Ipv6(ipv6_pkt.dst_addr());
|
||||
|
||||
let IpAddress::Ipv4(src) = rule.src;
|
||||
if src != packet.src_addr() {
|
||||
packet.set_src_addr(src);
|
||||
packet.fill_checksum()
|
||||
let (next_hop, dev_name, route_type) = {
|
||||
let route_table = self.route_table.borrow();
|
||||
let Some(rule) = route_table.lookup_rule(&dst_addr) else {
|
||||
warn!("No route found for destination: {}", dst_addr);
|
||||
continue;
|
||||
};
|
||||
let next_hop = rule.via.unwrap_or(dst_addr);
|
||||
(next_hop, rule.dev.clone(), rule.route_type)
|
||||
};
|
||||
|
||||
match route_type {
|
||||
RouteType::Blackhole => continue,
|
||||
RouteType::Unreachable | RouteType::Prohibit => continue,
|
||||
RouteType::Unicast => {}
|
||||
}
|
||||
|
||||
dev.send(next_hop, packet.into_inner(), now);
|
||||
let mut devices = self.devices.borrow_mut();
|
||||
let Some(dev) = devices.get_mut(&dev_name) else {
|
||||
warn!("Device {} not found", dev_name);
|
||||
continue;
|
||||
};
|
||||
|
||||
if self.filter_output(&packet, filter_table, now) == Verdict::Drop {
|
||||
debug!("filter: dropped OUTPUT IPv6 packet");
|
||||
continue;
|
||||
}
|
||||
|
||||
dev.send(next_hop, &packet, now);
|
||||
}
|
||||
version => {
|
||||
debug!("Dropped packet with unknown IP version: {}", version);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,3 +475,108 @@ impl<'a> smoltcp::phy::RxToken for RxToken<'a> {
|
||||
f(buf)
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_context(
|
||||
hook: Hook,
|
||||
_dev_name: Option<Rc<str>>,
|
||||
packet: &[u8],
|
||||
) -> PacketContext<'_> {
|
||||
let version = if packet.is_empty() { 0 } else { packet[0] >> 4 };
|
||||
match version {
|
||||
4 => {
|
||||
if let Ok(ipv4) = Ipv4Packet::new_checked(packet) {
|
||||
let (src_port, dst_port) = parse_ports(&ipv4.next_header(), ipv4.payload());
|
||||
PacketContext {
|
||||
hook,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: IpAddress::Ipv4(ipv4.src_addr()),
|
||||
dst_addr: IpAddress::Ipv4(ipv4.dst_addr()),
|
||||
protocol: ipv4.next_header().into(),
|
||||
src_port,
|
||||
dst_port,
|
||||
packet,
|
||||
}
|
||||
} else {
|
||||
PacketContext {
|
||||
hook,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED),
|
||||
dst_addr: IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED),
|
||||
protocol: 0,
|
||||
src_port: None,
|
||||
dst_port: None,
|
||||
packet,
|
||||
}
|
||||
}
|
||||
}
|
||||
6 => {
|
||||
if let Ok(ipv6) = Ipv6Packet::new_checked(packet) {
|
||||
let nh = ipv6.next_header();
|
||||
let payload_start = 40; // fixed IPv6 header
|
||||
// Extension headers (Hop-by-Hop, Routing, Fragment, etc.)
|
||||
// would shift the transport header to a higher offset.
|
||||
// smoltcp's next_header() chases extension headers to return
|
||||
// the final protocol, but we don't compute the actual
|
||||
// transport offset. For packets with extension headers the
|
||||
// port extraction below will read the wrong bytes and return
|
||||
// None/None, which is safe: the filter matches on IP+protocol
|
||||
// but silently skips port matching.
|
||||
let payload = if packet.len() > payload_start {
|
||||
&packet[payload_start..]
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
let (src_port, dst_port) = parse_ports(&nh, payload);
|
||||
PacketContext {
|
||||
hook,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: IpAddress::Ipv6(ipv6.src_addr()),
|
||||
dst_addr: IpAddress::Ipv6(ipv6.dst_addr()),
|
||||
protocol: nh.into(),
|
||||
src_port,
|
||||
dst_port,
|
||||
packet,
|
||||
}
|
||||
} else {
|
||||
PacketContext {
|
||||
hook,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: IpAddress::Ipv6(smoltcp::wire::Ipv6Address::UNSPECIFIED),
|
||||
dst_addr: IpAddress::Ipv6(smoltcp::wire::Ipv6Address::UNSPECIFIED),
|
||||
protocol: 0,
|
||||
src_port: None,
|
||||
dst_port: None,
|
||||
packet,
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => PacketContext {
|
||||
hook,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED),
|
||||
dst_addr: IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED),
|
||||
protocol: 0,
|
||||
src_port: None,
|
||||
dst_port: None,
|
||||
packet,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ports(protocol: &smoltcp::wire::IpProtocol, payload: &[u8]) -> (Option<u16>, Option<u16>) {
|
||||
let proto_byte: u8 = (*protocol).into();
|
||||
if proto_byte != 6 && proto_byte != 17 && proto_byte != 58 {
|
||||
return (None, None);
|
||||
}
|
||||
if payload.len() < 4 {
|
||||
return (None, None);
|
||||
}
|
||||
let src = u16::from_be_bytes([payload[0], payload[1]]);
|
||||
let dst = u16::from_be_bytes([payload[2], payload[3]]);
|
||||
(Some(src), Some(dst))
|
||||
}
|
||||
|
||||
@@ -3,12 +3,42 @@ use std::rc::Rc;
|
||||
|
||||
use smoltcp::wire::{IpAddress, IpCidr};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RouteType {
|
||||
Unicast,
|
||||
Blackhole,
|
||||
Unreachable,
|
||||
Prohibit,
|
||||
}
|
||||
|
||||
impl RouteType {
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
RouteType::Unicast => "unicast",
|
||||
RouteType::Blackhole => "blackhole",
|
||||
RouteType::Unreachable => "unreachable",
|
||||
RouteType::Prohibit => "prohibit",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"unicast" => Some(RouteType::Unicast),
|
||||
"blackhole" => Some(RouteType::Blackhole),
|
||||
"unreachable" => Some(RouteType::Unreachable),
|
||||
"prohibit" => Some(RouteType::Prohibit),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Rule {
|
||||
pub filter: IpCidr,
|
||||
pub via: Option<IpAddress>,
|
||||
pub dev: Rc<str>,
|
||||
pub src: IpAddress,
|
||||
pub route_type: RouteType,
|
||||
}
|
||||
|
||||
impl Rule {
|
||||
@@ -18,12 +48,26 @@ impl Rule {
|
||||
via,
|
||||
dev,
|
||||
src,
|
||||
route_type: RouteType::Unicast,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_type(
|
||||
filter: IpCidr,
|
||||
via: Option<IpAddress>,
|
||||
dev: Rc<str>,
|
||||
src: IpAddress,
|
||||
route_type: RouteType,
|
||||
) -> Self {
|
||||
Self { filter, via, dev, src, route_type }
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Rule {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.route_type != RouteType::Unicast {
|
||||
write!(f, "{} ", self.route_type.name())?;
|
||||
}
|
||||
if self.filter.prefix_len() == 0 {
|
||||
write!(f, "default")?;
|
||||
} else {
|
||||
@@ -86,6 +130,10 @@ impl RouteTable {
|
||||
rule.src = new_src;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.rules.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RouteTable {
|
||||
|
||||
+53
-14
@@ -17,6 +17,16 @@ use crate::router::Router;
|
||||
|
||||
pub type IcmpScheme = SchemeWrapper<IcmpSocket<'static>>;
|
||||
|
||||
// ICMP socket variants.
|
||||
//
|
||||
// Echo: standard ICMP echo request/reply (e.g. for ping). Ident is
|
||||
// assigned from the ICMP ident pool (1..0xffff).
|
||||
//
|
||||
// Udp: notification endpoint for ICMP error messages received for
|
||||
// connected UDP sockets. Mirrors Linux's `IP_RECVERR` mechanism. When
|
||||
// the network receives an ICMPv4 error for a UDP datagram, the matching
|
||||
// socket is woken with `EWOULDBLOCK` and the error is queued. The user
|
||||
// reads it via the ICMP scheme's read path. This path is read-only.
|
||||
enum IcmpSocketType {
|
||||
Echo,
|
||||
Udp,
|
||||
@@ -214,22 +224,51 @@ impl<'a> SchemeSocket for IcmpSocket<'a> {
|
||||
return Ok(0);
|
||||
}
|
||||
while self.can_recv(&file.data) {
|
||||
let (payload, _) = self.recv().expect("Can't recv icmp packet");
|
||||
let (payload, _) = match self.recv() {
|
||||
Ok(p) => p,
|
||||
Err(_) => break, // malformed recv, stop reading
|
||||
};
|
||||
let icmp_packet = Icmpv4Packet::new_unchecked(&payload);
|
||||
//TODO: replace default with actual caps
|
||||
let icmp_repr = Icmpv4Repr::parse(&icmp_packet, &Default::default()).unwrap();
|
||||
// Drop packets that fail to parse — don't crash the daemon.
|
||||
let Ok(icmp_repr) = Icmpv4Repr::parse(&icmp_packet, &Default::default()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Icmpv4Repr::EchoReply { seq_no, data, .. } = icmp_repr {
|
||||
if buf.len() < mem::size_of::<u16>() + data.len() {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
match file.data.socket_type {
|
||||
IcmpSocketType::Echo => {
|
||||
// For echo sockets, only echo replies are expected.
|
||||
// Other ICMP types are silently dropped.
|
||||
if let Icmpv4Repr::EchoReply { seq_no, data, .. } = icmp_repr {
|
||||
if buf.len() < mem::size_of::<u16>() + data.len() {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
}
|
||||
buf[0..2].copy_from_slice(&seq_no.to_be_bytes());
|
||||
for i in 0..data.len() {
|
||||
buf[mem::size_of::<u16>() + i] = data[i];
|
||||
}
|
||||
return Ok(mem::size_of::<u16>() + data.len());
|
||||
}
|
||||
}
|
||||
buf[0..2].copy_from_slice(&seq_no.to_be_bytes());
|
||||
|
||||
for i in 0..data.len() {
|
||||
buf[mem::size_of::<u16>() + i] = data[i];
|
||||
IcmpSocketType::Udp => {
|
||||
// For ICMP error notification endpoints (IP_RECVERR
|
||||
// style), serialize the ICMP error type and original
|
||||
// packet metadata. Format:
|
||||
// [0]: icmp type (e.g. 3=dst unreachable, 11=time exceeded)
|
||||
// [1]: icmp code
|
||||
// [2..6]: reserved
|
||||
// [6..]: original IP header (up to buf.len()-6)
|
||||
if buf.len() < 7 {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
}
|
||||
buf[0] = payload[0];
|
||||
buf[1] = payload[1];
|
||||
let copy_len = (buf.len() - 6).min(payload.len().saturating_sub(8));
|
||||
if copy_len > 0 {
|
||||
buf[6..6 + copy_len].copy_from_slice(&payload[8..8 + copy_len]);
|
||||
}
|
||||
return Ok(6 + copy_len);
|
||||
}
|
||||
|
||||
return Ok(mem::size_of::<u16>() + data.len());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,9 +308,9 @@ impl<'a> SchemeSocket for IcmpSocket<'a> {
|
||||
|
||||
fn handle_recvmsg(
|
||||
&mut self,
|
||||
file: &mut SchemeFile<Self>,
|
||||
how: &mut [u8],
|
||||
flags: usize,
|
||||
_file: &mut SchemeFile<Self>,
|
||||
_how: &mut [u8],
|
||||
_flags: usize,
|
||||
) -> SyscallResult<usize> {
|
||||
return Err(SyscallError::new(syscall::EOPNOTSUPP));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user