From 23f3aefa1dda453d3e3b0bd5faa11adcaff32d3c Mon Sep 17 00:00:00 2001 From: Maksym Pavlenko Date: Tue, 19 Jan 2021 09:44:50 -0800 Subject: [PATCH] agent: Implement new netlink module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds new netlink module (based on `rtnetlink` crate), so we don’t have to write a low level code to interact with netlink sockets, but use a high level API. As a side effect, `rtnetlink` crate got full IPv6 support, so it fixes #1171 Fixes: #1294 Signed-off-by: Maksym Pavlenko --- src/agent/Cargo.lock | 364 ++++++++----- src/agent/Cargo.toml | 3 + src/agent/src/main.rs | 1 + src/agent/src/netlink2.rs | 1020 +++++++++++++++++++++++++++++++++++++ 4 files changed, 1272 insertions(+), 116 deletions(-) create mode 100644 src/agent/src/netlink2.rs diff --git a/src/agent/Cargo.lock b/src/agent/Cargo.lock index 9118d2665f..05bf8d6edd 100644 --- a/src/agent/Cargo.lock +++ b/src/agent/Cargo.lock @@ -2,9 +2,9 @@ # It is not intended for manual editing. [[package]] name = "addr2line" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072" +checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423" dependencies = [ "gimli", ] @@ -23,24 +23,24 @@ checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "aho-corasick" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b476ce7103678b0c6d3d395dbbae31d48ff910bd28be979ba5d48c6351131d0d" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" dependencies = [ "memchr", ] [[package]] name = "anyhow" -version = "1.0.33" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1fd36ffbb1fb7c834eac128ea8d0e310c5aeb635548f9d58861e1308d46e71c" +checksum = "68803225a7b13e47191bab76f2687382b60d259e8cf37f6e1893658b84bb9479" [[package]] name = "arc-swap" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" +checksum = "dabe5a181f83789739c194cbe5a897dde195078fac08568d09221fd6137a7ba8" [[package]] name = "arrayref" @@ -50,9 +50,9 @@ checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" [[package]] name = "arrayvec" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "async-trait" @@ -61,8 +61,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.45", + "quote 1.0.8", + "syn 1.0.55", ] [[package]] @@ -73,9 +73,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "backtrace" -version = "0.3.53" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707b586e0e2f247cbde68cdd2c3ce69ea7b7be43e1c5b426e37c9319c4b9838e" +checksum = "ef5140344c85b01f9bbb4d4b7288a8aa4b3287ccef913a14bcc78a1063623598" dependencies = [ "addr2line", "cfg-if 1.0.0", @@ -87,9 +87,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.12.3" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bitflags" @@ -99,9 +99,9 @@ checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] name = "blake2b_simd" -version = "0.5.10" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" dependencies = [ "arrayref", "arrayvec", @@ -114,6 +114,15 @@ version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +[[package]] +name = "byteordered" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32687ee8ab498526e3ef07dfbede151650ce202dc83c53494645a24677d89b37" +dependencies = [ + "byteorder", +] + [[package]] name = "bytes" version = "0.4.12" @@ -143,9 +152,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.61" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed67cbde08356238e75fc4656be4749481eeffb09e19f320a25237d5221c985d" +checksum = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48" [[package]] name = "cfg-if" @@ -185,15 +194,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "cloudabi" -version = "0.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" -dependencies = [ - "bitflags", -] - [[package]] name = "constant_time_eq" version = "0.1.5" @@ -202,11 +202,11 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" [[package]] name = "crc32fast" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", ] [[package]] @@ -215,7 +215,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.7.2", "maybe-uninit", ] @@ -230,6 +230,17 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d96d1e189ef58269ebe5b97953da3274d83a93af647c2ddd6f9dab28cedb8d" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "lazy_static", +] + [[package]] name = "derive-new" version = "0.5.8" @@ -237,8 +248,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71f31892cd5c62e414316f2963c5689242c43d8e7bbcaaeca97e5e28c95d91d9" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.45", + "quote 1.0.8", + "syn 1.0.55", ] [[package]] @@ -269,9 +280,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "errno" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eab5ee3df98a279d9b316b1af6ac95422127b1290317e6d18c1743c99418b01" +checksum = "fa68f2fb9cae9d37c9b2b3584aba698a2e97f72d7aef7b9f7aa71d8b54ce46fe" dependencies = [ "errno-dragonfly", "libc", @@ -390,8 +401,8 @@ checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556" dependencies = [ "proc-macro-hack", "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.45", + "quote 1.0.8", + "syn 1.0.55", ] [[package]] @@ -448,15 +459,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724" +checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" [[package]] name = "heck" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +checksum = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac" dependencies = [ "unicode-segmentation", ] @@ -498,6 +509,15 @@ dependencies = [ "libc", ] +[[package]] +name = "instant" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +dependencies = [ + "cfg-if 1.0.0", +] + [[package]] name = "iovec" version = "0.1.4" @@ -507,6 +527,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ipnetwork" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c3eaab3ac0ede60ffa41add21970a7df7d91772c03383aac6c2c3d53cc716b" +dependencies = [ + "serde", +] + [[package]] name = "itertools" version = "0.8.2" @@ -530,12 +559,14 @@ dependencies = [ "async-trait", "cgroups-rs", "futures", + "ipnetwork", "lazy_static", "libc", "log", "logging", "netlink", - "netlink-sys", + "netlink-packet-utils", + "netlink-sys 0.4.0", "nix 0.17.0", "oci", "prctl", @@ -544,6 +575,7 @@ dependencies = [ "protobuf", "protocols", "regex", + "rtnetlink", "rustjail", "scan_fmt", "scopeguard", @@ -576,15 +608,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.79" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2448f6066e80e3bfc792e9c98bf705b4b0fc6e8ef5b43e5889aff0eaa9c58743" +checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" [[package]] name = "libflate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9bac9023e1db29c084f9f8cd9d3852e5e8fddf98fb47c4964a0ea4663d95949" +checksum = "389de7875e06476365974da3e7ff85d55f1972188ccd9f6020dd7c8156e17914" dependencies = [ "adler32", "crc32fast", @@ -600,9 +632,9 @@ checksum = "3286f09f7d4926fc486334f28d8d2e6ebe4f7f9994494b6dab27ddfad2c9b11b" [[package]] name = "lock_api" -version = "0.3.4" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +checksum = "dd96ffd135b2fd7b973ac026d28085defbe8983df057ced3eb4f2130b0831312" dependencies = [ "scopeguard", ] @@ -635,9 +667,9 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "memchr" -version = "2.3.3" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] name = "miniz_oxide" @@ -755,6 +787,59 @@ dependencies = [ "slog-scope", ] +[[package]] +name = "netlink-packet-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "559c08b3ea479ace788a8f75388e3db15210561a4c0f37c4ec7bdf40a3be564e" +dependencies = [ + "anyhow", + "byteorder", + "libc", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2253105e60b35a3fb6cf342b56a45ee1c76ef4b1e68c59b08f813f24c3b7b469" +dependencies = [ + "anyhow", + "bitflags", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2afb159d0e3ac700e85f0df25b8438b99d43ed0c0b685242fcdf1b5673e54d" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror", +] + +[[package]] +name = "netlink-proto" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31dfd4f1653ba8e1e2410b3def2313f3399d9b9f7ec3a8a6a8f2f670c3e58d71" +dependencies = [ + "bytes 0.5.6", + "futures", + "log", + "netlink-packet-core", + "netlink-sys 0.5.0", + "tokio 0.2.24", + "tokio-util", +] + [[package]] name = "netlink-sys" version = "0.4.0" @@ -768,6 +853,19 @@ dependencies = [ "tokio 0.2.24", ] +[[package]] +name = "netlink-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf10c3ab67b9c09b42abb5a53ecb8ffdad160d6485b140a6f21f53ba5362042d" +dependencies = [ + "futures", + "libc", + "log", + "mio 0.6.23", + "tokio 0.2.24", +] + [[package]] name = "nix" version = "0.16.1" @@ -808,13 +906,13 @@ dependencies = [ [[package]] name = "nix" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85db2feff6bf70ebc3a4793191517d5f0331100a2f10f9bf93b5e5214f32b7b7" +checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2" dependencies = [ "bitflags", "cc", - "cfg-if 0.1.10", + "cfg-if 1.0.0", "libc", ] @@ -835,9 +933,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ "autocfg", "num-traits", @@ -845,9 +943,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", ] @@ -864,9 +962,9 @@ dependencies = [ [[package]] name = "object" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37fd5004feb2ce328a52b0b3d01dbf4ffff72583493900ed15f22d4111c51693" +checksum = "8d3b63360ec3cb337817c2dbd47ab4a0f170d285d8e5a2064600f3def1402397" [[package]] name = "oci" @@ -886,28 +984,35 @@ checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" [[package]] name = "parking_lot" -version = "0.10.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" dependencies = [ + "instant", "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.7.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272" dependencies = [ - "cfg-if 0.1.10", - "cloudabi", + "cfg-if 1.0.0", + "instant", "libc", "redox_syscall", "smallvec", "winapi 0.3.9", ] +[[package]] +name = "paste" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d65c4d95931acda4498f675e332fcbdc9a06705cd07086c510e9b6009cd1c1" + [[package]] name = "path-absolutize" version = "1.2.1" @@ -952,8 +1057,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.45", + "quote 1.0.8", + "syn 1.0.55", ] [[package]] @@ -976,9 +1081,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "ppv-lite86" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" +checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "prctl" @@ -987,7 +1092,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "059a34f111a9dee2ce1ac2826a68b24601c4298cfeb1a587c3cb493d5ab46f52" dependencies = [ "libc", - "nix 0.19.0", + "nix 0.19.1", ] [[package]] @@ -1161,9 +1266,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" dependencies = [ "proc-macro2 1.0.24", ] @@ -1228,9 +1333,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8963b85b8ce3074fecffde43b4b0dded83ce2f367dc8d363afc56679f3ee820b" +checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" dependencies = [ "aho-corasick", "memchr", @@ -1240,9 +1345,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.20" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cab7a364d15cde1e505267766a2d3c4e22a843e1a601f0fa7564c0f82ced11c" +checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" [[package]] name = "remove_dir_all" @@ -1260,15 +1365,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cabe4fa914dec5870285fa7f71f602645da47c486e68486d2b4ceb4a343e90ac" [[package]] -name = "rust-argon2" -version = "0.8.2" +name = "rtnetlink" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dab61250775933275e84053ac235621dfb739556d5c54a2f2e9313b7cf43a19" +checksum = "0c942df3c7725a0500971d857a080d6dc537e257e19ccb352f80b2c726ef7007" +dependencies = [ + "byteordered", + "futures", + "log", + "netlink-packet-route", + "netlink-proto", + "thiserror", +] + +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" dependencies = [ "base64", "blake2b_simd", "constant_time_eq", - "crossbeam-utils", + "crossbeam-utils 0.8.1", ] [[package]] @@ -1357,26 +1476,26 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" [[package]] name = "serde_derive" -version = "1.0.117" +version = "1.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd1ae72adb44aab48f325a02444a5fc079349a8d804c1fc922aed3f7454c74e" +checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.45", + "quote 1.0.8", + "syn 1.0.55", ] [[package]] name = "serde_json" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" +checksum = "1500e84d27fe482ed1dc791a56eddc2f230046a040fa908c08bda1d9fb615779" dependencies = [ "itoa", "ryu", @@ -1385,9 +1504,9 @@ dependencies = [ [[package]] name = "serial_test" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b15f74add9a9d4a3eb2bf739c9a427d266d3895b53d992c3a7c234fec2ff1f1" +checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" dependencies = [ "lazy_static", "parking_lot", @@ -1396,13 +1515,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65f59259be9fc1bf677d06cc1456e97756004a1a5a577480f71430bd7c17ba33" +checksum = "b2acd6defeddb41eb60bb468f8825d0cfd0c2a76bc03bfd235b6a1dc4f6a1ad5" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.45", + "quote 1.0.8", + "syn 1.0.55", ] [[package]] @@ -1417,11 +1536,10 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e12110bc539e657a646068aaf5eb5b63af9d0c1f7b29c97113fad80e15f035" +checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab" dependencies = [ - "arc-swap", "libc", ] @@ -1439,9 +1557,9 @@ checksum = "2f7fb98e76e2022054673f3ebc43a4e12890ec6272530629df6237cafbb70569" [[package]] name = "slog" -version = "2.5.2" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc9c640a4adbfbcc11ffb95efe5aa7af7309e002adab54b185507dbf2377b99" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" [[package]] name = "slog-async" @@ -1491,9 +1609,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.4.2" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" +checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75" [[package]] name = "socket2" @@ -1525,12 +1643,12 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.45" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9c5432ff16d6152371f808fb5a871cd67368171b09bb21b43df8e4a47a3556" +checksum = "a571a711dddd09019ccc628e1b17fe87c59b09d513c06c026877aa708334f37a" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", + "quote 1.0.8", "unicode-xid 0.2.1", ] @@ -1556,22 +1674,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "318234ffa22e0920fe9a40d7b8369b5f649d490980cf7aadcf1eb91594869b42" +checksum = "0e9ae34b84616eedaaf1e9dd6026dbe00dcafa92aa0c8077cb69df1fcfe5e53e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cae2447b6282786c3493999f40a9be2a6ad20cb8bd268b0a0dbf5a065535c0ab" +checksum = "9ba20f23e85b10754cd195504aebf6a27e2e6cbe28c17778a0c930724628dd56" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.45", + "quote 1.0.8", + "syn 1.0.55", ] [[package]] @@ -1636,8 +1754,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e44da00bfc73a25f814cd8d7e57a68a5c31b74b3152a0a1d1f590c97ed06265a" dependencies = [ "proc-macro2 1.0.24", - "quote 1.0.7", - "syn 1.0.45", + "quote 1.0.8", + "syn 1.0.55", +] + +[[package]] +name = "tokio-util" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "571da51182ec208780505a32528fc5512a8fe1443ab960b3f2f3ef093cd16930" +dependencies = [ + "bytes 0.5.6", + "futures-core", + "futures-sink", + "log", + "pin-project-lite 0.1.11", + "tokio 0.2.24", ] [[package]] @@ -1704,9 +1836,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" +checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" [[package]] name = "unicode-xid" diff --git a/src/agent/Cargo.toml b/src/agent/Cargo.toml index cde97aba50..4798aacd50 100644 --- a/src/agent/Cargo.toml +++ b/src/agent/Cargo.toml @@ -27,6 +27,9 @@ tokio = { version = "0.2", features = ["rt-core", "sync", "uds", "stream", "macr futures = "0.3" netlink-sys = { version = "0.4.0", features = ["tokio_socket",]} tokio-vsock = "0.2.2" +rtnetlink = "0.6.0" +netlink-packet-utils = "0.4.0" +ipnetwork = "0.17.0" # slog: # - Dynamic keys required to allow HashMap keys to be slog::Serialized. diff --git a/src/agent/src/main.rs b/src/agent/src/main.rs index 2073d1b2b3..2a13d16ae1 100644 --- a/src/agent/src/main.rs +++ b/src/agent/src/main.rs @@ -55,6 +55,7 @@ mod linux_abi; mod metrics; mod mount; mod namespace; +mod netlink2; mod network; pub mod random; mod sandbox; diff --git a/src/agent/src/netlink2.rs b/src/agent/src/netlink2.rs new file mode 100644 index 0000000000..202f1a9901 --- /dev/null +++ b/src/agent/src/netlink2.rs @@ -0,0 +1,1020 @@ +use anyhow::Context; +use anyhow::{anyhow, Result}; +use futures::{future, StreamExt, TryStreamExt}; +use ipnetwork::{IpNetwork, Ipv4Network, Ipv6Network}; +use protobuf::RepeatedField; +use protocols::types::{ARPNeighbor, IPAddress, IPFamily, Interface, Route}; +use rtnetlink::{new_connection, packet, IpVersion}; +use std::convert::{TryFrom, TryInto}; +use std::fmt; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::ops::Deref; +use std::str::{self, FromStr}; + +/// Search criteria to use when looking for a link in `find_link`. +pub enum LinkFilter<'a> { + /// Find by link name. + Name(&'a str), + /// Find by link index. + Index(u32), + /// Find by MAC address. + Address(&'a str), +} + +impl fmt::Display for LinkFilter<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LinkFilter::Name(name) => write!(f, "Name: {}", name), + LinkFilter::Index(idx) => write!(f, "Index: {}", idx), + LinkFilter::Address(addr) => write!(f, "Address: {}", addr), + } + } +} + +/// A filter to query addresses. +pub enum AddressFilter { + /// Return addresses that belong to the given interface. + LinkIndex(u32), + /// Get addresses with the given prefix. + IpAddress(IpAddr), +} + +/// A high level wrapper for netlink (and `rtnetlink` crate) for use by the Agent's RPC. +/// It is expected to be consumed by the `agentService`, so it operates with protobuf +/// structures directly for convenience. +#[derive(Debug)] +pub struct Handle { + handle: rtnetlink::Handle, +} + +impl Handle { + pub(crate) fn new() -> Result { + let (conn, handle, _) = new_connection()?; + tokio::spawn(conn); + + Ok(Handle { handle }) + } + + pub async fn update_interface(&mut self, iface: &Interface) -> Result<()> { + // The reliable way to find link is using hardware address + // as filter. However, hardware filter might not be supported + // by netlink, we may have to dump link list and the find the + // target link. filter using name or family is supported, but + // we cannot use that to find target link. + // let's try if hardware address filter works. -_- + let link = self.find_link(LinkFilter::Address(&iface.hwAddr)).await?; + + // Bring down interface if it is UP + if link.is_up() { + self.enable_link(link.index(), false).await?; + } + + // Delete all addresses associated with the link + let addresses = self + .list_addresses(AddressFilter::LinkIndex(link.index())) + .await?; + self.delete_addresses(addresses).await?; + + // Add new ip addresses from request + for ip_address in &iface.IPAddresses { + let ip = IpAddr::from_str(&ip_address.get_address())?; + let mask = u8::from_str_radix(ip_address.get_mask(), 10)?; + + self.add_addresses(link.index(), std::iter::once(IpNetwork::new(ip, mask)?)) + .await?; + } + + // Update link + let mut request = self.handle.link().set(link.index()); + request.message_mut().header = link.header.clone(); + + request + .mtu(iface.mtu as _) + .name(iface.name.clone()) + .arp(iface.raw_flags & libc::IFF_NOARP as u32 == 0) + .up() + .execute() + .await?; + + Ok(()) + } + + pub async fn handle_localhost(&self) -> Result<()> { + let link = self.find_link(LinkFilter::Name("lo")).await?; + self.enable_link(link.index(), true).await?; + Ok(()) + } + + pub async fn update_routes(&mut self, list: I) -> Result<()> + where + I: IntoIterator, + { + let old_routes = self + .query_routes(None) + .await + .with_context(|| "Failed to query old routes")?; + + self.delete_routes(old_routes) + .await + .with_context(|| "Failed to delete old routes")?; + + self.add_routes(list) + .await + .with_context(|| "Failed to add new routes")?; + + Ok(()) + } + + /// Retireve available network interfaces. + pub async fn list_interfaces(&self) -> Result> { + let mut list = Vec::new(); + + let links = self.list_links().await?; + + for link in &links { + let mut iface = Interface { + name: link.name(), + hwAddr: link.address(), + mtu: link.mtu().unwrap_or(0), + ..Default::default() + }; + + let ips = self + .list_addresses(AddressFilter::LinkIndex(link.index())) + .await? + .into_iter() + .map(|p| p.try_into()) + .collect::>>()?; + + iface.IPAddresses = RepeatedField::from_vec(ips); + + list.push(iface); + } + + Ok(list) + } + + async fn find_link(&self, filter: LinkFilter<'_>) -> Result { + let request = self.handle.link().get(); + + let filtered = match filter { + LinkFilter::Name(name) => request.set_name_filter(name.to_owned()), + LinkFilter::Index(index) => request.match_index(index), + _ => request, // Post filters + }; + + let mut stream = filtered.execute(); + + let next = if let LinkFilter::Address(addr) = filter { + use packet::link::nlas::Nla; + + let mac_addr = parse_mac_address(addr) + .with_context(|| format!("Failed to parse MAC address: {}", addr))?; + + // Hardware filter might not be supported by netlink, + // we may have to dump link list and the find the target link. + stream + .try_filter(|f| { + let result = f.nlas.iter().any(|n| match n { + Nla::Address(data) => data.eq(&mac_addr), + _ => false, + }); + + future::ready(result) + }) + .try_next() + .await? + } else { + stream.try_next().await? + }; + + next.map(|msg| msg.into()) + .ok_or(anyhow!("Link not found ({})", filter)) + } + + async fn list_links(&self) -> Result> { + let result = self + .handle + .link() + .get() + .execute() + .try_filter_map(|msg| future::ready(Ok(Some(msg.into())))) // Don't filter, just map + .try_collect::>() + .await?; + Ok(result) + } + + pub async fn enable_link(&self, link_index: u32, up: bool) -> Result<()> { + let link_req = self.handle.link().set(link_index); + let set_req = if up { link_req.up() } else { link_req.down() }; + set_req.execute().await?; + Ok(()) + } + + pub async fn delete_links(&mut self, list: I) -> Result<()> + where + I: IntoIterator, + { + for index in list.into_iter() { + self.handle.link().del(index).execute().await?; + } + + Ok(()) + } + + async fn query_routes( + &self, + ip_version: Option, + ) -> Result> { + let list = if let Some(ip_version) = ip_version { + self.handle + .route() + .get(ip_version) + .execute() + .try_collect() + .await? + } else { + // These queries must be executed sequentially, otherwise + // it'll throw "Device or resource busy (os error 16)" + let routes4 = self + .handle + .route() + .get(IpVersion::V4) + .execute() + .try_collect::>() + .await + .with_context(|| "Failed to query IP v4 routes")?; + + let routes6 = self + .handle + .route() + .get(IpVersion::V6) + .execute() + .try_collect::>() + .await + .with_context(|| "Failed to query IP v6 routes")?; + + [routes4, routes6].concat() + }; + + Ok(list) + } + + pub async fn list_routes(&self) -> Result> { + let mut result = Vec::new(); + + for msg in self.query_routes(None).await? { + // Ignore non-main tables + if msg.header.table != packet::constants::RT_TABLE_MAIN { + continue; + } + + let mut route = Route { + scope: msg.header.scope as _, + ..Default::default() + }; + + if let Some((ip, mask)) = msg.destination_prefix() { + route.dest = format!("{}/{}", ip, mask); + } + + if let Some((ip, mask)) = msg.source_prefix() { + route.source = format!("{}/{}", ip, mask); + } + + if let Some(addr) = msg.gateway() { + route.gateway = addr.to_string(); + + // For gateway, destination is 0.0.0.0 + route.dest = if addr.is_ipv4() { + String::from("0.0.0.0") + } else { + String::from("::1") + } + } + + if let Some(index) = msg.output_interface() { + route.device = self.find_link(LinkFilter::Index(index)).await?.name(); + } + + result.push(route); + } + + Ok(result) + } + + /// Adds a list of routes from iterable object `I`. + /// It can accept both a collection of routes or a single item (via `iter::once()`). + /// It'll also take care of proper order when adding routes (gateways first, everything else after). + async fn add_routes(&mut self, list: I) -> Result<()> + where + I: IntoIterator, + { + // Split the list so we add routes with no gateway first. + // Note: `partition_in_place` is a better fit here, since it reorders things inplace (instead of + // allocating two separate collections), however it's not yet in stable Rust. + let (a, b): (Vec, Vec) = list.into_iter().partition(|p| p.gateway.len() == 0); + let list = a.iter().chain(&b); + + for route in list { + let link = self.find_link(LinkFilter::Name(&route.device)).await?; + let is_v6 = route.gateway.contains("::") || route.dest.contains("::"); + + const MAIN_TABLE: u8 = packet::constants::RT_TABLE_MAIN; + const UNICAST: u8 = packet::constants::RTN_UNICAST; + const BOOT_PROT: u8 = packet::constants::RTPROT_BOOT; + + let scope = route.scope as u8; + + use packet::nlas::route::Nla; + + // `rtnetlink` offers a separate request builders for different IP versions (IP v4 and v6). + // This if branch is a bit clumsy because it does almost the same. + // TODO: Simplify this once https://github.com/little-dude/netlink/pull/140 is merged and released + if is_v6 { + let dest_addr = if route.dest.len() > 0 { + Ipv6Network::from_str(&route.dest)? + } else { + Ipv6Network::new(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0), 0)? + }; + + // Build IP v6 request + let mut request = self + .handle + .route() + .add_v6() + .table(MAIN_TABLE) + .kind(UNICAST) + .protocol(BOOT_PROT) + .scope(scope) + .destination_prefix(dest_addr.ip(), dest_addr.prefix()) + .output_interface(link.index()); + + if route.source.len() > 0 { + let network = Ipv6Network::from_str(&route.source)?; + if network.prefix() > 0 { + request = request.source_prefix(network.ip(), network.prefix()); + } else { + request + .message_mut() + .nlas + .push(Nla::PrefSource(network.ip().octets().to_vec())); + } + } + + if route.gateway.len() > 0 { + let ip = Ipv6Addr::from_str(&route.gateway)?; + request = request.gateway(ip); + } + + request.execute().await.with_context(|| { + format!( + "Failed to add IP v6 route (src: {}, dst: {}, gtw: {})", + route.get_source(), + route.get_dest(), + route.get_gateway() + ) + })?; + } else { + let dest_addr = if route.dest.len() > 0 { + Ipv4Network::from_str(&route.dest)? + } else { + Ipv4Network::new(Ipv4Addr::new(0, 0, 0, 0), 0)? + }; + + // Build IP v4 request + let mut request = self + .handle + .route() + .add_v4() + .table(MAIN_TABLE) + .kind(UNICAST) + .protocol(BOOT_PROT) + .scope(scope) + .destination_prefix(dest_addr.ip(), dest_addr.prefix()) + .output_interface(link.index()); + + if route.source.len() > 0 { + let network = Ipv4Network::from_str(&route.source)?; + if network.prefix() > 0 { + request = request.source_prefix(network.ip(), network.prefix()); + } else { + request + .message_mut() + .nlas + .push(Nla::PrefSource(network.ip().octets().to_vec())); + } + } + + if route.gateway.len() > 0 { + let ip = Ipv4Addr::from_str(&route.gateway)?; + request = request.gateway(ip); + } + + request.execute().await?; + } + } + + Ok(()) + } + + async fn delete_routes(&mut self, routes: I) -> Result<()> + where + I: IntoIterator, + { + for route in routes.into_iter() { + if route.header.protocol == packet::constants::RTPROT_KERNEL { + continue; + } + + let index = if let Some(index) = route.output_interface() { + index + } else { + continue; + }; + + let link = self.find_link(LinkFilter::Index(index)).await?; + + let name = link.name(); + if name.contains("lo") || name.contains("::1") { + continue; + } + + self.handle.route().del(route).execute().await?; + } + + Ok(()) + } + + async fn list_addresses(&self, filter: F) -> Result> + where + F: Into>, + { + let mut request = self.handle.address().get(); + + if let Some(filter) = filter.into() { + request = match filter { + AddressFilter::LinkIndex(index) => request.set_link_index_filter(index), + AddressFilter::IpAddress(addr) => request.set_address_filter(addr), + }; + }; + + let list = request + .execute() + .try_filter_map(|msg| future::ready(Ok(Some(Address(msg))))) // Map message to `Address` + .try_collect() + .await?; + Ok(list) + } + + async fn add_addresses(&mut self, index: u32, list: I) -> Result<()> + where + I: IntoIterator, + { + for net in list.into_iter() { + self.handle + .address() + .add(index, net.ip(), net.prefix()) + .execute() + .await + .map_err(|err| anyhow!("Failed to add address {}: {:?}", net.ip(), err))?; + } + + Ok(()) + } + + async fn delete_addresses(&mut self, list: I) -> Result<()> + where + I: IntoIterator, + { + for addr in list.into_iter() { + self.handle.address().del(addr.0).execute().await?; + } + + Ok(()) + } + + pub async fn add_arp_neighbors(&mut self, list: I) -> Result<()> + where + I: IntoIterator, + { + for neigh in list.into_iter() { + self.add_arp_neighbor(&neigh).await.map_err(|err| { + anyhow!( + "Failed to add ARP neighbor {}: {:?}", + neigh.get_toIPAddress().get_address(), + err + ) + })?; + } + + Ok(()) + } + + /// Adds an ARP neighbor. + /// TODO: `rtnetlink` has no neighbours API, remove this after https://github.com/little-dude/netlink/pull/135 + async fn add_arp_neighbor(&mut self, neigh: &ARPNeighbor) -> Result<()> { + let ip_address = neigh + .toIPAddress + .as_ref() + .map(|to| to.address.as_str()) // Extract address field + .and_then(|addr| if addr.is_empty() { None } else { Some(addr) }) // Make sure it's not empty + .ok_or_else(|| nix::Error::Sys(nix::errno::Errno::EINVAL))?; + + let ip = IpAddr::from_str(&ip_address) + .map_err(|e| anyhow!("Failed to parse IP {}: {:?}", ip_address, e))?; + + // Import rtnetlink objects that make sense only for this function + use packet::constants::{NDA_UNSPEC, NLM_F_ACK, NLM_F_CREATE, NLM_F_EXCL, NLM_F_REQUEST}; + use packet::neighbour::{NeighbourHeader, NeighbourMessage}; + use packet::nlas::neighbour::Nla; + use packet::{NetlinkMessage, NetlinkPayload, RtnlMessage}; + use rtnetlink::Error; + + const IFA_F_PERMANENT: u16 = 0x80; // See https://github.com/little-dude/netlink/blob/0185b2952505e271805902bf175fee6ea86c42b8/netlink-packet-route/src/rtnl/constants.rs#L770 + + let link = self.find_link(LinkFilter::Name(&neigh.device)).await?; + + let message = NeighbourMessage { + header: NeighbourHeader { + family: match ip { + IpAddr::V4(_) => packet::AF_INET, + IpAddr::V6(_) => packet::AF_INET6, + } as u8, + ifindex: link.index(), + state: if neigh.state != 0 { + neigh.state as u16 + } else { + IFA_F_PERMANENT + }, + flags: neigh.flags as u8, + ntype: NDA_UNSPEC as u8, + }, + nlas: { + let mut nlas = vec![]; + + nlas.push(Nla::Destination(match ip { + IpAddr::V4(v4) => v4.octets().to_vec(), + IpAddr::V6(v6) => v6.octets().to_vec(), + })); + + if !neigh.lladdr.is_empty() { + nlas.push(Nla::LinkLocalAddress( + parse_mac_address(&neigh.lladdr)?.to_vec(), + )); + } + + nlas + }, + }; + + // Send request and ACK + let mut req = NetlinkMessage::from(RtnlMessage::NewNeighbour(message)); + req.header.flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_EXCL | NLM_F_CREATE; + + let mut response = self.handle.request(req)?; + while let Some(message) = response.next().await { + if let NetlinkPayload::Error(err) = message.payload { + return Err(anyhow!(Error::NetlinkError(err))); + } + } + + Ok(()) + } +} + +fn format_address(data: &[u8]) -> Result { + match data.len() { + 4 => { + // IP v4 + Ok(format!("{}.{}.{}.{}", data[0], data[1], data[2], data[3])) + } + 6 => { + // Mac address + Ok(format!( + "{:0>2X}:{:0>2X}:{:0>2X}:{:0>2X}:{:0>2X}:{:0>2X}", + data[0], data[1], data[2], data[3], data[4], data[5] + )) + } + 16 => { + // IP v6 + let octets = <[u8; 16]>::try_from(data)?; + Ok(Ipv6Addr::from(octets).to_string()) + } + _ => Err(anyhow!("Unsupported address length: {}", data.len())), + } +} + +fn parse_mac_address(addr: &str) -> Result<[u8; 6]> { + let mut split = addr.splitn(6, ":"); + + // Parse single Mac address block + let mut parse_next = || -> Result { + let v = u8::from_str_radix( + split + .next() + .ok_or_else(|| nix::Error::Sys(nix::errno::Errno::EINVAL))?, + 16, + )?; + Ok(v) + }; + + // Parse all 6 blocks + let arr = [ + parse_next()?, + parse_next()?, + parse_next()?, + parse_next()?, + parse_next()?, + parse_next()?, + ]; + + Ok(arr) +} + +/// Wraps external type with the local one, so we can implement various extensions and type conversions. +struct Link(packet::LinkMessage); + +impl Link { + /// If name. + fn name(&self) -> String { + use packet::nlas::link::Nla; + self.nlas + .iter() + .find_map(|n| { + if let Nla::IfName(name) = n { + Some(name.clone()) + } else { + None + } + }) + .unwrap_or(String::new()) + } + + /// Extract Mac address. + fn address(&self) -> String { + use packet::nlas::link::Nla; + self.nlas + .iter() + .find_map(|n| { + if let Nla::Address(data) = n { + format_address(data).ok() + } else { + None + } + }) + .unwrap_or(String::new()) + } + + /// Returns whether the link is UP + fn is_up(&self) -> bool { + self.header.flags & packet::rtnl::constants::IFF_UP > 0 + } + + fn index(&self) -> u32 { + self.header.index + } + + fn mtu(&self) -> Option { + use packet::nlas::link::Nla; + self.nlas.iter().find_map(|n| { + if let Nla::Mtu(mtu) = n { + Some(*mtu as u64) + } else { + None + } + }) + } +} + +impl From for Link { + fn from(msg: packet::LinkMessage) -> Self { + Link(msg) + } +} + +impl Deref for Link { + type Target = packet::LinkMessage; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +struct Address(packet::AddressMessage); + +impl TryFrom
for IPAddress { + type Error = anyhow::Error; + + fn try_from(value: Address) -> Result { + let family = if value.is_ipv6() { + IPFamily::v4 + } else { + IPFamily::v6 + }; + + let mut address = value.address(); + if address.is_empty() { + address = value.local(); + } + + let mask = format!("{}", value.0.header.prefix_len); + + Ok(IPAddress { + family, + address, + mask, + ..Default::default() + }) + } +} + +impl Address { + fn is_ipv6(&self) -> bool { + self.0.header.family == packet::constants::AF_INET6 as u8 + } + + fn prefix(&self) -> u8 { + self.0.header.prefix_len + } + + fn address(&self) -> String { + use packet::nlas::address::Nla; + self.0 + .nlas + .iter() + .find_map(|n| { + if let Nla::Address(data) = n { + format_address(data).ok() + } else { + None + } + }) + .unwrap_or(String::new()) + } + + fn local(&self) -> String { + use packet::nlas::address::Nla; + self.0 + .nlas + .iter() + .find_map(|n| { + if let Nla::Local(data) = n { + format_address(data).ok() + } else { + None + } + }) + .unwrap_or(String::new()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::skip_if_not_root; + use rtnetlink::packet; + use std::iter; + use std::process::Command; + + #[tokio::test] + async fn find_link_by_name() { + let message = Handle::new() + .expect("Failed to create netlink handle") + .find_link(LinkFilter::Name("lo")) + .await + .expect("Loopback not found"); + + assert_ne!(message.header, packet::LinkHeader::default()); + assert_eq!(message.name(), "lo"); + } + + #[tokio::test] + async fn find_link_by_addr() { + let handle = Handle::new().unwrap(); + + let list = handle.list_links().await.unwrap(); + let link = list.first().expect("At least one link required"); + + let result = handle + .find_link(LinkFilter::Address(&link.address())) + .await + .expect("Failed to query link by address"); + + assert_eq!(result.header.index, link.header.index); + } + + #[tokio::test] + async fn link_up() { + skip_if_not_root!(); + + let handle = Handle::new().unwrap(); + let link = handle.find_link(LinkFilter::Name("lo")).await.unwrap(); + + handle + .enable_link(link.header.index, true) + .await + .expect("Failed to bring link up"); + + assert!(handle + .find_link(LinkFilter::Name("lo")) + .await + .unwrap() + .is_up()); + } + + #[tokio::test] + async fn link_ext() { + let lo = Handle::new() + .unwrap() + .find_link(LinkFilter::Name("lo")) + .await + .unwrap(); + + assert_eq!(lo.name(), "lo"); + assert_ne!(lo.address().len(), 0); + } + + #[tokio::test] + async fn list_routes() { + let all = Handle::new() + .unwrap() + .list_routes() + .await + .expect("Failed to list routes"); + + assert_ne!(all.len(), 0); + + for r in &all { + assert_ne!(r.device.len(), 0); + } + } + + #[tokio::test] + async fn list_addresses() { + let list = Handle::new() + .unwrap() + .list_addresses(None) + .await + .expect("Failed to list addresses"); + + assert_ne!(list.len(), 0); + for addr in &list { + assert_ne!(addr.0.header, packet::AddressHeader::default()); + } + } + + #[tokio::test] + async fn list_interfaces() { + let list = Handle::new() + .unwrap() + .list_interfaces() + .await + .expect("Failed to list interfaces"); + + for iface in &list { + assert_ne!(iface.name.len(), 0); + assert_ne!(iface.hwAddr.len(), 0); + assert_ne!(iface.mtu, 0); + + for ip in &iface.IPAddresses { + assert_ne!(ip.mask.len(), 0); + assert_ne!(ip.address.len(), 0); + } + } + } + + #[tokio::test] + async fn add_delete_addresses() { + skip_if_not_root!(); + + let list = vec![ + IpNetwork::from_str("169.254.1.1/31").unwrap(), + IpNetwork::from_str("2001:db8:85a3::8a2e:370:7334/128").unwrap(), + ]; + + let mut handle = Handle::new().unwrap(); + let lo = handle.find_link(LinkFilter::Name("lo")).await.unwrap(); + + for network in list { + handle + .add_addresses(lo.index(), iter::once(network)) + .await + .expect("Failed to add IP"); + + // Make sure the address is there + let result = handle + .list_addresses(AddressFilter::LinkIndex(lo.index())) + .await + .unwrap() + .into_iter() + .find(|p| { + p.prefix() == network.prefix() && p.address() == network.ip().to_string() + }); + + assert!(result.is_some()); + + // Delete it + handle + .delete_addresses(iter::once(result.unwrap())) + .await + .expect("Failed to delete address"); + } + } + + #[test] + fn format_addr() { + let buf = [1u8, 2u8, 3u8, 4u8]; + let addr = format_address(&buf).unwrap(); + assert_eq!(addr, "1.2.3.4"); + + let buf = [1u8, 2u8, 3u8, 4u8, 5u8, 10u8]; + let addr = format_address(&buf).unwrap(); + assert_eq!(addr, "01:02:03:04:05:0A"); + } + + #[test] + fn parse_mac() { + let bytes = parse_mac_address("AB:0C:DE:12:34:56").expect("Failed to parse mac address"); + assert_eq!(bytes, [0xAB, 0x0C, 0xDE, 0x12, 0x34, 0x56]); + } + + fn clean_env_for_test_add_one_arp_neighbor(dummy_name: &str, ip: &str) { + // ip link delete dummy + Command::new("ip") + .args(&["link", "delete", dummy_name]) + .output() + .expect("prepare: failed to delete dummy"); + + // ip neigh del dev dummy ip + Command::new("ip") + .args(&["neigh", "del", dummy_name, ip]) + .output() + .expect("prepare: failed to delete neigh"); + } + + fn prepare_env_for_test_add_one_arp_neighbor(dummy_name: &str, ip: &str) { + clean_env_for_test_add_one_arp_neighbor(dummy_name, ip); + // modprobe dummy + Command::new("modprobe") + .arg("dummy") + .output() + .expect("failed to run modprobe dummy"); + + // ip link add dummy type dummy + Command::new("ip") + .args(&["link", "add", dummy_name, "type", "dummy"]) + .output() + .expect("failed to add dummy interface"); + + // ip addr add 192.168.0.2/16 dev dummy + Command::new("ip") + .args(&["addr", "add", "192.168.0.2/16", "dev", dummy_name]) + .output() + .expect("failed to add ip for dummy"); + + // ip link set dummy up; + Command::new("ip") + .args(&["link", "set", dummy_name, "up"]) + .output() + .expect("failed to up dummy"); + } + + #[tokio::test] + async fn test_add_one_arp_neighbor() { + skip_if_not_root!(); + + let mac = "6a:92:3a:59:70:aa"; + let to_ip = "169.254.1.1"; + let dummy_name = "dummy_for_arp"; + + prepare_env_for_test_add_one_arp_neighbor(dummy_name, to_ip); + + let mut ip_address = IPAddress::new(); + ip_address.set_address(to_ip.to_string()); + + let mut neigh = ARPNeighbor::new(); + neigh.set_toIPAddress(ip_address); + neigh.set_device(dummy_name.to_string()); + neigh.set_lladdr(mac.to_string()); + neigh.set_state(0x80); + + Handle::new() + .unwrap() + .add_arp_neighbor(&neigh) + .await + .expect("Failed to add ARP neighbor"); + + // ip neigh show dev dummy ip + let stdout = Command::new("ip") + .args(&["neigh", "show", "dev", dummy_name, to_ip]) + .output() + .expect("failed to show neigh") + .stdout; + + let stdout = std::str::from_utf8(&stdout).expect("failed to conveert stdout"); + assert_eq!(stdout, format!("{} lladdr {} PERMANENT\n", to_ip, mac)); + + clean_env_for_test_add_one_arp_neighbor(dummy_name, to_ip); + } +}