diff --git a/files/build_templates/sonic_debian_extension.j2 b/files/build_templates/sonic_debian_extension.j2 index aa970873bba2..a0651c2efb03 100644 --- a/files/build_templates/sonic_debian_extension.j2 +++ b/files/build_templates/sonic_debian_extension.j2 @@ -99,6 +99,11 @@ sudo mkdir -p $FILESYSTEM_ROOT_USR_SHARE_SONIC_FIRMWARE/ # Keeping it generic. It should not harm anyways. sudo mkdir -p $FILESYSTEM_ROOT_USR_LIB_SYSTEMD_SYSTEM +# Install sonic-nettools +sudo dpkg --root=$FILESYSTEM_ROOT -i $debs_path/sonic-nettools_*.deb || \ + sudo LANG=C DEBIAN_FRONTEND=noninteractive chroot $FILESYSTEM_ROOT apt-get -y install -f +sudo setcap 'cap_net_raw=+ep' $FILESYSTEM_ROOT/usr/bin/wol + # Install a patched version of ifupdown2 (and its dependencies via 'apt-get -y install -f') sudo dpkg --root=$FILESYSTEM_ROOT -i $debs_path/ifupdown2_*.deb || \ sudo LANG=C DEBIAN_FRONTEND=noninteractive chroot $FILESYSTEM_ROOT apt-get -y install -f diff --git a/rules/sonic-nettools.mk b/rules/sonic-nettools.mk new file mode 100644 index 000000000000..44351ec6be2a --- /dev/null +++ b/rules/sonic-nettools.mk @@ -0,0 +1,7 @@ +SONIC_NETTOOLS_VERSION = 0.0.1-0 + +export SONIC_NETTOOLS_VERSION + +SONIC_NETTOOLS = sonic-nettools_$(SONIC_NETTOOLS_VERSION)_$(CONFIGURED_ARCH).deb +$(SONIC_NETTOOLS)_SRC_PATH = $(SRC_PATH)/sonic-nettools +SONIC_DPKG_DEBS += $(SONIC_NETTOOLS) diff --git a/slave.mk b/slave.mk index 030923f60d7d..cdc2aba00d71 100644 --- a/slave.mk +++ b/slave.mk @@ -362,6 +362,15 @@ CROSS_COMPILE_FLAGS := CGO_ENABLED=1 GOOS=linux GOARCH=$(GOARCH) CROSS_COMPILE=$ endif +ifeq ($(CROSS_BUILD_ENVIRON),y) +ifeq ($(CONFIGURED_ARCH),armhf) +RUST_CROSS_COMPILE_TARGET = armv7-unknown-linux-gnueabihf +else ifeq ($(CONFIGURED_ARCH),arm64) +RUST_CROSS_COMPILE_TARGET = aarch64-unknown-linux-gnu +endif +export RUST_CROSS_COMPILE_TARGET +endif + ############################################################################### ## Routing stack related exports ############################################################################### @@ -1370,6 +1379,7 @@ $(addprefix $(TARGET_PATH)/, $(SONIC_INSTALLERS)) : $(TARGET_PATH)/% : \ $(PYTHON_SWSSCOMMON) \ $(PYTHON3_SWSSCOMMON) \ $(SONIC_DB_CLI) \ + $(SONIC_NETTOOLS) \ $(SONIC_RSYSLOG_PLUGIN) \ $(SONIC_UTILITIES_DATA) \ $(SONIC_HOST_SERVICES_DATA) \ diff --git a/sonic-slave-bookworm/Dockerfile.j2 b/sonic-slave-bookworm/Dockerfile.j2 index fbe78a335afd..67f2b1f2c58a 100644 --- a/sonic-slave-bookworm/Dockerfile.j2 +++ b/sonic-slave-bookworm/Dockerfile.j2 @@ -113,6 +113,7 @@ RUN apt-get update && apt-get install -y eatmydata && eatmydata apt-get install git-buildpackage \ perl-modules \ libclass-accessor-perl \ + libcap2-bin \ libswitch-perl \ libzmq5 \ libzmq3-dev \ @@ -657,6 +658,7 @@ RUN eatmydata apt-get install -y \ pps-tools:$arch \ libpam-cap:$arch \ libcap-dev:$arch \ + libcap2-bin:$arch \ libpam0g-dev:$arch \ libaudit-dev:$arch \ libgtk-3-dev:$arch \ @@ -738,3 +740,15 @@ RUN eatmydata apt install -y python-is-python3 ARG bazelisk_url=https://github.com/bazelbuild/bazelisk/releases/latest/download/bazelisk-linux-{{ CONFIGURED_ARCH }} RUN curl -fsSL -o /usr/local/bin/bazel ${bazelisk_url} && chmod 755 /usr/local/bin/bazel {% endif -%} + +# Install Rust +ARG RUST_ROOT=/usr/.cargo +RUN RUSTUP_HOME=$RUST_ROOT CARGO_HOME=$RUST_ROOT bash -c 'curl --proto "=https" -sSf https://sh.rustup.rs | sh -s -- --default-toolchain 1.79.0 -y' +{% if CROSS_BUILD_ENVIRON == "y" and CONFIGURED_ARCH == "armhf" %} +RUN mkdir -p /.cargo && $RUST_ROOT/bin/rustup target add armv7-unknown-linux-gnueabihf && echo "[target.armv7-unknown-linux-gnueabihf]\nlinker = \"arm-linux-gnueabihf-gcc\"" >> /.cargo/config.toml +{% endif -%} +{% if CROSS_BUILD_ENVIRON == "y" and CONFIGURED_ARCH == "arm64" %} +RUN mkdir -p /.cargo && $RUST_ROOT/bin/rustup target add aarch64-unknown-linux-gnu && echo "[target.aarch64-unknown-linux-gnu]\nlinker = \"aarch64-linux-gnu-gcc\"" >> /.cargo/config.toml +{% endif -%} +ENV RUSTUP_HOME $RUST_ROOT +ENV PATH $PATH:$RUST_ROOT/bin \ No newline at end of file diff --git a/src/sonic-nettools/.gitignore b/src/sonic-nettools/.gitignore new file mode 100644 index 000000000000..046d25633d83 --- /dev/null +++ b/src/sonic-nettools/.gitignore @@ -0,0 +1,3 @@ +target/ +bin/ +.vscode/ \ No newline at end of file diff --git a/src/sonic-nettools/Cargo.lock b/src/sonic-nettools/Cargo.lock new file mode 100644 index 000000000000..695393102228 --- /dev/null +++ b/src/sonic-nettools/Cargo.lock @@ -0,0 +1,442 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "clap" +version = "4.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" +dependencies = [ + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "pnet" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "682396b533413cc2e009fbb48aadf93619a149d3e57defba19ff50ce0201bd0d" +dependencies = [ + "ipnetwork", + "pnet_base", + "pnet_datalink", + "pnet_packet", + "pnet_sys", + "pnet_transport", +] + +[[package]] +name = "pnet_base" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc190d4067df16af3aba49b3b74c469e611cad6314676eaf1157f31aa0fb2f7" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_datalink" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79e70ec0be163102a332e1d2d5586d362ad76b01cec86f830241f2b6452a7b7" +dependencies = [ + "ipnetwork", + "libc", + "pnet_base", + "pnet_sys", + "winapi", +] + +[[package]] +name = "pnet_macros" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13325ac86ee1a80a480b0bc8e3d30c25d133616112bb16e86f712dcf8a71c863" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "pnet_macros_support" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed67a952585d509dd0003049b1fc56b982ac665c8299b124b90ea2bdb3134ab" +dependencies = [ + "pnet_base", +] + +[[package]] +name = "pnet_packet" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c96ebadfab635fcc23036ba30a7d33a80c39e8461b8bd7dc7bb186acb96560f" +dependencies = [ + "glob", + "pnet_base", + "pnet_macros", + "pnet_macros_support", +] + +[[package]] +name = "pnet_sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4643d3d4db6b08741050c2f3afa9a892c4244c085a72fcda93c9c2c9a00f4b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "pnet_transport" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f604d98bc2a6591cf719b58d3203fd882bdd6bf1db696c4ac97978e9f4776bf" +dependencies = [ + "libc", + "pnet_base", + "pnet_packet", + "pnet_sys", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "serde" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201fcda3845c23e8212cd466bfebf0bd20694490fc0356ae8e428e0824a915a6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[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 = "wol" +version = "0.0.1" +dependencies = [ + "clap", + "pnet", +] diff --git a/src/sonic-nettools/Cargo.toml b/src/sonic-nettools/Cargo.toml new file mode 100644 index 000000000000..37d4a6460a8e --- /dev/null +++ b/src/sonic-nettools/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] + +members = [ + "wol", +] diff --git a/src/sonic-nettools/Makefile b/src/sonic-nettools/Makefile new file mode 100644 index 000000000000..021b1eb805c6 --- /dev/null +++ b/src/sonic-nettools/Makefile @@ -0,0 +1,17 @@ +include ../../rules/sonic-nettools.mk + +$(addprefix $(DEST)/, $(SONIC_NETTOOLS)): $(DEST)/% : + mkdir -p bin +ifeq ($(CROSS_BUILD_ENVIRON), y) + cargo test --target=$(RUST_CROSS_COMPILE_TARGET) + cargo build --release --target=$(RUST_CROSS_COMPILE_TARGET) + mv -f target/$(RUST_CROSS_COMPILE_TARGET)/release/wol bin/ +else + cargo test + cargo build --release + mv -f target/release/wol bin/ +endif + +clean: + rm -rf target + rm -rf bin \ No newline at end of file diff --git a/src/sonic-nettools/debian/changelog b/src/sonic-nettools/debian/changelog new file mode 100644 index 000000000000..6123545b50b2 --- /dev/null +++ b/src/sonic-nettools/debian/changelog @@ -0,0 +1,5 @@ +sonic-nettools (0.0.1-0) UNRELEASED; urgency=medium + + * Initial release. + + -- Wenda Chu Mon, 10 Jun 2024 12:49:43 -0700 diff --git a/src/sonic-nettools/debian/compat b/src/sonic-nettools/debian/compat new file mode 100644 index 000000000000..48082f72f087 --- /dev/null +++ b/src/sonic-nettools/debian/compat @@ -0,0 +1 @@ +12 diff --git a/src/sonic-nettools/debian/control b/src/sonic-nettools/debian/control new file mode 100644 index 000000000000..b77ec470ea1b --- /dev/null +++ b/src/sonic-nettools/debian/control @@ -0,0 +1,9 @@ +Source: sonic-nettools +Maintainer: Wenda Chu +Standards-Version: 0.0.1 +Section: custom + +Package: sonic-nettools +Priority: optional +Architecture: any +Description: Networking command line tools diff --git a/src/sonic-nettools/debian/install b/src/sonic-nettools/debian/install new file mode 100644 index 000000000000..902a829820b1 --- /dev/null +++ b/src/sonic-nettools/debian/install @@ -0,0 +1 @@ +bin/wol /usr/bin/ diff --git a/src/sonic-nettools/debian/rules b/src/sonic-nettools/debian/rules new file mode 100755 index 000000000000..cbe925d75871 --- /dev/null +++ b/src/sonic-nettools/debian/rules @@ -0,0 +1,3 @@ +#!/usr/bin/make -f +%: + dh $@ diff --git a/src/sonic-nettools/wol/Cargo.toml b/src/sonic-nettools/wol/Cargo.toml new file mode 100644 index 000000000000..6b2276e79724 --- /dev/null +++ b/src/sonic-nettools/wol/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "wol" +version = "0.0.1" + +[dependencies] +pnet = "0.35.0" +clap = { version = "4.5.7", features = ["derive"] } diff --git a/src/sonic-nettools/wol/src/main.rs b/src/sonic-nettools/wol/src/main.rs new file mode 100644 index 000000000000..c04ba9411bcd --- /dev/null +++ b/src/sonic-nettools/wol/src/main.rs @@ -0,0 +1,13 @@ +mod wol; + +extern crate clap; +extern crate pnet; + +fn main() { + if let Err(e) = wol::build_and_send() { + eprintln!("Error: {}", e.msg); + std::process::exit(e.code); + } else { + std::process::exit(0); + } +} diff --git a/src/sonic-nettools/wol/src/wol.rs b/src/sonic-nettools/wol/src/wol.rs new file mode 100644 index 000000000000..b5d598d369f3 --- /dev/null +++ b/src/sonic-nettools/wol/src/wol.rs @@ -0,0 +1,687 @@ +use clap::builder::ArgPredicate; +use clap::Parser; +use pnet::datalink::Channel::Ethernet; +use pnet::datalink::{self, DataLinkSender, MacAddr, NetworkInterface}; +use std::fs::read_to_string; +use std::result::Result; +use std::str::FromStr; +use std::thread; +use std::time::Duration; + +const BROADCAST_MAC: [u8; 6] = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff]; + +#[derive(Parser, Debug)] +#[command( + next_line_help = true, + about = " +This tool can generate and send wake on LAN magic packets with target interface and mac + +Examples: + wol Ethernet10 00:11:22:33:44:55 + wol Ethernet10 00:11:22:33:44:55 -b + wol Vlan1000 00:11:22:33:44:55,11:33:55:77:99:bb -p 00:22:44:66:88:aa + wol Vlan1000 00:11:22:33:44:55,11:33:55:77:99:bb -p 192.168.1.1 -c 3 -i 2000" +)] +struct WolArgs { + /// The name of the network interface to send the magic packet through + interface: String, + + /// The MAC address of the target device, formatted as a colon-separated string (e.g. "00:11:22:33:44:55") + target_mac: String, + + /// The flag to indicate if use broadcast MAC address instead of target device's MAC address as Destination MAC Address in Ethernet Frame Header [default: false] + #[arg(short, long, default_value_t = false)] + broadcast: bool, + + /// An optional 4 or 6 byte password, in ethernet hex format or quad-dotted decimal (e.g. "127.0.0.1" or "00:11:22:33:44:55") + #[arg(short, long, value_parser = parse_password)] + password: Option, + + /// For each target MAC address, the count of magic packets to send. count must between 1 and 5. This param must use with -i. [default: 1] + #[arg( + short, + long, + default_value_t = 1, + requires_if(ArgPredicate::IsPresent, "interval") + )] + count: u8, + + /// Wait interval milliseconds between sending each magic packet. interval must between 0 and 2000. This param must use with -c. [default: 0] + #[arg( + short, + long, + default_value_t = 0, + requires_if(ArgPredicate::IsPresent, "count") + )] + interval: u64, + + /// The flag to indicate if we should print verbose output + #[arg(short, long)] + verbose: bool, +} + +#[derive(Debug, Clone)] +struct Password(Vec); + +impl Password { + fn ref_bytes(&self) -> &Vec { + &self.0 + } +} + +#[derive(Debug)] +pub struct WolErr { + pub msg: String, + pub code: i32, +} + +impl std::error::Error for WolErr {} + +impl std::fmt::Display for WolErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Error: {}", self.msg) + } +} + +enum WolErrCode { + SocketError = 1, + InvalidArguments = 2, + UnknownError = 999, +} + +pub fn build_and_send() -> Result<(), WolErr> { + let args = WolArgs::parse(); + let target_macs = parse_target_macs(&args)?; + valide_arguments(&args)?; + let src_mac = get_interface_mac(&args.interface)?; + let mut tx = open_tx_channel(&args.interface)?; + for target_mac in target_macs { + if args.verbose { + println!( + "Building and sending packet to target mac address {}", + target_mac + .iter() + .map(|b| format!("{:02X}", b)) + .collect::>() + .join(":") + ); + } + let dst_mac = if args.broadcast { + BROADCAST_MAC + } else { + target_mac + }; + let magic_bytes = build_magic_packet(&src_mac, &dst_mac, &target_mac, &args.password)?; + send_magic_packet( + &mut tx, + magic_bytes, + &args.count, + &args.interval, + &args.verbose, + )?; + } + + Ok(()) +} + +fn valide_arguments(args: &WolArgs) -> Result<(), WolErr> { + if !is_operstate_up(&args.interface)? { + return Err(WolErr { + msg: format!( + "Invalid value for \"INTERFACE\": interface {} is not up", + args.interface + ), + code: WolErrCode::InvalidArguments as i32, + }); + } + + if args.interval > 2000 { + return Err(WolErr { + msg: String::from("Invalid value for \"INTERVAL\": interval must between 0 and 2000"), + code: WolErrCode::InvalidArguments as i32, + }); + } + + if args.count == 0 || args.count > 5 { + return Err(WolErr { + msg: String::from("Invalid value for \"COUNT\": count must between 1 and 5"), + code: WolErrCode::InvalidArguments as i32, + }); + } + + Ok(()) +} + +fn parse_mac_addr(mac_str: &str) -> Result<[u8; 6], WolErr> { + MacAddr::from_str(mac_str) + .map(|mac| mac.octets()) + .map_err(|_| WolErr { + msg: String::from("Invalid MAC address"), + code: WolErrCode::InvalidArguments as i32, + }) +} + +fn parse_ipv4_addr(ipv4_str: &str) -> Result, WolErr> { + if !is_ipv4_address_valid(ipv4_str) { + Err(WolErr { + msg: String::from("Invalid IPv4 address"), + code: WolErrCode::InvalidArguments as i32, + }) + } else { + ipv4_str + .split('.') + .map(|octet| octet.parse::()) + .collect::, _>>() + .map_err(|_| WolErr { + msg: String::from("Invalid IPv4 address"), + code: WolErrCode::InvalidArguments as i32, + }) + } +} + +fn parse_password(password: &str) -> Result { + if is_ipv4_address_valid(password) { + Ok(Password(parse_ipv4_addr(password)?)) + } else if is_mac_string_valid(password) { + parse_mac_addr(password).map(|mac| Password(mac.to_vec())) + } else { + Err(WolErr { + msg: String::from("Invalid password"), + code: WolErrCode::InvalidArguments as i32, + }) + } +} + +fn parse_target_macs(args: &WolArgs) -> Result, WolErr> { + let target_macs: Vec<&str> = args.target_mac.split(',').collect(); + let mut macs = Vec::new(); + for mac_str in target_macs { + macs.push(parse_mac_addr(mac_str)?); + } + Ok(macs) +} + +fn is_operstate_up(interface: &str) -> Result { + let state_file_path = format!("/sys/class/net/{}/operstate", interface); + match read_to_string(state_file_path) { + Ok(content) => Ok(content.trim() == "up"), + Err(_) => Err(WolErr { + msg: format!( + "Invalid value for \"INTERFACE\": invalid SONiC interface name {}", + interface + ), + code: WolErrCode::InvalidArguments as i32, + }), + } +} + +fn is_mac_string_valid(mac_str: &str) -> bool { + let mac_str = mac_str.replace(':', ""); + mac_str.len() == 12 && mac_str.chars().all(|c| c.is_ascii_hexdigit()) +} + +fn is_ipv4_address_valid(ipv4_str: &str) -> bool { + ipv4_str.split('.').count() == 4 + && ipv4_str + .split('.') + .all(|octet| octet.parse::().map_or(false, |n| n < 256)) +} + +fn get_interface_mac(interface_name: &String) -> Result<[u8; 6], WolErr> { + if let Some(interface) = datalink::interfaces() + .into_iter() + .find(|iface: &NetworkInterface| iface.name == *interface_name) + { + if let Some(mac) = interface.mac { + Ok(mac.octets()) + } else { + Err(WolErr { + msg: String::from("Could not get MAC address of target interface"), + code: WolErrCode::UnknownError as i32, + }) + } + } else { + Err(WolErr { + msg: String::from("Could not find target interface"), + code: WolErrCode::InvalidArguments as i32, + }) + } +} + +fn build_magic_packet( + src_mac: &[u8; 6], + dst_mac: &[u8; 6], + target_mac: &[u8; 6], + password: &Option, +) -> Result, WolErr> { + let password_len = password.as_ref().map_or(0, |p| p.ref_bytes().len()); + let mut pkt = vec![0u8; 116 + password_len]; + pkt[0..6].copy_from_slice(dst_mac); + pkt[6..12].copy_from_slice(src_mac); + pkt[12..14].copy_from_slice(&[0x08, 0x42]); + pkt[14..20].copy_from_slice(&[0xff; 6]); + pkt[20..116].copy_from_slice(&target_mac.repeat(16)); + if let Some(p) = password { + pkt[116..116 + password_len].copy_from_slice(p.ref_bytes()); + } + Ok(pkt) +} + +fn send_magic_packet( + tx: &mut Box, + packet: Vec, + count: &u8, + interval: &u64, + verbose: &bool, +) -> Result<(), WolErr> { + for nth in 0..*count { + match tx.send_to(&packet, None) { + Some(Ok(_)) => {} + Some(Err(e)) => { + return Err(WolErr { + msg: format!("Network is down: {}", e), + code: WolErrCode::SocketError as i32, + }); + } + None => { + return Err(WolErr { + msg: String::from("Network is down"), + code: WolErrCode::SocketError as i32, + }); + } + } + if *verbose { + println!( + " | -> Sent the {}th packet and sleep for {} seconds", + &nth + 1, + &interval + ); + println!( + " | -> Packet bytes in hex {}", + &packet + .iter() + .fold(String::new(), |acc, b| acc + &format!("{:02X}", b)) + ) + } + thread::sleep(Duration::from_millis(*interval)); + } + Ok(()) +} + +fn open_tx_channel(interface: &str) -> Result, WolErr> { + if let Some(interface) = datalink::interfaces() + .into_iter() + .find(|iface: &NetworkInterface| iface.name == interface) + { + match datalink::channel(&interface, Default::default()) { + Ok(Ethernet(tx, _)) => Ok(tx), + Ok(_) => Err(WolErr { + msg: String::from("Network is down"), + code: WolErrCode::SocketError as i32, + }), + Err(e) => Err(WolErr { + msg: format!("Network is down: {}", e), + code: WolErrCode::SocketError as i32, + }), + } + } else { + Err(WolErr { + msg: format!( + "Invalid value for \"INTERFACE\": interface {} is not up", + interface + ), + code: WolErrCode::InvalidArguments as i32, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_mac_addr() { + let mac_str = "00:11:22:33:44:55"; + let mac = parse_mac_addr(mac_str).unwrap(); + assert_eq!(mac, [0x00, 0x11, 0x22, 0x33, 0x44, 0x55]); + + let mac_str = "00:11:22:33:44:GG"; + assert!(parse_mac_addr(mac_str).is_err()); + assert_eq!( + parse_mac_addr(mac_str).unwrap_err().msg, + "Invalid MAC address" + ); + assert_eq!( + parse_mac_addr(mac_str).unwrap_err().code, + WolErrCode::InvalidArguments as i32 + ); + + let mac_str = "00-01-22-33-44-55"; + assert!(parse_mac_addr(mac_str).is_err()); + assert_eq!( + parse_mac_addr(mac_str).unwrap_err().msg, + "Invalid MAC address" + ); + assert_eq!( + parse_mac_addr(mac_str).unwrap_err().code, + WolErrCode::InvalidArguments as i32 + ); + } + + #[test] + fn test_parse_ipv4_addr() { + let ipv4_str = "127.0.0.1"; + let ipv4 = parse_ipv4_addr(ipv4_str).unwrap(); + assert_eq!(ipv4, [127, 0, 0, 1]); + + let ipv4_str = "127.0.0.256"; + assert!(parse_ipv4_addr(ipv4_str).is_err()); + assert_eq!( + parse_ipv4_addr(ipv4_str).unwrap_err().msg, + "Invalid IPv4 address" + ); + assert_eq!( + parse_ipv4_addr(ipv4_str).unwrap_err().code, + WolErrCode::InvalidArguments as i32 + ); + + let ipv4_str = "127.0.0"; + assert!(parse_ipv4_addr(ipv4_str).is_err()); + assert_eq!( + parse_ipv4_addr(ipv4_str).unwrap_err().msg, + "Invalid IPv4 address" + ); + assert_eq!( + parse_ipv4_addr(ipv4_str).unwrap_err().code, + WolErrCode::InvalidArguments as i32 + ); + + let ipv4_str = "::1"; + assert!(parse_ipv4_addr(ipv4_str).is_err()); + assert_eq!( + parse_ipv4_addr(ipv4_str).unwrap_err().msg, + "Invalid IPv4 address" + ); + assert_eq!( + parse_ipv4_addr(ipv4_str).unwrap_err().code, + WolErrCode::InvalidArguments as i32 + ); + } + + #[test] + fn test_parse_password() { + let password_str = "127.0.0.1"; + let password = parse_password(password_str); + assert_eq!(*password.unwrap().ref_bytes(), [127, 0, 0, 1]); + + let password_str = "00:11:22:33:44:55"; + let password = parse_password(password_str); + assert_eq!(*password.unwrap().ref_bytes(), [0, 17, 34, 51, 68, 85]); + + let password_str = "127.0.0.256"; + assert!(parse_password(password_str).is_err()); + assert_eq!( + parse_password(password_str).unwrap_err().code, + WolErrCode::InvalidArguments as i32 + ); + + let password_str = "127.0.0"; + assert!(parse_password(password_str).is_err()); + assert_eq!( + parse_password(password_str).unwrap_err().code, + WolErrCode::InvalidArguments as i32 + ); + + let password_str = "::1"; + assert!(parse_password(password_str).is_err()); + assert_eq!( + parse_password(password_str).unwrap_err().code, + WolErrCode::InvalidArguments as i32 + ); + + let password_str = "00:11:22:33:44:GG"; + assert!(parse_password(password_str).is_err()); + assert_eq!( + parse_password(password_str).unwrap_err().code, + WolErrCode::InvalidArguments as i32 + ); + + let password_str = "00-01-22-33-44-55"; + assert!(parse_password(password_str).is_err()); + assert_eq!( + parse_password(password_str).unwrap_err().code, + WolErrCode::InvalidArguments as i32 + ); + } + + #[test] + fn test_parse_target_macs() { + let mut args = WolArgs { + interface: "Ethernet10".to_string(), + target_mac: "00:11:22:33:44:55".to_string(), + broadcast: false, + password: None, + count: 1, + interval: 0, + verbose: false, + }; + let target_macs = parse_target_macs(&args).unwrap(); + assert_eq!(target_macs.len(), 1); + assert_eq!(target_macs[0], [0x00, 0x11, 0x22, 0x33, 0x44, 0x55]); + + args.target_mac = "00:11:22:33:44:55,11:22:33:44:55:66,22:33:44:55:66:77".to_string(); + let target_macs = parse_target_macs(&args).unwrap(); + assert_eq!(target_macs.len(), 3); + assert_eq!(target_macs[0], [0x00, 0x11, 0x22, 0x33, 0x44, 0x55]); + assert_eq!(target_macs[1], [0x11, 0x22, 0x33, 0x44, 0x55, 0x66]); + assert_eq!(target_macs[2], [0x22, 0x33, 0x44, 0x55, 0x66, 0x77]); + + args.target_mac = "00:01".to_string(); + assert!(parse_target_macs(&args).is_err()); + assert_eq!( + parse_target_macs(&args).unwrap_err().msg, + "Invalid MAC address" + ); + assert_eq!( + parse_target_macs(&args).unwrap_err().code, + WolErrCode::InvalidArguments as i32 + ); + } + + #[test] + fn test_is_mac_string_valid() { + assert!(is_mac_string_valid("00:11:22:33:44:55")); + assert!(!is_mac_string_valid("")); + assert!(!is_mac_string_valid("0:1:2:3:4:G")); + assert!(!is_mac_string_valid("00:11:22:33:44:GG")); + assert!(!is_mac_string_valid("00-11-22-33-44-55")); + assert!(!is_mac_string_valid("00:11:22:33:44:55:66")); + } + + #[test] + fn test_is_ipv4_address_valid() { + assert!(is_ipv4_address_valid("192.168.1.1")); + assert!(!is_ipv4_address_valid("")); + assert!(!is_ipv4_address_valid("0::1")); + assert!(!is_ipv4_address_valid("192.168.1")); + assert!(!is_ipv4_address_valid("192.168.1.256")); + assert!(!is_ipv4_address_valid("192.168.1.1.1")); + } + + #[test] + fn test_build_magic_packet() { + let src_mac = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55]; + let target_mac = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff]; + let four_bytes_password = Some(Password(vec![0x00, 0x11, 0x22, 0x33])); + let magic_packet = + build_magic_packet(&src_mac, &target_mac, &target_mac, &four_bytes_password).unwrap(); + assert_eq!(magic_packet.len(), 120); + assert_eq!(&magic_packet[0..6], &target_mac); + assert_eq!(&magic_packet[6..12], &src_mac); + assert_eq!(&magic_packet[12..14], &[0x08, 0x42]); + assert_eq!(&magic_packet[14..20], &[0xff; 6]); + assert_eq!(&magic_packet[20..116], target_mac.repeat(16)); + assert_eq!(&magic_packet[116..120], &[0x00, 0x11, 0x22, 0x33]); + let six_bytes_password = Some(Password(vec![0x00, 0x11, 0x22, 0x33, 0x44, 0x55])); + let magic_packet = + build_magic_packet(&src_mac, &target_mac, &target_mac, &six_bytes_password).unwrap(); + assert_eq!(magic_packet.len(), 122); + assert_eq!(&magic_packet[0..6], &target_mac); + assert_eq!(&magic_packet[6..12], &src_mac); + assert_eq!(&magic_packet[12..14], &[0x08, 0x42]); + assert_eq!(&magic_packet[14..20], &[0xff; 6]); + assert_eq!(&magic_packet[20..116], target_mac.repeat(16)); + assert_eq!( + &magic_packet[116..122], + &[0x00, 0x11, 0x22, 0x33, 0x44, 0x55] + ); + } + + #[test] + fn test_build_magic_packet_without_password() { + let src_mac = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55]; + let dst_mac = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff]; + let target_mac = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06]; + let magic_packet = build_magic_packet(&src_mac, &dst_mac, &target_mac, &None).unwrap(); + assert_eq!(magic_packet.len(), 116); + assert_eq!(&magic_packet[0..6], &dst_mac); + assert_eq!(&magic_packet[6..12], &src_mac); + assert_eq!(&magic_packet[12..14], &[0x08, 0x42]); + assert_eq!(&magic_packet[14..20], &[0xff; 6]); + assert_eq!(&magic_packet[20..116], target_mac.repeat(16)); + } + + #[test] + fn verify_args_parse() { + // Interface and target mac are required + let result = WolArgs::try_parse_from(&["wol", "eth0", "00:11:22:33:44:55"]); + assert!(result.as_ref().is_ok_and(|a| a.interface == "eth0")); + assert!(result.is_ok_and(|a| a.target_mac == "00:11:22:33:44:55")); + let result = WolArgs::try_parse_from(&["wol"]); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "error: the following required arguments were not provided:\n \n \n\nUsage: wol \n\nFor more information, try '--help'.\n" + ); + // Mac address should valid + let args = + WolArgs::try_parse_from(&["wol", "Ethernet10", "00:11:22:33:44:55,00:01:02:03:04:05"]) + .unwrap(); + let macs = parse_target_macs(&args).unwrap(); + assert_eq!(macs.len(), 2); + assert_eq!(macs[0], [0x00, 0x11, 0x22, 0x33, 0x44, 0x55]); + assert_eq!(macs[1], [0x00, 0x01, 0x02, 0x03, 0x04, 0x05]); + let args = WolArgs::try_parse_from(&["wol", "Ethernet10", "00:11:22:33:44:GG"]).unwrap(); + let result: Result, WolErr> = parse_target_macs(&args); + assert!(result.is_err()); + assert_eq!(result.as_ref().unwrap_err().msg, "Invalid MAC address"); + assert_eq!( + result.unwrap_err().code, + WolErrCode::InvalidArguments as i32 + ); + // Password can be set + let args = WolArgs::try_parse_from(&[ + "wol", + "eth0", + "00:01:02:03:04:05", + "-b", + "-p", + "192.168.0.0", + ]) + .unwrap(); + assert_eq!(args.password.unwrap().ref_bytes(), &[192, 168, 0, 0]); + let args = WolArgs::try_parse_from(&[ + "wol", + "eth0", + "00:01:02:03:04:05", + "-b", + "-p", + "00:01:02:03:04:05", + ]) + .unwrap(); + assert_eq!(args.password.unwrap().ref_bytes(), &[0, 1, 2, 3, 4, 5]); + let result = WolArgs::try_parse_from(&["wol", "eth0", "-b", "-p", "xxx"]); + assert!(result.is_err()); + assert_eq!(result.unwrap_err().to_string(), "error: invalid value 'xxx' for '--password ': Error: Invalid password\n\nFor more information, try '--help'.\n"); + // Count should be between 1 and 5 + let args = WolArgs::try_parse_from(&["wol", "eth0", "00:01:02:03:04:05", "-b"]).unwrap(); + assert_eq!(args.count, 1); // default value + let args = WolArgs::try_parse_from(&[ + "wol", + "eth0", + "00:01:02:03:04:05", + "-b", + "-c", + "5", + "-i", + "0", + ]) + .unwrap(); + assert_eq!(args.count, 5); + let args = WolArgs::try_parse_from(&[ + "wol", + "eth0", + "00:01:02:03:04:05", + "-b", + "-c", + "0", + "-i", + "0", + ]); + let result = valide_arguments(&args.unwrap()); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Error: Invalid value for \"COUNT\": count must between 1 and 5" + ); + // Interval should be between 0 and 2000 + let args = WolArgs::try_parse_from(&["wol", "eth0", "00:01:02:03:04:05", "-b"]).unwrap(); + assert_eq!(args.interval, 0); // default value + let args = WolArgs::try_parse_from(&[ + "wol", + "eth0", + "00:01:02:03:04:05", + "-b", + "-i", + "2000", + "-c", + "0", + ]) + .unwrap(); + assert_eq!(args.interval, 2000); + let args = WolArgs::try_parse_from(&[ + "wol", + "eth0", + "00:01:02:03:04:05", + "-b", + "-i", + "2001", + "-c", + "0", + ]); + let result = valide_arguments(&args.unwrap()); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "Error: Invalid value for \"INTERVAL\": interval must between 0 and 2000" + ); + // Interval and count should specified together + let result = WolArgs::try_parse_from(&["wol", "eth0", "00:01:02:03:04:05", "-i", "2000"]); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "error: the following required arguments were not provided:\n --count \n\nUsage: wol --interval --count \n\nFor more information, try '--help'.\n" + ); + let result = WolArgs::try_parse_from(&["wol", "eth0", "00:01:02:03:04:05", "-c", "1"]); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err().to_string(), + "error: the following required arguments were not provided:\n --interval \n\nUsage: wol --count --interval \n\nFor more information, try '--help'.\n" + ); + // Verbose can be set + let args = + WolArgs::try_parse_from(&["wol", "eth0", "00:01:02:03:04:05", "-b", "--verbose"]) + .unwrap(); + assert_eq!(args.verbose, true); + } +}