diff --git a/.gitignore b/.gitignore
index ea8c4bf7f35f6f77f75d92ad8ce8349f6e81ddba..db38427607ec0b382995057c607becfd79047c10 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
 /target
+config.toml
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000000000000000000000000000000000..2fd2423990769ae8f26b90a7cb9d616ee9601385
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,5 @@
+[submodule "third_party/NetworkManager"]
+	path = third_party/NetworkManager
+	url = https://gitlab.freedesktop.org/NetworkManager/NetworkManager.git
+	shallow = true
+	branch = 1.32.10
diff --git a/Cargo.lock b/Cargo.lock
index a24b6bb12da3f0a18689e96fa06edafaf1859468..9b82d6cda6bbc963a51a4616113c0501aa79c919 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,26 @@ version = "1.0.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
 
+[[package]]
+name = "async-trait"
+version = "0.1.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atomic"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3410529e8288c463bedb5930f82833bc0c90e5d2fe639a56582a4d09220b281"
+dependencies = [
+ "autocfg",
+]
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -28,12 +48,99 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "autocfg"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bytes"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "clap"
+version = "3.0.0-beta.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "feff3878564edb93745d58cf63e17b63f24142506e7a20c87a5521ed7bfb1d63"
+dependencies = [
+ "atty",
+ "bitflags",
+ "clap_derive",
+ "indexmap",
+ "lazy_static",
+ "os_str_bytes",
+ "strsim",
+ "termcolor",
+ "textwrap",
+ "unicase",
+]
+
+[[package]]
+name = "clap_derive"
+version = "3.0.0-beta.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b15c6b4f786ffb6192ffe65a36855bc1fc2444bcd0945ae16748dcd6ed7d0d3"
+dependencies = [
+ "heck",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "collective"
+version = "0.1.2"
+source = "git+ssh://git@gitlab.chromabits.com:30022/etcinit/collective.git?branch=master#91bd46040fe1cdc00b7fa5059ec83f9cbc89c683"
+dependencies = [
+ "clap",
+ "figment",
+ "log",
+ "pretty_env_logger",
+ "serde",
+ "thiserror",
+ "toml",
+]
+
+[[package]]
+name = "dbus"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8862bb50aa3b2a2db5bfd2c875c73b3038aa931c411087e335ca8ca0ed430b9"
+dependencies = [
+ "futures-channel",
+ "futures-util",
+ "libc",
+ "libdbus-sys",
+ "winapi",
+]
+
+[[package]]
+name = "dbus-tokio"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fdf8ad870c4824f71f40fd16776b6ee841c89703d6c9d1608c547a2c8c3ffda"
+dependencies = [
+ "dbus",
+ "libc",
+ "tokio",
+]
+
 [[package]]
 name = "env_logger"
 version = "0.7.1"
@@ -47,6 +154,124 @@ dependencies = [
  "termcolor",
 ]
 
+[[package]]
+name = "figment"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "790b4292c72618abbab50f787a477014fe15634f96291de45672ce46afe122df"
+dependencies = [
+ "atomic",
+ "pear",
+ "serde",
+ "toml",
+ "uncased",
+ "version_check",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cd0210d8c325c245ff06fd95a3b13689a1a276ac8cfa8e8720cb840bfb84b9e"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fc8cd39e3dbf865f7340dce6a2d401d24fd37c6fe6c4f0ee0de8bfca2252d27"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e481354db6b5c353246ccf6a728b0c5511d752c08da7260546fc0933869daa11"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89f17b21645bc4ed773c69af9c9a0effd4a3f1a3876eadd453469f8854e7fdd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "996c6442437b62d21a32cd9906f9c41e7dc1e19a9579843fad948696769305af"
+
+[[package]]
+name = "futures-task"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
+
+[[package]]
+name = "futures-util"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+
+[[package]]
+name = "heck"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
+dependencies = [
+ "unicode-segmentation",
+]
+
 [[package]]
 name = "hermit-abi"
 version = "0.1.18"
@@ -65,12 +290,43 @@ dependencies = [
  "quick-error",
 ]
 
+[[package]]
+name = "indexmap"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "inlinable_string"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3094308123a0e9fd59659ce45e22de9f53fc1d2ac6e1feb9fef988e4f76cad77"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
 [[package]]
 name = "libc"
 version = "0.2.94"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e"
 
+[[package]]
+name = "libdbus-sys"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc12a3bc971424edbbf7edaf6e5740483444db63aa8e23d3751ff12a30f306f0"
+dependencies = [
+ "pkg-config",
+]
+
 [[package]]
 name = "log"
 version = "0.4.14"
@@ -87,14 +343,144 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
 
 [[package]]
-name = "nm-wg-dispatcher"
+name = "mio"
+version = "0.7.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
+dependencies = [
+ "libc",
+ "log",
+ "miow",
+ "ntapi",
+ "winapi",
+]
+
+[[package]]
+name = "miow"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "nm-reactor"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "async-trait",
+ "clap",
+ "collective",
+ "dbus",
+ "dbus-tokio",
+ "futures",
+ "futures-channel",
  "log",
+ "num-derive",
+ "num-traits",
  "pretty_env_logger",
+ "serde",
+ "serde_derive",
+ "tokio",
+ "tokio-stream",
+]
+
+[[package]]
+name = "ntapi"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "num-derive"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+
+[[package]]
+name = "os_str_bytes"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addaa943333a514159c80c97ff4a93306530d965d27e139188283cd13e06a799"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "pear"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15e44241c5e4c868e3eaa78b7c1848cadd6344ed4f54d029832d32b415a58702"
+dependencies = [
+ "inlinable_string",
+ "pear_codegen",
+ "yansi",
+]
+
+[[package]]
+name = "pear_codegen"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82a5ca643c2303ecb740d506539deba189e16f2754040a42901cd8105d0282d0"
+dependencies = [
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn",
 ]
 
+[[package]]
+name = "pin-project-lite"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
+
 [[package]]
 name = "pretty_env_logger"
 version = "0.4.0"
@@ -105,12 +491,67 @@ dependencies = [
  "log",
 ]
 
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
+dependencies = [
+ "unicode-xid",
+]
+
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bf29726d67464d49fa6224a1d07936a8c08bb3fba727c7493f6cf1616fdaada"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+ "yansi",
+]
+
 [[package]]
 name = "quick-error"
 version = "1.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
 
+[[package]]
+name = "quote"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
+dependencies = [
+ "proc-macro2",
+]
+
 [[package]]
 name = "regex"
 version = "1.5.4"
@@ -128,6 +569,55 @@ version = "0.6.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
 
+[[package]]
+name = "serde"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+
+[[package]]
+name = "serde_derive"
+version = "1.0.130"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "syn"
+version = "1.0.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6f107db402c2c2055242dbf4d2af0e69197202e9faacbef9571bbe47f5a1b84"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-xid",
+]
+
 [[package]]
 name = "termcolor"
 version = "1.1.2"
@@ -137,6 +627,127 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "textwrap"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4efe6fc2395938c8155973d7be49fe8d03a843726e285e100a8a383cc0154ce"
+dependencies = [
+ "autocfg",
+ "bytes",
+ "libc",
+ "memchr",
+ "mio",
+ "num_cpus",
+ "once_cell",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "tokio-macros",
+ "winapi",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "uncased"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicase"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
+
+[[package]]
+name = "unicode-width"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
+
+[[package]]
+name = "version_check"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+
 [[package]]
 name = "winapi"
 version = "0.3.9"
@@ -167,3 +778,9 @@ 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 = "yansi"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71"
diff --git a/Cargo.toml b/Cargo.toml
index 1ab0f745882008ac12617f1afb1091fc0b7f4cda..86994ae1150fbf54131792ca1009d31254146e64 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,5 +1,5 @@
 [package]
-name = "nm-wg-dispatcher"
+name = "nm-reactor"
 version = "0.1.0"
 authors = ["Eduardo Trujillo <ed@chromabits.com>"]
 edition = "2018"
@@ -9,4 +9,17 @@ edition = "2018"
 [dependencies]
 anyhow = "1.0.40"
 pretty_env_logger = "0.4.0"
-log = "0.4.14"
\ No newline at end of file
+log = "0.4.14"
+dbus = {version = "0.9.3", features=["futures"]}
+dbus-tokio = "0.7.4"
+tokio = {version = "1.0", features=["time", "net", "macros", "rt-multi-thread", "signal", "process", "io-std", "io-util"]}
+tokio-stream = "0.1"
+futures-channel = "0.3.17"
+collective = { git = "ssh://git@gitlab.chromabits.com:30022/etcinit/collective.git", branch = "master" }
+clap = "3.0.0-beta.5"
+serde = "1.0.115"
+serde_derive = "1.0.115"
+num-derive = "0.3.3"
+num-traits = "0.2"
+futures = "0.3.18"
+async-trait = "0.1.52"
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..ff5b85fa9014687de98afee9bc9077b12ff67c0c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,28 @@
+src/dbus_codegen:
+	mkdir src/dbus_codegen
+
+src/dbus_codegen/mod.rs: src/dbus_codegen
+	@echo -e "pub mod network_manager;\npub mod network_manager_access_point;\npub mod network_manager_settings;\npub mod network_manager_connection_active;\npub mod network_manager_device;\n" > src/dbus_codegen/mod.rs
+
+src/dbus_codegen/network_manager.rs: src/dbus_codegen
+	dbus-codegen-rust --file third_party/NetworkManager/introspection/org.freedesktop.NetworkManager.xml -m None -c nonblock -o src/dbus_codegen/network_manager.rs
+
+src/dbus_codegen/network_manager_access_point.rs: src/dbus_codegen
+	dbus-codegen-rust --file third_party/NetworkManager/introspection/org.freedesktop.NetworkManager.AccessPoint.xml -m None -c nonblock -o src/dbus_codegen/network_manager_access_point.rs
+
+src/dbus_codegen/network_manager_connection_active.rs: src/dbus_codegen
+	dbus-codegen-rust --file third_party/NetworkManager/introspection/org.freedesktop.NetworkManager.Connection.Active.xml -m None -c nonblock -o src/dbus_codegen/network_manager_connection_active.rs
+
+src/dbus_codegen/network_manager_device.rs: src/dbus_codegen
+	dbus-codegen-rust --file third_party/NetworkManager/introspection/org.freedesktop.NetworkManager.Device.xml -m None -c nonblock -o src/dbus_codegen/network_manager_device.rs
+
+src/dbus_codegen/network_manager_settings.rs: src/dbus_codegen
+	dbus-codegen-rust --file third_party/NetworkManager/introspection/org.freedesktop.NetworkManager.Settings.xml -m None -c nonblock -o src/dbus_codegen/network_manager_settings.rs
+
+dbus: src/dbus_codegen/mod.rs src/dbus_codegen/network_manager.rs src/dbus_codegen/network_manager_connection_active.rs src/dbus_codegen/network_manager_device.rs src/dbus_codegen/network_manager_settings.rs
+
+dbus-clean:
+	rm -r src/dbus_codegen
+
+all: dbus
+clean: dbus-clean
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..5bf4737199a1b1cd960cacb58e33f8d67f9e7d81
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+# nm-reactor
+
+_A [NetworkManager][nm] reactor_. If-this-then-that for NetworkManager.
+
+[nm]: https://gitlab.freedesktop.org/NetworkManager/NetworkManager
\ No newline at end of file
diff --git a/src/action.rs b/src/action.rs
new file mode 100644
index 0000000000000000000000000000000000000000..868a093467afbb541e5b68b3ae0bc861ce6fd09f
--- /dev/null
+++ b/src/action.rs
@@ -0,0 +1,142 @@
+use std::sync::Arc;
+
+use crate::{
+    dbus_wrappers::manager::ManagerWrapper,
+    identifier::{ActiveConnectionIdentifier, ConnectionIdentifier, DeviceIdentifier},
+};
+
+use anyhow::Result;
+use dbus::{nonblock::SyncConnection, Path};
+use serde_derive::{Deserialize, Serialize};
+use tokio::{
+    io::{self, AsyncWriteExt},
+    process::Command,
+};
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(tag = "type")]
+pub enum Action {
+    // Activates the specified connection.
+    ActivateConnection {
+        connection_identifier: Option<ConnectionIdentifier>,
+        device_identifier: Option<DeviceIdentifier>,
+        specific_object: Option<String>,
+    },
+    // Deactivates the specified connection.
+    DeactivateConnection {
+        active_connection_identifier: ActiveConnectionIdentifier,
+    },
+    // Executes the specified command.
+    //
+    // WARNING: Use caution with this option as the command will be executed
+    // using the daemon's context. This could be a security issue.
+    RunCommand {
+        program: String,
+        arguments: Vec<String>,
+    },
+    // Log a message to the output of the daemon. Useful for debugging.
+    Log {
+        message: String,
+    },
+}
+
+impl Action {
+    pub async fn execute(&self, conn: &Arc<SyncConnection>) -> Result<()> {
+        match self {
+            Action::ActivateConnection {
+                connection_identifier,
+                device_identifier,
+                specific_object,
+            } => {
+                log::debug!(
+                    "Executing ActivateConnection action for {:?} {:?} {:?}",
+                    connection_identifier,
+                    device_identifier,
+                    specific_object
+                );
+
+                let maybe_connection = match connection_identifier {
+                    Some(connection_identifier) => {
+                        connection_identifier.into_connection(conn).await
+                    }
+                    None => Ok(None),
+                }?;
+
+                let maybe_device = match device_identifier {
+                    Some(device_identifier) => {
+                        let identifier = device_identifier.into_device(conn).await?;
+
+                        Some(identifier)
+                    }
+                    None => None,
+                };
+
+                let manager = ManagerWrapper::from_connection(conn).await;
+
+                let result = manager
+                    .activate_connection(
+                        maybe_connection.as_ref(),
+                        maybe_device.as_ref(),
+                        match specific_object {
+                            Some(inner) => Some(Path::from(inner.clone())),
+                            None => None,
+                        },
+                    )
+                    .await?;
+
+                log::info!("Connection activated: {}", result.get_path());
+
+                Ok(())
+            }
+            Action::DeactivateConnection {
+                active_connection_identifier,
+            } => {
+                log::debug!(
+                    "Executing DeactivateConnection action for {:?}",
+                    active_connection_identifier
+                );
+
+                match active_connection_identifier
+                    .into_active_connection(conn)
+                    .await?
+                {
+                    Some(active_connection) => {
+                        let manager = ManagerWrapper::from_connection(conn).await;
+
+                        manager.deactive_connection(&active_connection).await?;
+                    }
+                    None => {
+                        log::warn!(
+                            "Unable to find active connection: {:?}",
+                            active_connection_identifier
+                        )
+                    }
+                }
+
+                Ok(())
+            }
+            Action::RunCommand { program, arguments } => {
+                log::debug!(
+                    "Executing RunCommand action: {} {}",
+                    program,
+                    arguments.join(" ")
+                );
+
+                let output = Command::new(program)
+                    .args(arguments.into_iter())
+                    .output()
+                    .await?;
+
+                io::stdout().write_all(&output.stdout).await?;
+                io::stderr().write_all(&output.stderr).await?;
+
+                Ok(())
+            }
+            Action::Log { message } => {
+                log::info!("{}", message);
+
+                Ok(())
+            }
+        }
+    }
+}
diff --git a/src/condition.rs b/src/condition.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ff1492847412ec2c20b383824f919283296c6882
--- /dev/null
+++ b/src/condition.rs
@@ -0,0 +1,188 @@
+use std::{collections::HashSet, sync::Arc};
+
+use anyhow::Result;
+use dbus::nonblock::SyncConnection;
+use futures::{future::join_all, stream, StreamExt};
+use serde_derive::{Deserialize, Serialize};
+
+use crate::{
+    dbus_wrappers::{
+        active_connection::ActiveConnectionWrapper, device::DeviceWrapper,
+        device_state::DeviceState,
+    },
+    identifier::{ActiveConnectionIdentifier, DeviceIdentifier},
+};
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(tag = "type")]
+pub enum Condition {
+    // Always passes.
+    AlwaysTrue,
+    // Always fails.
+    AlwaysFalse,
+    // Passes if the device has any active connections.
+    DeviceIsConnected {
+        device_identifier: DeviceIdentifier,
+    },
+    // Fails if the device has any active connections.
+    DeviceIsNotConnected {
+        device_identifier: DeviceIdentifier,
+    },
+    // Passes if the device has any of the listed states.
+    DeviceStateIsAnyOf {
+        device_identifier: DeviceIdentifier,
+        states: HashSet<DeviceState>,
+    },
+    // Passes if the device is connected to any of the listed connections.
+    DeviceIsConnectedToOneOf {
+        device_identifier: DeviceIdentifier,
+        connections: Vec<ActiveConnectionIdentifier>,
+    },
+    // Passes if the device is not connected to the listed connections.
+    DeviceIsNotConnectedToOneOf {
+        device_identifier: DeviceIdentifier,
+        connections: Vec<ActiveConnectionIdentifier>,
+    },
+    // TODO: Passes if the device is connected to one of the listed SSIDs.
+    // DeviceIsConnectedToOneOfSSIDs {
+    //     device_identifier: DeviceIdentifier,
+    //     ssids: Vec<String>,
+    // },
+    // TODO: Passes if the device is not connected to the listed SSIDs.
+    // DeviceIsNotConnectedToOneOfSSIDs {
+    //     device_identifier: DeviceIdentifier,
+    //     ssids: Vec<String>,
+    // },
+}
+
+impl Condition {
+    pub async fn evaluate(&self, conn: &Arc<SyncConnection>) -> Result<bool> {
+        match self {
+            Condition::AlwaysTrue => Ok(true),
+            Condition::AlwaysFalse => Ok(false),
+            Condition::DeviceIsConnected {
+                device_identifier: device_id,
+            } => {
+                log::debug!("Evaluating DeviceIsConnected condition for {:?}", device_id);
+
+                let device = device_id.into_device(conn).await?;
+
+                device_matches_states(&device, vec![DeviceState::Activated].into_iter().collect())
+                    .await
+            }
+            Condition::DeviceIsNotConnected {
+                device_identifier: device_id,
+            } => {
+                log::debug!(
+                    "Evaluating DeviceIsNotConnected condition for {:?}",
+                    device_id
+                );
+
+                let device = device_id.into_device(conn).await?;
+
+                device_matches_states(
+                    &device,
+                    vec![DeviceState::Disconnected, DeviceState::Unavailable]
+                        .into_iter()
+                        .collect(),
+                )
+                .await
+            }
+            Condition::DeviceStateIsAnyOf {
+                device_identifier: device_id,
+                states,
+            } => {
+                log::debug!(
+                    "Evaluating DeviceStateIsAnyOf condition for {:?}",
+                    device_id
+                );
+
+                let device = device_id.into_device(conn).await?;
+
+                device_matches_states(&device, states.clone()).await
+            }
+            Condition::DeviceIsConnectedToOneOf {
+                device_identifier: device_id,
+                connections,
+            } => {
+                log::debug!(
+                    "Evaluating DeviceIsConnectedToOneOf condition for {:?}",
+                    device_id
+                );
+
+                device_is_connected_to_one_of(conn, device_id, connections).await
+            }
+            Condition::DeviceIsNotConnectedToOneOf {
+                device_identifier: device_id,
+                connections,
+            } => {
+                log::debug!(
+                    "Evaluating DeviceIsConnectedToOneOf condition for {:?}",
+                    device_id
+                );
+
+                Ok(!device_is_connected_to_one_of(conn, device_id, connections).await?)
+            }
+            _ => Ok(false),
+        }
+    }
+}
+
+async fn device_matches_states(
+    device: &DeviceWrapper<'_>,
+    states: HashSet<DeviceState>,
+) -> Result<bool> {
+    match device.get_state().await? {
+        Some(state) => Ok(states.contains(&state)),
+        _ => Ok(false),
+    }
+}
+
+async fn device_is_connected_to_one_of<'a>(
+    conn: &Arc<SyncConnection>,
+    device_identifier: &DeviceIdentifier,
+    connections: &'a Vec<ActiveConnectionIdentifier>,
+) -> Result<bool> {
+    let device = device_identifier.into_device(conn).await?;
+
+    let active_connections: Vec<ActiveConnectionWrapper<'a>> = stream::iter(connections)
+        .filter_map(|connection_identifier| async move {
+            match connection_identifier.into_active_connection(conn).await {
+                Ok(active_connection) => active_connection,
+                Err(err) => {
+                    log::error!(
+                        "Got error retrieving active connection for {:?}: {:?}",
+                        connection_identifier,
+                        err
+                    );
+
+                    None
+                }
+            }
+        })
+        .collect()
+        .await;
+
+    let has_any_matches = stream::iter(active_connections).any(|active_connection| {
+        let device_path = device.get_path();
+
+        async move {
+            let device_paths = active_connection.get_device_paths().await;
+
+            match device_paths {
+                Ok(device_paths) => device_paths.iter().any(|x| x.eq(&device_path)),
+                Err(err) => {
+                    log::error!(
+                        "Unable to get device paths for active connection {:?}: {:?}",
+                        active_connection.get_path(),
+                        err
+                    );
+
+                    false
+                }
+            }
+        }
+    });
+
+    Ok(has_any_matches.await)
+}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000000000000000000000000000000000000..abd21fa5d5601705b0543c4399a637915a65f67c
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,19 @@
+use serde_derive::{Deserialize, Serialize};
+
+use crate::rule::Rule;
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct Config {
+    // Rules to evaluate when processing events.
+    //
+    // Rules are evaluated serially in the order provided.
+    pub rules: Vec<Rule>
+}
+
+impl Default for Config {
+    fn default() -> Self {
+        Config {
+            rules: vec![]
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/dbus_codegen/mod.rs b/src/dbus_codegen/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..59fda6e8bed3a2ad0dbef50277dcba00d574530f
--- /dev/null
+++ b/src/dbus_codegen/mod.rs
@@ -0,0 +1,6 @@
+pub mod network_manager;
+pub mod network_manager_access_point;
+pub mod network_manager_settings;
+pub mod network_manager_connection_active;
+pub mod network_manager_device;
+
diff --git a/src/dbus_codegen/network_manager.rs b/src/dbus_codegen/network_manager.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e1d6009ba540c152533dbfd502db9d09d3d6c80b
--- /dev/null
+++ b/src/dbus_codegen/network_manager.rs
@@ -0,0 +1,353 @@
+// This code was autogenerated with `dbus-codegen-rust --file third_party/NetworkManager/introspection/org.freedesktop.NetworkManager.xml -m None -c nonblock -o src/dbus_codegen/network_manager.rs`, see https://github.com/diwic/dbus-rs
+use dbus as dbus;
+#[allow(unused_imports)]
+use dbus::arg;
+use dbus::nonblock;
+
+pub trait OrgFreedesktopNetworkManager {
+    fn reload(&self, flags: u32) -> nonblock::MethodReply<()>;
+    fn get_devices(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>>;
+    fn get_all_devices(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>>;
+    fn get_device_by_ip_iface(&self, iface: &str) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn activate_connection(&self, connection: dbus::Path, device: dbus::Path, specific_object: dbus::Path) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn add_and_activate_connection(&self, connection: ::std::collections::HashMap<&str, arg::PropMap>, device: dbus::Path, specific_object: dbus::Path) -> nonblock::MethodReply<(dbus::Path<'static>, dbus::Path<'static>)>;
+    fn add_and_activate_connection2(&self, connection: ::std::collections::HashMap<&str, arg::PropMap>, device: dbus::Path, specific_object: dbus::Path, options: arg::PropMap) -> nonblock::MethodReply<(dbus::Path<'static>, dbus::Path<'static>, arg::PropMap)>;
+    fn deactivate_connection(&self, active_connection: dbus::Path) -> nonblock::MethodReply<()>;
+    fn sleep(&self, sleep: bool) -> nonblock::MethodReply<()>;
+    fn enable(&self, enable: bool) -> nonblock::MethodReply<()>;
+    fn get_permissions(&self) -> nonblock::MethodReply<::std::collections::HashMap<String, String>>;
+    fn set_logging(&self, level: &str, domains: &str) -> nonblock::MethodReply<()>;
+    fn get_logging(&self) -> nonblock::MethodReply<(String, String)>;
+    fn check_connectivity(&self) -> nonblock::MethodReply<u32>;
+    fn state(&self) -> nonblock::MethodReply<u32>;
+    fn checkpoint_create(&self, devices: Vec<dbus::Path>, rollback_timeout: u32, flags: u32) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn checkpoint_destroy(&self, checkpoint: dbus::Path) -> nonblock::MethodReply<()>;
+    fn checkpoint_rollback(&self, checkpoint: dbus::Path) -> nonblock::MethodReply<::std::collections::HashMap<String, u32>>;
+    fn checkpoint_adjust_rollback_timeout(&self, checkpoint: dbus::Path, add_timeout: u32) -> nonblock::MethodReply<()>;
+    fn devices(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>>;
+    fn all_devices(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>>;
+    fn checkpoints(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>>;
+    fn networking_enabled(&self) -> nonblock::MethodReply<bool>;
+    fn wireless_enabled(&self) -> nonblock::MethodReply<bool>;
+    fn set_wireless_enabled(&self, value: bool) -> nonblock::MethodReply<()>;
+    fn wireless_hardware_enabled(&self) -> nonblock::MethodReply<bool>;
+    fn wwan_enabled(&self) -> nonblock::MethodReply<bool>;
+    fn set_wwan_enabled(&self, value: bool) -> nonblock::MethodReply<()>;
+    fn wwan_hardware_enabled(&self) -> nonblock::MethodReply<bool>;
+    fn wimax_enabled(&self) -> nonblock::MethodReply<bool>;
+    fn set_wimax_enabled(&self, value: bool) -> nonblock::MethodReply<()>;
+    fn wimax_hardware_enabled(&self) -> nonblock::MethodReply<bool>;
+    fn active_connections(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>>;
+    fn primary_connection(&self) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn primary_connection_type(&self) -> nonblock::MethodReply<String>;
+    fn metered(&self) -> nonblock::MethodReply<u32>;
+    fn activating_connection(&self) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn startup(&self) -> nonblock::MethodReply<bool>;
+    fn version(&self) -> nonblock::MethodReply<String>;
+    fn capabilities(&self) -> nonblock::MethodReply<Vec<u32>>;
+    fn state_(&self) -> nonblock::MethodReply<u32>;
+    fn connectivity(&self) -> nonblock::MethodReply<u32>;
+    fn connectivity_check_available(&self) -> nonblock::MethodReply<bool>;
+    fn connectivity_check_enabled(&self) -> nonblock::MethodReply<bool>;
+    fn set_connectivity_check_enabled(&self, value: bool) -> nonblock::MethodReply<()>;
+    fn connectivity_check_uri(&self) -> nonblock::MethodReply<String>;
+    fn global_dns_configuration(&self) -> nonblock::MethodReply<arg::PropMap>;
+    fn set_global_dns_configuration(&self, value: arg::PropMap) -> nonblock::MethodReply<()>;
+}
+
+impl<'a, T: nonblock::NonblockReply, C: ::std::ops::Deref<Target=T>> OrgFreedesktopNetworkManager for nonblock::Proxy<'a, C> {
+
+    fn reload(&self, flags: u32) -> nonblock::MethodReply<()> {
+        self.method_call("org.freedesktop.NetworkManager", "Reload", (flags, ))
+    }
+
+    fn get_devices(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>> {
+        self.method_call("org.freedesktop.NetworkManager", "GetDevices", ())
+            .and_then(|r: (Vec<dbus::Path<'static>>, )| Ok(r.0, ))
+    }
+
+    fn get_all_devices(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>> {
+        self.method_call("org.freedesktop.NetworkManager", "GetAllDevices", ())
+            .and_then(|r: (Vec<dbus::Path<'static>>, )| Ok(r.0, ))
+    }
+
+    fn get_device_by_ip_iface(&self, iface: &str) -> nonblock::MethodReply<dbus::Path<'static>> {
+        self.method_call("org.freedesktop.NetworkManager", "GetDeviceByIpIface", (iface, ))
+            .and_then(|r: (dbus::Path<'static>, )| Ok(r.0, ))
+    }
+
+    fn activate_connection(&self, connection: dbus::Path, device: dbus::Path, specific_object: dbus::Path) -> nonblock::MethodReply<dbus::Path<'static>> {
+        self.method_call("org.freedesktop.NetworkManager", "ActivateConnection", (connection, device, specific_object, ))
+            .and_then(|r: (dbus::Path<'static>, )| Ok(r.0, ))
+    }
+
+    fn add_and_activate_connection(&self, connection: ::std::collections::HashMap<&str, arg::PropMap>, device: dbus::Path, specific_object: dbus::Path) -> nonblock::MethodReply<(dbus::Path<'static>, dbus::Path<'static>)> {
+        self.method_call("org.freedesktop.NetworkManager", "AddAndActivateConnection", (connection, device, specific_object, ))
+    }
+
+    fn add_and_activate_connection2(&self, connection: ::std::collections::HashMap<&str, arg::PropMap>, device: dbus::Path, specific_object: dbus::Path, options: arg::PropMap) -> nonblock::MethodReply<(dbus::Path<'static>, dbus::Path<'static>, arg::PropMap)> {
+        self.method_call("org.freedesktop.NetworkManager", "AddAndActivateConnection2", (connection, device, specific_object, options, ))
+    }
+
+    fn deactivate_connection(&self, active_connection: dbus::Path) -> nonblock::MethodReply<()> {
+        self.method_call("org.freedesktop.NetworkManager", "DeactivateConnection", (active_connection, ))
+    }
+
+    fn sleep(&self, sleep: bool) -> nonblock::MethodReply<()> {
+        self.method_call("org.freedesktop.NetworkManager", "Sleep", (sleep, ))
+    }
+
+    fn enable(&self, enable: bool) -> nonblock::MethodReply<()> {
+        self.method_call("org.freedesktop.NetworkManager", "Enable", (enable, ))
+    }
+
+    fn get_permissions(&self) -> nonblock::MethodReply<::std::collections::HashMap<String, String>> {
+        self.method_call("org.freedesktop.NetworkManager", "GetPermissions", ())
+            .and_then(|r: (::std::collections::HashMap<String, String>, )| Ok(r.0, ))
+    }
+
+    fn set_logging(&self, level: &str, domains: &str) -> nonblock::MethodReply<()> {
+        self.method_call("org.freedesktop.NetworkManager", "SetLogging", (level, domains, ))
+    }
+
+    fn get_logging(&self) -> nonblock::MethodReply<(String, String)> {
+        self.method_call("org.freedesktop.NetworkManager", "GetLogging", ())
+    }
+
+    fn check_connectivity(&self) -> nonblock::MethodReply<u32> {
+        self.method_call("org.freedesktop.NetworkManager", "CheckConnectivity", ())
+            .and_then(|r: (u32, )| Ok(r.0, ))
+    }
+
+    fn state(&self) -> nonblock::MethodReply<u32> {
+        self.method_call("org.freedesktop.NetworkManager", "state", ())
+            .and_then(|r: (u32, )| Ok(r.0, ))
+    }
+
+    fn checkpoint_create(&self, devices: Vec<dbus::Path>, rollback_timeout: u32, flags: u32) -> nonblock::MethodReply<dbus::Path<'static>> {
+        self.method_call("org.freedesktop.NetworkManager", "CheckpointCreate", (devices, rollback_timeout, flags, ))
+            .and_then(|r: (dbus::Path<'static>, )| Ok(r.0, ))
+    }
+
+    fn checkpoint_destroy(&self, checkpoint: dbus::Path) -> nonblock::MethodReply<()> {
+        self.method_call("org.freedesktop.NetworkManager", "CheckpointDestroy", (checkpoint, ))
+    }
+
+    fn checkpoint_rollback(&self, checkpoint: dbus::Path) -> nonblock::MethodReply<::std::collections::HashMap<String, u32>> {
+        self.method_call("org.freedesktop.NetworkManager", "CheckpointRollback", (checkpoint, ))
+            .and_then(|r: (::std::collections::HashMap<String, u32>, )| Ok(r.0, ))
+    }
+
+    fn checkpoint_adjust_rollback_timeout(&self, checkpoint: dbus::Path, add_timeout: u32) -> nonblock::MethodReply<()> {
+        self.method_call("org.freedesktop.NetworkManager", "CheckpointAdjustRollbackTimeout", (checkpoint, add_timeout, ))
+    }
+
+    fn devices(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "Devices")
+    }
+
+    fn all_devices(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "AllDevices")
+    }
+
+    fn checkpoints(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "Checkpoints")
+    }
+
+    fn networking_enabled(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "NetworkingEnabled")
+    }
+
+    fn wireless_enabled(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "WirelessEnabled")
+    }
+
+    fn wireless_hardware_enabled(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "WirelessHardwareEnabled")
+    }
+
+    fn wwan_enabled(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "WwanEnabled")
+    }
+
+    fn wwan_hardware_enabled(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "WwanHardwareEnabled")
+    }
+
+    fn wimax_enabled(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "WimaxEnabled")
+    }
+
+    fn wimax_hardware_enabled(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "WimaxHardwareEnabled")
+    }
+
+    fn active_connections(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "ActiveConnections")
+    }
+
+    fn primary_connection(&self) -> nonblock::MethodReply<dbus::Path<'static>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "PrimaryConnection")
+    }
+
+    fn primary_connection_type(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "PrimaryConnectionType")
+    }
+
+    fn metered(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "Metered")
+    }
+
+    fn activating_connection(&self) -> nonblock::MethodReply<dbus::Path<'static>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "ActivatingConnection")
+    }
+
+    fn startup(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "Startup")
+    }
+
+    fn version(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "Version")
+    }
+
+    fn capabilities(&self) -> nonblock::MethodReply<Vec<u32>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "Capabilities")
+    }
+
+    fn state_(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "State")
+    }
+
+    fn connectivity(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "Connectivity")
+    }
+
+    fn connectivity_check_available(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "ConnectivityCheckAvailable")
+    }
+
+    fn connectivity_check_enabled(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "ConnectivityCheckEnabled")
+    }
+
+    fn connectivity_check_uri(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "ConnectivityCheckUri")
+    }
+
+    fn global_dns_configuration(&self) -> nonblock::MethodReply<arg::PropMap> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager", "GlobalDnsConfiguration")
+    }
+
+    fn set_wireless_enabled(&self, value: bool) -> nonblock::MethodReply<()> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::set(&self, "org.freedesktop.NetworkManager", "WirelessEnabled", value)
+    }
+
+    fn set_wwan_enabled(&self, value: bool) -> nonblock::MethodReply<()> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::set(&self, "org.freedesktop.NetworkManager", "WwanEnabled", value)
+    }
+
+    fn set_wimax_enabled(&self, value: bool) -> nonblock::MethodReply<()> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::set(&self, "org.freedesktop.NetworkManager", "WimaxEnabled", value)
+    }
+
+    fn set_connectivity_check_enabled(&self, value: bool) -> nonblock::MethodReply<()> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::set(&self, "org.freedesktop.NetworkManager", "ConnectivityCheckEnabled", value)
+    }
+
+    fn set_global_dns_configuration(&self, value: arg::PropMap) -> nonblock::MethodReply<()> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::set(&self, "org.freedesktop.NetworkManager", "GlobalDnsConfiguration", value)
+    }
+}
+
+#[derive(Debug)]
+pub struct OrgFreedesktopNetworkManagerCheckPermissions {
+}
+
+impl arg::AppendAll for OrgFreedesktopNetworkManagerCheckPermissions {
+    fn append(&self, _: &mut arg::IterAppend) {
+    }
+}
+
+impl arg::ReadAll for OrgFreedesktopNetworkManagerCheckPermissions {
+    fn read(_: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
+        Ok(OrgFreedesktopNetworkManagerCheckPermissions {
+        })
+    }
+}
+
+impl dbus::message::SignalArgs for OrgFreedesktopNetworkManagerCheckPermissions {
+    const NAME: &'static str = "CheckPermissions";
+    const INTERFACE: &'static str = "org.freedesktop.NetworkManager";
+}
+
+#[derive(Debug)]
+pub struct OrgFreedesktopNetworkManagerStateChanged {
+    pub state: u32,
+}
+
+impl arg::AppendAll for OrgFreedesktopNetworkManagerStateChanged {
+    fn append(&self, i: &mut arg::IterAppend) {
+        arg::RefArg::append(&self.state, i);
+    }
+}
+
+impl arg::ReadAll for OrgFreedesktopNetworkManagerStateChanged {
+    fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
+        Ok(OrgFreedesktopNetworkManagerStateChanged {
+            state: i.read()?,
+        })
+    }
+}
+
+impl dbus::message::SignalArgs for OrgFreedesktopNetworkManagerStateChanged {
+    const NAME: &'static str = "StateChanged";
+    const INTERFACE: &'static str = "org.freedesktop.NetworkManager";
+}
+
+#[derive(Debug)]
+pub struct OrgFreedesktopNetworkManagerDeviceAdded {
+    pub device_path: dbus::Path<'static>,
+}
+
+impl arg::AppendAll for OrgFreedesktopNetworkManagerDeviceAdded {
+    fn append(&self, i: &mut arg::IterAppend) {
+        arg::RefArg::append(&self.device_path, i);
+    }
+}
+
+impl arg::ReadAll for OrgFreedesktopNetworkManagerDeviceAdded {
+    fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
+        Ok(OrgFreedesktopNetworkManagerDeviceAdded {
+            device_path: i.read()?,
+        })
+    }
+}
+
+impl dbus::message::SignalArgs for OrgFreedesktopNetworkManagerDeviceAdded {
+    const NAME: &'static str = "DeviceAdded";
+    const INTERFACE: &'static str = "org.freedesktop.NetworkManager";
+}
+
+#[derive(Debug)]
+pub struct OrgFreedesktopNetworkManagerDeviceRemoved {
+    pub device_path: dbus::Path<'static>,
+}
+
+impl arg::AppendAll for OrgFreedesktopNetworkManagerDeviceRemoved {
+    fn append(&self, i: &mut arg::IterAppend) {
+        arg::RefArg::append(&self.device_path, i);
+    }
+}
+
+impl arg::ReadAll for OrgFreedesktopNetworkManagerDeviceRemoved {
+    fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
+        Ok(OrgFreedesktopNetworkManagerDeviceRemoved {
+            device_path: i.read()?,
+        })
+    }
+}
+
+impl dbus::message::SignalArgs for OrgFreedesktopNetworkManagerDeviceRemoved {
+    const NAME: &'static str = "DeviceRemoved";
+    const INTERFACE: &'static str = "org.freedesktop.NetworkManager";
+}
diff --git a/src/dbus_codegen/network_manager_access_point.rs b/src/dbus_codegen/network_manager_access_point.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ec7ef6bb0978ea1554538bfd97bbe94b74db251c
--- /dev/null
+++ b/src/dbus_codegen/network_manager_access_point.rs
@@ -0,0 +1,61 @@
+// This code was autogenerated with `dbus-codegen-rust --file third_party/NetworkManager/introspection/org.freedesktop.NetworkManager.AccessPoint.xml -m None -c nonblock -o src/dbus_codegen/network_manager_access_point.rs`, see https://github.com/diwic/dbus-rs
+use dbus as dbus;
+#[allow(unused_imports)]
+use dbus::arg;
+use dbus::nonblock;
+
+pub trait OrgFreedesktopNetworkManagerAccessPoint {
+    fn flags(&self) -> nonblock::MethodReply<u32>;
+    fn wpa_flags(&self) -> nonblock::MethodReply<u32>;
+    fn rsn_flags(&self) -> nonblock::MethodReply<u32>;
+    fn ssid(&self) -> nonblock::MethodReply<Vec<u8>>;
+    fn frequency(&self) -> nonblock::MethodReply<u32>;
+    fn hw_address(&self) -> nonblock::MethodReply<String>;
+    fn mode(&self) -> nonblock::MethodReply<u32>;
+    fn max_bitrate(&self) -> nonblock::MethodReply<u32>;
+    fn strength(&self) -> nonblock::MethodReply<u8>;
+    fn last_seen(&self) -> nonblock::MethodReply<i32>;
+}
+
+impl<'a, T: nonblock::NonblockReply, C: ::std::ops::Deref<Target=T>> OrgFreedesktopNetworkManagerAccessPoint for nonblock::Proxy<'a, C> {
+
+    fn flags(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.AccessPoint", "Flags")
+    }
+
+    fn wpa_flags(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.AccessPoint", "WpaFlags")
+    }
+
+    fn rsn_flags(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.AccessPoint", "RsnFlags")
+    }
+
+    fn ssid(&self) -> nonblock::MethodReply<Vec<u8>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.AccessPoint", "Ssid")
+    }
+
+    fn frequency(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.AccessPoint", "Frequency")
+    }
+
+    fn hw_address(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.AccessPoint", "HwAddress")
+    }
+
+    fn mode(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.AccessPoint", "Mode")
+    }
+
+    fn max_bitrate(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.AccessPoint", "MaxBitrate")
+    }
+
+    fn strength(&self) -> nonblock::MethodReply<u8> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.AccessPoint", "Strength")
+    }
+
+    fn last_seen(&self) -> nonblock::MethodReply<i32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.AccessPoint", "LastSeen")
+    }
+}
diff --git a/src/dbus_codegen/network_manager_connection_active.rs b/src/dbus_codegen/network_manager_connection_active.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1e08a866e4b7fb5366e8c8c7aba3d84ad83bbd00
--- /dev/null
+++ b/src/dbus_codegen/network_manager_connection_active.rs
@@ -0,0 +1,118 @@
+// This code was autogenerated with `dbus-codegen-rust --file third_party/NetworkManager/introspection/org.freedesktop.NetworkManager.Connection.Active.xml -m None -c nonblock -o src/dbus_codegen/network_manager_connection_active.rs`, see https://github.com/diwic/dbus-rs
+use dbus as dbus;
+#[allow(unused_imports)]
+use dbus::arg;
+use dbus::nonblock;
+
+pub trait OrgFreedesktopNetworkManagerConnectionActive {
+    fn connection(&self) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn specific_object(&self) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn id(&self) -> nonblock::MethodReply<String>;
+    fn uuid(&self) -> nonblock::MethodReply<String>;
+    fn type_(&self) -> nonblock::MethodReply<String>;
+    fn devices(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>>;
+    fn state(&self) -> nonblock::MethodReply<u32>;
+    fn state_flags(&self) -> nonblock::MethodReply<u32>;
+    fn default(&self) -> nonblock::MethodReply<bool>;
+    fn ip4_config(&self) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn dhcp4_config(&self) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn default6(&self) -> nonblock::MethodReply<bool>;
+    fn ip6_config(&self) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn dhcp6_config(&self) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn vpn(&self) -> nonblock::MethodReply<bool>;
+    fn master(&self) -> nonblock::MethodReply<dbus::Path<'static>>;
+}
+
+impl<'a, T: nonblock::NonblockReply, C: ::std::ops::Deref<Target=T>> OrgFreedesktopNetworkManagerConnectionActive for nonblock::Proxy<'a, C> {
+
+    fn connection(&self) -> nonblock::MethodReply<dbus::Path<'static>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "Connection")
+    }
+
+    fn specific_object(&self) -> nonblock::MethodReply<dbus::Path<'static>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "SpecificObject")
+    }
+
+    fn id(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "Id")
+    }
+
+    fn uuid(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "Uuid")
+    }
+
+    fn type_(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "Type")
+    }
+
+    fn devices(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "Devices")
+    }
+
+    fn state(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "State")
+    }
+
+    fn state_flags(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "StateFlags")
+    }
+
+    fn default(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "Default")
+    }
+
+    fn ip4_config(&self) -> nonblock::MethodReply<dbus::Path<'static>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "Ip4Config")
+    }
+
+    fn dhcp4_config(&self) -> nonblock::MethodReply<dbus::Path<'static>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "Dhcp4Config")
+    }
+
+    fn default6(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "Default6")
+    }
+
+    fn ip6_config(&self) -> nonblock::MethodReply<dbus::Path<'static>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "Ip6Config")
+    }
+
+    fn dhcp6_config(&self) -> nonblock::MethodReply<dbus::Path<'static>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "Dhcp6Config")
+    }
+
+    fn vpn(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "Vpn")
+    }
+
+    fn master(&self) -> nonblock::MethodReply<dbus::Path<'static>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Connection.Active", "Master")
+    }
+}
+
+#[derive(Debug)]
+pub struct OrgFreedesktopNetworkManagerConnectionActiveStateChanged {
+    pub state: u32,
+    pub reason: u32,
+}
+
+impl arg::AppendAll for OrgFreedesktopNetworkManagerConnectionActiveStateChanged {
+    fn append(&self, i: &mut arg::IterAppend) {
+        arg::RefArg::append(&self.state, i);
+        arg::RefArg::append(&self.reason, i);
+    }
+}
+
+impl arg::ReadAll for OrgFreedesktopNetworkManagerConnectionActiveStateChanged {
+    fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
+        Ok(OrgFreedesktopNetworkManagerConnectionActiveStateChanged {
+            state: i.read()?,
+            reason: i.read()?,
+        })
+    }
+}
+
+impl dbus::message::SignalArgs for OrgFreedesktopNetworkManagerConnectionActiveStateChanged {
+    const NAME: &'static str = "StateChanged";
+    const INTERFACE: &'static str = "org.freedesktop.NetworkManager.Connection.Active";
+}
diff --git a/src/dbus_codegen/network_manager_device.rs b/src/dbus_codegen/network_manager_device.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3a7dad57f6576bee2f77c720a98486f8f1eea94d
--- /dev/null
+++ b/src/dbus_codegen/network_manager_device.rs
@@ -0,0 +1,226 @@
+// This code was autogenerated with `dbus-codegen-rust --file third_party/NetworkManager/introspection/org.freedesktop.NetworkManager.Device.xml -m None -c nonblock -o src/dbus_codegen/network_manager_device.rs`, see https://github.com/diwic/dbus-rs
+use dbus as dbus;
+#[allow(unused_imports)]
+use dbus::arg;
+use dbus::nonblock;
+
+pub trait OrgFreedesktopNetworkManagerDevice {
+    fn reapply(&self, connection: ::std::collections::HashMap<&str, arg::PropMap>, version_id: u64, flags: u32) -> nonblock::MethodReply<()>;
+    fn get_applied_connection(&self, flags: u32) -> nonblock::MethodReply<(::std::collections::HashMap<String, arg::PropMap>, u64)>;
+    fn disconnect(&self) -> nonblock::MethodReply<()>;
+    fn delete(&self) -> nonblock::MethodReply<()>;
+    fn udi(&self) -> nonblock::MethodReply<String>;
+    fn path(&self) -> nonblock::MethodReply<String>;
+    fn interface(&self) -> nonblock::MethodReply<String>;
+    fn ip_interface(&self) -> nonblock::MethodReply<String>;
+    fn driver(&self) -> nonblock::MethodReply<String>;
+    fn driver_version(&self) -> nonblock::MethodReply<String>;
+    fn firmware_version(&self) -> nonblock::MethodReply<String>;
+    fn capabilities(&self) -> nonblock::MethodReply<u32>;
+    fn ip4_address(&self) -> nonblock::MethodReply<u32>;
+    fn state(&self) -> nonblock::MethodReply<u32>;
+    fn state_reason(&self) -> nonblock::MethodReply<(u32, u32)>;
+    fn active_connection(&self) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn ip4_config(&self) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn dhcp4_config(&self) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn ip6_config(&self) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn dhcp6_config(&self) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn managed(&self) -> nonblock::MethodReply<bool>;
+    fn set_managed(&self, value: bool) -> nonblock::MethodReply<()>;
+    fn autoconnect(&self) -> nonblock::MethodReply<bool>;
+    fn set_autoconnect(&self, value: bool) -> nonblock::MethodReply<()>;
+    fn firmware_missing(&self) -> nonblock::MethodReply<bool>;
+    fn nm_plugin_missing(&self) -> nonblock::MethodReply<bool>;
+    fn device_type(&self) -> nonblock::MethodReply<u32>;
+    fn available_connections(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>>;
+    fn physical_port_id(&self) -> nonblock::MethodReply<String>;
+    fn mtu(&self) -> nonblock::MethodReply<u32>;
+    fn metered(&self) -> nonblock::MethodReply<u32>;
+    fn lldp_neighbors(&self) -> nonblock::MethodReply<Vec<arg::PropMap>>;
+    fn real(&self) -> nonblock::MethodReply<bool>;
+    fn ip4_connectivity(&self) -> nonblock::MethodReply<u32>;
+    fn ip6_connectivity(&self) -> nonblock::MethodReply<u32>;
+    fn interface_flags(&self) -> nonblock::MethodReply<u32>;
+    fn hw_address(&self) -> nonblock::MethodReply<String>;
+}
+
+impl<'a, T: nonblock::NonblockReply, C: ::std::ops::Deref<Target=T>> OrgFreedesktopNetworkManagerDevice for nonblock::Proxy<'a, C> {
+
+    fn reapply(&self, connection: ::std::collections::HashMap<&str, arg::PropMap>, version_id: u64, flags: u32) -> nonblock::MethodReply<()> {
+        self.method_call("org.freedesktop.NetworkManager.Device", "Reapply", (connection, version_id, flags, ))
+    }
+
+    fn get_applied_connection(&self, flags: u32) -> nonblock::MethodReply<(::std::collections::HashMap<String, arg::PropMap>, u64)> {
+        self.method_call("org.freedesktop.NetworkManager.Device", "GetAppliedConnection", (flags, ))
+    }
+
+    fn disconnect(&self) -> nonblock::MethodReply<()> {
+        self.method_call("org.freedesktop.NetworkManager.Device", "Disconnect", ())
+    }
+
+    fn delete(&self) -> nonblock::MethodReply<()> {
+        self.method_call("org.freedesktop.NetworkManager.Device", "Delete", ())
+    }
+
+    fn udi(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Udi")
+    }
+
+    fn path(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Path")
+    }
+
+    fn interface(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Interface")
+    }
+
+    fn ip_interface(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "IpInterface")
+    }
+
+    fn driver(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Driver")
+    }
+
+    fn driver_version(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "DriverVersion")
+    }
+
+    fn firmware_version(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "FirmwareVersion")
+    }
+
+    fn capabilities(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Capabilities")
+    }
+
+    fn ip4_address(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Ip4Address")
+    }
+
+    fn state(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "State")
+    }
+
+    fn state_reason(&self) -> nonblock::MethodReply<(u32, u32)> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "StateReason")
+    }
+
+    fn active_connection(&self) -> nonblock::MethodReply<dbus::Path<'static>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "ActiveConnection")
+    }
+
+    fn ip4_config(&self) -> nonblock::MethodReply<dbus::Path<'static>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Ip4Config")
+    }
+
+    fn dhcp4_config(&self) -> nonblock::MethodReply<dbus::Path<'static>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Dhcp4Config")
+    }
+
+    fn ip6_config(&self) -> nonblock::MethodReply<dbus::Path<'static>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Ip6Config")
+    }
+
+    fn dhcp6_config(&self) -> nonblock::MethodReply<dbus::Path<'static>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Dhcp6Config")
+    }
+
+    fn managed(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Managed")
+    }
+
+    fn autoconnect(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Autoconnect")
+    }
+
+    fn firmware_missing(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "FirmwareMissing")
+    }
+
+    fn nm_plugin_missing(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "NmPluginMissing")
+    }
+
+    fn device_type(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "DeviceType")
+    }
+
+    fn available_connections(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "AvailableConnections")
+    }
+
+    fn physical_port_id(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "PhysicalPortId")
+    }
+
+    fn mtu(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Mtu")
+    }
+
+    fn metered(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Metered")
+    }
+
+    fn lldp_neighbors(&self) -> nonblock::MethodReply<Vec<arg::PropMap>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "LldpNeighbors")
+    }
+
+    fn real(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Real")
+    }
+
+    fn ip4_connectivity(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Ip4Connectivity")
+    }
+
+    fn ip6_connectivity(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "Ip6Connectivity")
+    }
+
+    fn interface_flags(&self) -> nonblock::MethodReply<u32> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "InterfaceFlags")
+    }
+
+    fn hw_address(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Device", "HwAddress")
+    }
+
+    fn set_managed(&self, value: bool) -> nonblock::MethodReply<()> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::set(&self, "org.freedesktop.NetworkManager.Device", "Managed", value)
+    }
+
+    fn set_autoconnect(&self, value: bool) -> nonblock::MethodReply<()> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::set(&self, "org.freedesktop.NetworkManager.Device", "Autoconnect", value)
+    }
+}
+
+#[derive(Debug)]
+pub struct OrgFreedesktopNetworkManagerDeviceStateChanged {
+    pub new_state: u32,
+    pub old_state: u32,
+    pub reason: u32,
+}
+
+impl arg::AppendAll for OrgFreedesktopNetworkManagerDeviceStateChanged {
+    fn append(&self, i: &mut arg::IterAppend) {
+        arg::RefArg::append(&self.new_state, i);
+        arg::RefArg::append(&self.old_state, i);
+        arg::RefArg::append(&self.reason, i);
+    }
+}
+
+impl arg::ReadAll for OrgFreedesktopNetworkManagerDeviceStateChanged {
+    fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
+        Ok(OrgFreedesktopNetworkManagerDeviceStateChanged {
+            new_state: i.read()?,
+            old_state: i.read()?,
+            reason: i.read()?,
+        })
+    }
+}
+
+impl dbus::message::SignalArgs for OrgFreedesktopNetworkManagerDeviceStateChanged {
+    const NAME: &'static str = "StateChanged";
+    const INTERFACE: &'static str = "org.freedesktop.NetworkManager.Device";
+}
diff --git a/src/dbus_codegen/network_manager_settings.rs b/src/dbus_codegen/network_manager_settings.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e8746013e945b14de995ee2e2f32b5ab3ac6dae3
--- /dev/null
+++ b/src/dbus_codegen/network_manager_settings.rs
@@ -0,0 +1,119 @@
+// This code was autogenerated with `dbus-codegen-rust --file third_party/NetworkManager/introspection/org.freedesktop.NetworkManager.Settings.xml -m None -c nonblock -o src/dbus_codegen/network_manager_settings.rs`, see https://github.com/diwic/dbus-rs
+use dbus as dbus;
+#[allow(unused_imports)]
+use dbus::arg;
+use dbus::nonblock;
+
+pub trait OrgFreedesktopNetworkManagerSettings {
+    fn list_connections(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>>;
+    fn get_connection_by_uuid(&self, uuid: &str) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn add_connection(&self, connection: ::std::collections::HashMap<&str, arg::PropMap>) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn add_connection_unsaved(&self, connection: ::std::collections::HashMap<&str, arg::PropMap>) -> nonblock::MethodReply<dbus::Path<'static>>;
+    fn add_connection2(&self, settings: ::std::collections::HashMap<&str, arg::PropMap>, flags: u32, args: arg::PropMap) -> nonblock::MethodReply<(dbus::Path<'static>, arg::PropMap)>;
+    fn load_connections(&self, filenames: Vec<&str>) -> nonblock::MethodReply<(bool, Vec<String>)>;
+    fn reload_connections(&self) -> nonblock::MethodReply<bool>;
+    fn save_hostname(&self, hostname: &str) -> nonblock::MethodReply<()>;
+    fn connections(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>>;
+    fn hostname(&self) -> nonblock::MethodReply<String>;
+    fn can_modify(&self) -> nonblock::MethodReply<bool>;
+}
+
+impl<'a, T: nonblock::NonblockReply, C: ::std::ops::Deref<Target=T>> OrgFreedesktopNetworkManagerSettings for nonblock::Proxy<'a, C> {
+
+    fn list_connections(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>> {
+        self.method_call("org.freedesktop.NetworkManager.Settings", "ListConnections", ())
+            .and_then(|r: (Vec<dbus::Path<'static>>, )| Ok(r.0, ))
+    }
+
+    fn get_connection_by_uuid(&self, uuid: &str) -> nonblock::MethodReply<dbus::Path<'static>> {
+        self.method_call("org.freedesktop.NetworkManager.Settings", "GetConnectionByUuid", (uuid, ))
+            .and_then(|r: (dbus::Path<'static>, )| Ok(r.0, ))
+    }
+
+    fn add_connection(&self, connection: ::std::collections::HashMap<&str, arg::PropMap>) -> nonblock::MethodReply<dbus::Path<'static>> {
+        self.method_call("org.freedesktop.NetworkManager.Settings", "AddConnection", (connection, ))
+            .and_then(|r: (dbus::Path<'static>, )| Ok(r.0, ))
+    }
+
+    fn add_connection_unsaved(&self, connection: ::std::collections::HashMap<&str, arg::PropMap>) -> nonblock::MethodReply<dbus::Path<'static>> {
+        self.method_call("org.freedesktop.NetworkManager.Settings", "AddConnectionUnsaved", (connection, ))
+            .and_then(|r: (dbus::Path<'static>, )| Ok(r.0, ))
+    }
+
+    fn add_connection2(&self, settings: ::std::collections::HashMap<&str, arg::PropMap>, flags: u32, args: arg::PropMap) -> nonblock::MethodReply<(dbus::Path<'static>, arg::PropMap)> {
+        self.method_call("org.freedesktop.NetworkManager.Settings", "AddConnection2", (settings, flags, args, ))
+    }
+
+    fn load_connections(&self, filenames: Vec<&str>) -> nonblock::MethodReply<(bool, Vec<String>)> {
+        self.method_call("org.freedesktop.NetworkManager.Settings", "LoadConnections", (filenames, ))
+    }
+
+    fn reload_connections(&self) -> nonblock::MethodReply<bool> {
+        self.method_call("org.freedesktop.NetworkManager.Settings", "ReloadConnections", ())
+            .and_then(|r: (bool, )| Ok(r.0, ))
+    }
+
+    fn save_hostname(&self, hostname: &str) -> nonblock::MethodReply<()> {
+        self.method_call("org.freedesktop.NetworkManager.Settings", "SaveHostname", (hostname, ))
+    }
+
+    fn connections(&self) -> nonblock::MethodReply<Vec<dbus::Path<'static>>> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Settings", "Connections")
+    }
+
+    fn hostname(&self) -> nonblock::MethodReply<String> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Settings", "Hostname")
+    }
+
+    fn can_modify(&self) -> nonblock::MethodReply<bool> {
+        <Self as nonblock::stdintf::org_freedesktop_dbus::Properties>::get(&self, "org.freedesktop.NetworkManager.Settings", "CanModify")
+    }
+}
+
+#[derive(Debug)]
+pub struct OrgFreedesktopNetworkManagerSettingsNewConnection {
+    pub connection: dbus::Path<'static>,
+}
+
+impl arg::AppendAll for OrgFreedesktopNetworkManagerSettingsNewConnection {
+    fn append(&self, i: &mut arg::IterAppend) {
+        arg::RefArg::append(&self.connection, i);
+    }
+}
+
+impl arg::ReadAll for OrgFreedesktopNetworkManagerSettingsNewConnection {
+    fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
+        Ok(OrgFreedesktopNetworkManagerSettingsNewConnection {
+            connection: i.read()?,
+        })
+    }
+}
+
+impl dbus::message::SignalArgs for OrgFreedesktopNetworkManagerSettingsNewConnection {
+    const NAME: &'static str = "NewConnection";
+    const INTERFACE: &'static str = "org.freedesktop.NetworkManager.Settings";
+}
+
+#[derive(Debug)]
+pub struct OrgFreedesktopNetworkManagerSettingsConnectionRemoved {
+    pub connection: dbus::Path<'static>,
+}
+
+impl arg::AppendAll for OrgFreedesktopNetworkManagerSettingsConnectionRemoved {
+    fn append(&self, i: &mut arg::IterAppend) {
+        arg::RefArg::append(&self.connection, i);
+    }
+}
+
+impl arg::ReadAll for OrgFreedesktopNetworkManagerSettingsConnectionRemoved {
+    fn read(i: &mut arg::Iter) -> Result<Self, arg::TypeMismatchError> {
+        Ok(OrgFreedesktopNetworkManagerSettingsConnectionRemoved {
+            connection: i.read()?,
+        })
+    }
+}
+
+impl dbus::message::SignalArgs for OrgFreedesktopNetworkManagerSettingsConnectionRemoved {
+    const NAME: &'static str = "ConnectionRemoved";
+    const INTERFACE: &'static str = "org.freedesktop.NetworkManager.Settings";
+}
diff --git a/src/dbus_wrappers/active_connection.rs b/src/dbus_wrappers/active_connection.rs
new file mode 100644
index 0000000000000000000000000000000000000000..79261a2571b8e832a3e3fada5e72e34bf9490081
--- /dev/null
+++ b/src/dbus_wrappers/active_connection.rs
@@ -0,0 +1,65 @@
+use std::{sync::Arc, time::Duration};
+
+use anyhow::Result;
+use dbus::{
+    nonblock::{Proxy, SyncConnection},
+    Path,
+};
+
+use crate::dbus_codegen::network_manager_connection_active::{
+    OrgFreedesktopNetworkManagerConnectionActive,
+    OrgFreedesktopNetworkManagerConnectionActiveStateChanged,
+};
+
+use super::signal::SignalStreamWrapper;
+
+pub struct ActiveConnectionWrapper<'a> {
+    conn: Arc<SyncConnection>,
+    proxy: Box<dyn OrgFreedesktopNetworkManagerConnectionActive + Send + Sync + 'a>,
+    path: Path<'a>,
+}
+
+impl<'a> ActiveConnectionWrapper<'a> {
+    pub async fn from_path<P>(conn: Arc<SyncConnection>, path: P) -> ActiveConnectionWrapper<'a>
+    where
+        P: Into<dbus::Path<'a>>,
+    {
+        let path = path.into();
+
+        let proxy: Proxy<'a, Arc<SyncConnection>> = Proxy::new(
+            "org.freedesktop.NetworkManager",
+            path.clone(),
+            Duration::from_millis(5000),
+            conn.clone(),
+        );
+
+        ActiveConnectionWrapper {
+            conn,
+            proxy: Box::new(proxy),
+            path,
+        }
+    }
+
+    pub fn get_path(&self) -> Path<'a> {
+        self.path.clone()
+    }
+
+    pub async fn get_uuid(&self) -> Result<String> {
+        let uuid = self.proxy.uuid().await?;
+
+        Ok(uuid)
+    }
+
+    pub async fn get_device_paths(&self) -> Result<Vec<Path<'static>>> {
+        let device_paths = self.proxy.devices().await?;
+
+        Ok(device_paths)
+    }
+
+    pub async fn state_changed_signal_stream(
+        &self,
+    ) -> anyhow::Result<SignalStreamWrapper<OrgFreedesktopNetworkManagerConnectionActiveStateChanged>>
+    {
+        SignalStreamWrapper::from_match_rule(&self.conn, None, Some(self.path.clone())).await
+    }
+}
diff --git a/src/dbus_wrappers/connection.rs b/src/dbus_wrappers/connection.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f56cea4641715c4cec9315cc68041506118b30a9
--- /dev/null
+++ b/src/dbus_wrappers/connection.rs
@@ -0,0 +1,40 @@
+use std::{sync::Arc, time::Duration};
+
+use anyhow::Result;
+use dbus::{Path, nonblock::{Proxy, SyncConnection}};
+
+use crate::dbus_codegen::{network_manager_connection_active::{OrgFreedesktopNetworkManagerConnectionActive, OrgFreedesktopNetworkManagerConnectionActiveStateChanged}};
+
+use super::signal::SignalStreamWrapper;
+
+pub struct ConnectionWrapper<'a> {
+    conn: Arc<SyncConnection>,
+    proxy: Box<dyn OrgFreedesktopNetworkManagerConnectionActive + Send + Sync + 'a>,
+    path: Path<'a>
+}
+
+impl<'a> ConnectionWrapper<'a> {
+    pub async fn from_path<P>(conn: Arc<SyncConnection>, path: P) -> ConnectionWrapper<'a>
+    where
+        P: Into<dbus::Path<'a>>,
+    {
+        let path = path.into();
+
+        let proxy: Proxy<'a, Arc<SyncConnection>> = Proxy::new(
+            "org.freedesktop.NetworkManager",
+            path.clone(),
+            Duration::from_millis(5000),
+            conn.clone(),
+        );
+
+        ConnectionWrapper {
+            conn,
+            proxy: Box::new(proxy),
+            path
+        }
+    }
+
+    pub fn get_path(&self) -> Path<'a> {
+        self.path.clone()
+    }
+}
diff --git a/src/dbus_wrappers/device.rs b/src/dbus_wrappers/device.rs
new file mode 100644
index 0000000000000000000000000000000000000000..84579d7692a7d18031f3ae5969299919212bc034
--- /dev/null
+++ b/src/dbus_wrappers/device.rs
@@ -0,0 +1,75 @@
+use std::{convert::TryFrom, sync::Arc, time::Duration};
+
+use anyhow::Result;
+use dbus::{
+    nonblock::{Proxy, SyncConnection},
+    Path,
+};
+use num_traits::FromPrimitive;
+
+use crate::dbus_codegen::network_manager_device::{
+    OrgFreedesktopNetworkManagerDevice, OrgFreedesktopNetworkManagerDeviceStateChanged,
+};
+
+use super::{
+    active_connection::ActiveConnectionWrapper, device_state::DeviceState,
+    signal::SignalStreamWrapper,
+};
+
+pub struct DeviceWrapper<'a> {
+    conn: Arc<SyncConnection>,
+    proxy: Box<dyn OrgFreedesktopNetworkManagerDevice + Send + Sync + 'a>,
+    path: Path<'a>,
+}
+
+impl<'a> DeviceWrapper<'a> {
+    pub async fn from_path<P>(conn: Arc<SyncConnection>, path: P) -> DeviceWrapper<'a>
+    where
+        P: Into<dbus::Path<'a>>,
+    {
+        let path = path.into();
+
+        let proxy: Proxy<'a, Arc<SyncConnection>> = Proxy::new(
+            "org.freedesktop.NetworkManager",
+            path.clone(),
+            Duration::from_millis(5000),
+            conn.clone(),
+        );
+
+        DeviceWrapper {
+            conn: conn.clone(),
+            proxy: Box::new(proxy),
+            path,
+        }
+    }
+
+    pub fn get_path(&self) -> Path<'a> {
+        self.path.clone()
+    }
+
+    pub async fn get_active_connection(&self) -> Result<Option<ActiveConnectionWrapper<'static>>> {
+        let path = self.proxy.active_connection().await?;
+
+        if path == Path::from("/") {
+            return Ok(None);
+        }
+
+        let connection = ActiveConnectionWrapper::from_path(self.conn.clone(), path).await;
+
+        Ok(Some(connection))
+    }
+
+    pub async fn state_changed_signal_stream(
+        &self,
+    ) -> anyhow::Result<SignalStreamWrapper<OrgFreedesktopNetworkManagerDeviceStateChanged>> {
+        SignalStreamWrapper::from_match_rule(&self.conn, None, Some(self.path.clone())).await
+    }
+
+    pub async fn get_state(&self) -> Result<Option<DeviceState>> {
+        let result = self.proxy.state().await?;
+
+        let result = FromPrimitive::from_u32(result);
+
+        Ok(result)
+    }
+}
diff --git a/src/dbus_wrappers/device_state.rs b/src/dbus_wrappers/device_state.rs
new file mode 100644
index 0000000000000000000000000000000000000000..cdd07277fc39465d828d4355ad74991f040358e7
--- /dev/null
+++ b/src/dbus_wrappers/device_state.rs
@@ -0,0 +1,20 @@
+use num_derive::FromPrimitive;
+use serde_derive::{Deserialize, Serialize};
+
+#[derive(FromPrimitive, PartialEq, Eq, Hash, Serialize, Deserialize, Clone, Debug)]
+#[repr(u32)]
+pub enum DeviceState {
+    Unknown = 0,
+    Unmanaged = 10,
+    Unavailable = 20,
+    Disconnected = 30,
+    Prepare = 40,
+    Config = 50,
+    NeedAuth = 60,
+    IPConfig = 70,
+    IPCheck = 80,
+    Secondaries = 90,
+    Activated = 100,
+    Deactivating = 110,
+    Failed = 120,
+}
\ No newline at end of file
diff --git a/src/dbus_wrappers/manager.rs b/src/dbus_wrappers/manager.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c45872a206befff0adc95414f3d7d79dc88776a6
--- /dev/null
+++ b/src/dbus_wrappers/manager.rs
@@ -0,0 +1,148 @@
+use std::{sync::Arc, time::Duration};
+
+use dbus::{
+    nonblock::{Proxy, SyncConnection},
+    Path,
+};
+use futures::{future::join_all, stream, StreamExt};
+
+use crate::dbus_codegen::network_manager::{
+    OrgFreedesktopNetworkManager, OrgFreedesktopNetworkManagerDeviceAdded,
+    OrgFreedesktopNetworkManagerDeviceRemoved,
+};
+
+use super::{
+    active_connection::ActiveConnectionWrapper, connection::ConnectionWrapper,
+    device::DeviceWrapper, signal::SignalStreamWrapper,
+};
+
+pub struct ManagerWrapper<'a> {
+    conn: Arc<SyncConnection>,
+    inner: Box<dyn OrgFreedesktopNetworkManager + Send + Sync + 'a>,
+}
+
+impl<'a> ManagerWrapper<'a> {
+    pub async fn from_connection(conn: &Arc<SyncConnection>) -> ManagerWrapper<'a> {
+        let inner_conn = conn.clone();
+
+        let proxy: Proxy<'a, Arc<SyncConnection>> = Proxy::new(
+            "org.freedesktop.NetworkManager",
+            "/org/freedesktop/NetworkManager",
+            Duration::from_millis(5000),
+            inner_conn,
+        );
+
+        ManagerWrapper {
+            conn: conn.clone(),
+            inner: Box::new(proxy),
+        }
+    }
+
+    pub async fn get_device_path_by_ip_iface(&self, iface: &str) -> anyhow::Result<Path<'_>> {
+        let device_path = self.inner.get_device_by_ip_iface(iface).await?;
+
+        Ok(device_path)
+    }
+
+    pub async fn get_device_by_ip_iface(&self, iface: &str) -> anyhow::Result<DeviceWrapper<'_>> {
+        let device_path = self.get_device_path_by_ip_iface(iface).await?;
+
+        Ok(DeviceWrapper::from_path(self.conn.clone(), device_path.clone()).await)
+    }
+
+    pub async fn get_all_device_paths(&self) -> anyhow::Result<Vec<Path<'a>>> {
+        let device_paths = self.inner.get_all_devices().await?;
+
+        Ok(device_paths)
+    }
+
+    pub async fn get_active_connections(
+        &self,
+    ) -> anyhow::Result<Vec<ActiveConnectionWrapper<'static>>> {
+        let active_connection_paths = self.inner.active_connections().await?;
+
+        let connections = active_connection_paths
+            .iter()
+            .map(|active_connection_path| {
+                ActiveConnectionWrapper::from_path(
+                    self.conn.clone(),
+                    Path::from(active_connection_path.to_string()),
+                )
+            });
+
+        Ok(join_all(connections).await)
+    }
+
+    pub async fn get_active_connection_from_uuid(
+        &self,
+        uuid: &str,
+    ) -> anyhow::Result<Option<ActiveConnectionWrapper<'static>>> {
+        let active_connections = self.get_active_connections().await?;
+
+        let stream = stream::iter(active_connections);
+
+        let mut matches: Vec<ActiveConnectionWrapper> = stream
+            .filter_map(|active_connection| async move {
+                let current_uuid = active_connection.get_uuid().await;
+
+                match current_uuid {
+                    Ok(current_uuid) => {
+                        if current_uuid == uuid {
+                            Some(active_connection)
+                        } else {
+                            None
+                        }
+                    }
+                    Err(_) => None,
+                }
+            })
+            .collect()
+            .await;
+
+        Ok(matches.pop())
+    }
+
+    pub async fn activate_connection(
+        &self,
+        connection: Option<&'a ConnectionWrapper<'a>>,
+        device: Option<&'a DeviceWrapper<'a>>,
+        specific_object: Option<Path<'a>>,
+    ) -> anyhow::Result<ActiveConnectionWrapper<'static>> {
+        let active_connection_path = self
+            .inner
+            .activate_connection(
+                connection.map_or(Path::from("/"), |c| c.get_path()),
+                device.map_or(Path::from("/"), |d| d.get_path()),
+                specific_object.unwrap_or(Path::from("/")),
+            )
+            .await?;
+
+        let active_connection =
+            ActiveConnectionWrapper::from_path(self.conn.clone(), active_connection_path).await;
+
+        Ok(active_connection)
+    }
+
+    pub async fn deactive_connection(
+        &self,
+        active_connection: &ActiveConnectionWrapper<'a>,
+    ) -> anyhow::Result<()> {
+        self.inner
+            .deactivate_connection(active_connection.get_path())
+            .await?;
+
+        Ok(())
+    }
+
+    pub async fn device_added_signal_stream(
+        &self,
+    ) -> anyhow::Result<SignalStreamWrapper<OrgFreedesktopNetworkManagerDeviceAdded>> {
+        SignalStreamWrapper::from_match_rule(&self.conn, None, None).await
+    }
+
+    pub async fn device_removed_signal_stream(
+        &self,
+    ) -> anyhow::Result<SignalStreamWrapper<OrgFreedesktopNetworkManagerDeviceRemoved>> {
+        SignalStreamWrapper::from_match_rule(&self.conn, None, None).await
+    }
+}
diff --git a/src/dbus_wrappers/manager_settings.rs b/src/dbus_wrappers/manager_settings.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e23bfaa2f05a16a551b227aa0146691830a1e24a
--- /dev/null
+++ b/src/dbus_wrappers/manager_settings.rs
@@ -0,0 +1,48 @@
+use std::{sync::Arc, time::Duration};
+
+use anyhow::Result;
+use dbus::{
+    nonblock::{Proxy, SyncConnection},
+    Path,
+};
+
+use crate::dbus_codegen::network_manager_settings::OrgFreedesktopNetworkManagerSettings;
+
+use super::connection::ConnectionWrapper;
+
+pub struct ManagerSettingsWrapper<'a> {
+    conn: Arc<SyncConnection>,
+    proxy: Box<dyn OrgFreedesktopNetworkManagerSettings + Send + Sync + 'a>,
+}
+
+impl<'a> ManagerSettingsWrapper<'a> {
+    pub async fn from_connection(conn: &Arc<SyncConnection>) -> ManagerSettingsWrapper<'a>
+    {
+        let proxy: Proxy<'a, Arc<SyncConnection>> = Proxy::new(
+            "org.freedesktop.NetworkManager",
+            "/org/freedesktop/NetworkManager/Settings",
+            Duration::from_millis(5000),
+            conn.clone(),
+        );
+
+        ManagerSettingsWrapper {
+            conn: conn.clone(),
+            proxy: Box::new(proxy),
+        }
+    }
+
+    pub async fn get_connection_from_uuid(
+        &self,
+        uuid: &str,
+    ) -> Result<Option<ConnectionWrapper<'static>>> {
+        let path = self.proxy.get_connection_by_uuid(uuid).await?;
+
+        if path == Path::from("/") {
+            return Ok(None);
+        }
+
+        let connection = ConnectionWrapper::from_path(self.conn.clone(), path).await;
+
+        Ok(Some(connection))
+    }
+}
diff --git a/src/dbus_wrappers/mod.rs b/src/dbus_wrappers/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..6c9a830e01ec1e3474a139e4cf70db0f3d829634
--- /dev/null
+++ b/src/dbus_wrappers/mod.rs
@@ -0,0 +1,7 @@
+pub mod active_connection;
+pub mod connection;
+pub mod device;
+pub mod device_state;
+pub mod manager;
+pub mod manager_settings;
+pub mod signal;
\ No newline at end of file
diff --git a/src/dbus_wrappers/signal.rs b/src/dbus_wrappers/signal.rs
new file mode 100644
index 0000000000000000000000000000000000000000..b7cc0b2658538c7f00fb8657bedc2508d738a936
--- /dev/null
+++ b/src/dbus_wrappers/signal.rs
@@ -0,0 +1,59 @@
+use std::sync::Arc;
+
+use dbus::{Message, MessageType, Path, arg::ReadAll, message::{MatchRule, SignalArgs}, nonblock::{MsgMatch, SyncConnection}, strings::BusName};
+use futures_channel::mpsc::UnboundedReceiver;
+use tokio_stream::{StreamExt, };
+
+pub struct SignalStreamWrapper<T> {
+    conn: Arc<SyncConnection>,
+    msg_match: MsgMatch,
+    stream: UnboundedReceiver<(Message, T)>,
+}
+
+impl<T> SignalStreamWrapper<T> {
+    pub fn from_stream(
+        conn: &Arc<SyncConnection>,
+        msg_match: MsgMatch,
+        stream: UnboundedReceiver<(Message, T)>,
+    ) -> SignalStreamWrapper<T> {
+        SignalStreamWrapper {
+            conn: conn.clone(),
+            msg_match,
+            stream,
+        }
+    }
+
+    pub async fn from_match_rule(
+        conn: &Arc<SyncConnection>,
+        sender: Option<BusName<'_>>,
+        path: Option<Path<'_>>,
+    ) -> anyhow::Result<SignalStreamWrapper<T>> where T: ReadAll + Send + SignalArgs + 'static {
+        let mut match_rule = MatchRule::default();
+
+        match_rule.sender = sender.map(|s| s.into_static());
+        match_rule.path = path.map(|p| p.into_static());
+        match_rule.msg_type = Some(MessageType::Signal);
+        match_rule.interface = Some(T::INTERFACE.into());
+        match_rule.member = Some(T::NAME.into());
+
+        let (msg_match, stream) = 
+            conn
+            .add_match(match_rule)
+            .await?
+            .stream::<T>();
+
+        Ok(Self::from_stream(conn, msg_match, stream))
+    }
+
+    pub async fn next(&mut self) -> Option<(Message, T)> {
+        self.stream.next().await
+    }
+
+    pub async fn dispose(&self) -> anyhow::Result<()> {
+        let result = self.conn
+            .remove_match(self.msg_match.token())
+            .await?;
+
+        Ok(result)
+    }
+}
\ No newline at end of file
diff --git a/src/device_watcher.rs b/src/device_watcher.rs
new file mode 100644
index 0000000000000000000000000000000000000000..09c57243fd8324ba912af38b1dae14a08767a61e
--- /dev/null
+++ b/src/device_watcher.rs
@@ -0,0 +1,66 @@
+use std::sync::Arc;
+
+use anyhow::Result;
+use dbus::{nonblock::SyncConnection, Path};
+use num_traits::FromPrimitive;
+use tokio::{sync::{broadcast, mpsc}, task::JoinHandle};
+
+use crate::{dbus_wrappers::{device::DeviceWrapper, device_state::DeviceState}, event::Event};
+
+pub async fn watch_device<'a>(
+    conn: &Arc<SyncConnection>,
+    mut stop_signal_rx: broadcast::Receiver<()>,
+    event_tx: mpsc::Sender<Event>,
+    device_path: Path<'a>,
+) -> Result<JoinHandle<Result<()>>> {
+    log::debug!("Setting up signal handlers for {}", device_path);
+
+    let conn_for_task = conn.clone();
+    let device_path_for_task = device_path.into_static();
+
+    let handler_handle: JoinHandle<Result<()>> = tokio::spawn(async move {
+        let device = DeviceWrapper::from_path(conn_for_task.clone(), device_path_for_task).await;
+
+        let mut state_changed_signal = device.state_changed_signal_stream().await?;
+
+        loop {
+            tokio::select! {
+                Some((msg, signal)) = state_changed_signal.next() => {
+                    log::debug!("Got StateChanged signal for {}: {:?}", device.get_path(), &msg);
+                    
+                    match FromPrimitive::from_u32(signal.new_state) {
+                        Some(DeviceState::Activated) => {
+                            event_tx.send(Event::DeviceActivated {
+                                device_path: device.get_path(),
+                            }).await?;
+                        }
+                        Some(DeviceState::Deactivating) => {
+                            event_tx.send(Event::DeviceDeactivating {
+                                device_path: device.get_path(),
+                            }).await?;
+                        }
+                        Some(DeviceState::Disconnected) => {
+                            event_tx.send(Event::DeviceDisconnected {
+                                device_path: device.get_path(),
+                            }).await?;
+                        }
+                        _ => {}
+                    }
+                },
+                _ = stop_signal_rx.recv() => {
+                    log::debug!("Stoping device watcher for {}", device.get_path());
+
+                    break;
+                }
+            }
+        }
+
+        log::debug!("Removing signal handlers for {}", device.get_path());
+
+        state_changed_signal.dispose().await?;
+
+        Ok(())
+    });
+
+    Ok(handler_handle)
+}
diff --git a/src/event.rs b/src/event.rs
new file mode 100644
index 0000000000000000000000000000000000000000..5f3c68c7739b0649269109f8b43e00d6ad77a1bd
--- /dev/null
+++ b/src/event.rs
@@ -0,0 +1,144 @@
+use std::sync::Arc;
+
+use anyhow::Result;
+use dbus::nonblock::SyncConnection;
+use serde_derive::{Deserialize, Serialize};
+use tokio::{
+    sync::{broadcast, mpsc},
+    task::JoinHandle,
+};
+
+use crate::{config::Config, identifier::DeviceIdentifier};
+
+#[derive(Debug)]
+pub enum Event {
+    DeviceAdded { device_path: dbus::Path<'static> },
+    DeviceRemoved { device_path: dbus::Path<'static> },
+    DeviceActivated { device_path: dbus::Path<'static> },
+    DeviceDeactivating { device_path: dbus::Path<'static> },
+    DeviceDisconnected { device_path: dbus::Path<'static> },
+}
+
+pub async fn handle_events(
+    conn: Arc<SyncConnection>,
+    config: Arc<Config>,
+    mut stop_signal_rx: broadcast::Receiver<()>,
+    mut event_rx: mpsc::Receiver<Event>,
+) -> Result<JoinHandle<Result<()>>> {
+    log::info!("Starting event handler task");
+
+    let join_handle = tokio::spawn(async move {
+        loop {
+            tokio::select! {
+                event = event_rx.recv() => {
+                    match event {
+                        Some(event) => {
+                            log::debug!("Got event: {:?}", &event);
+
+                            for rule in &config.rules {
+                                if let Err(err) = rule.evaluate(&conn, &event).await {
+                                    log::error!("Got error evaluating rule: {:?} {:?}", rule, err);
+                                };
+                            }
+                        }
+                        None => {
+                            log::warn!("Got an empty event");
+                        }
+                    }
+                    
+                },
+                _ = stop_signal_rx.recv() => {
+                    log::info!("Stoping event handler task");
+
+                    break;
+                }
+            }
+        }
+
+        Ok(())
+    });
+
+    Ok(join_handle)
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(tag = "type")]
+pub enum Trigger {
+    DeviceAdded {
+        device_identifier: Option<DeviceIdentifier>,
+    },
+    DeviceRemoved {
+        device_identifier: Option<DeviceIdentifier>,
+    },
+    DeviceActivated {
+        device_identifier: Option<DeviceIdentifier>,
+    },
+    DeviceDeactivating {
+        device_identifier: Option<DeviceIdentifier>,
+    },
+    DeviceDisconnected {
+        device_identifier: Option<DeviceIdentifier>,
+    },
+}
+
+impl Trigger {
+    pub async fn matches_event(&self, conn: &Arc<SyncConnection>, event: &Event) -> Result<bool> {
+        match (event, self) {
+            (Event::DeviceAdded { device_path }, Trigger::DeviceAdded { device_identifier }) => {
+                match device_identifier {
+                    Some(device_identified) => {
+                        let trigger_device_path = device_identified.into_path(conn).await?;
+
+                        Ok(device_path.eq(&trigger_device_path))
+                    }
+                    None => Ok(true),
+                }
+            }
+            (
+                Event::DeviceRemoved { device_path },
+                Trigger::DeviceRemoved { device_identifier },
+            ) => match device_identifier {
+                Some(device_identified) => {
+                    let trigger_device_path = device_identified.into_path(conn).await?;
+
+                    Ok(device_path.eq(&trigger_device_path))
+                }
+                None => Ok(true),
+            },
+            (
+                Event::DeviceActivated { device_path },
+                Trigger::DeviceActivated { device_identifier },
+            ) => match device_identifier {
+                Some(device_identified) => {
+                    let trigger_device_path = device_identified.into_path(conn).await?;
+
+                    Ok(device_path.eq(&trigger_device_path))
+                }
+                None => Ok(true),
+            },
+            (
+                Event::DeviceDeactivating { device_path },
+                Trigger::DeviceDeactivating { device_identifier },
+            ) => match device_identifier {
+                Some(device_identified) => {
+                    let trigger_device_path = device_identified.into_path(conn).await?;
+
+                    Ok(device_path.eq(&trigger_device_path))
+                }
+                None => Ok(true),
+            },
+            (
+                Event::DeviceDisconnected { device_path },
+                Trigger::DeviceDisconnected { device_identifier },
+            ) => match device_identifier {
+                Some(device_identified) => {
+                    let trigger_device_path = device_identified.into_path(conn).await?;
+
+                    Ok(device_path.eq(&trigger_device_path))
+                }
+                None => Ok(true),
+            },
+            _ => Ok(false),
+        }
+    }
+}
diff --git a/src/identifier.rs b/src/identifier.rs
new file mode 100644
index 0000000000000000000000000000000000000000..869d6d0e3809e47cd41b31c22d23680e96b08d39
--- /dev/null
+++ b/src/identifier.rs
@@ -0,0 +1,115 @@
+use std::sync::Arc;
+
+use anyhow::Result;
+use dbus::{nonblock::SyncConnection, Path};
+use serde_derive::{Deserialize, Serialize};
+
+use crate::dbus_wrappers::{
+    active_connection::ActiveConnectionWrapper, connection::ConnectionWrapper,
+    device::DeviceWrapper, manager::ManagerWrapper, manager_settings::ManagerSettingsWrapper,
+};
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(tag = "type")]
+pub enum DeviceIdentifier {
+    DeviceInterface { device_interface: String },
+    DevicePath { device_path: String },
+}
+
+impl DeviceIdentifier {
+    pub async fn into_path(&self, conn: &Arc<SyncConnection>) -> Result<Path<'static>> {
+        match self {
+            DeviceIdentifier::DeviceInterface { device_interface } => {
+                let manager = ManagerWrapper::from_connection(conn).await;
+                let device_path = manager
+                    .get_device_path_by_ip_iface(&device_interface)
+                    .await?;
+
+                let owned_path = device_path.into_static();
+
+                Ok(owned_path)
+            }
+            DeviceIdentifier::DevicePath { device_path } => Ok(Path::from(device_path.clone())),
+        }
+    }
+
+    pub async fn into_device(&self, conn: &Arc<SyncConnection>) -> Result<DeviceWrapper<'static>> {
+        let device_path = self.into_path(conn).await?;
+
+        let device = DeviceWrapper::from_path(conn.clone(), device_path).await;
+
+        Ok(device)
+    }
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(tag = "type")]
+pub enum ConnectionIdentifier {
+    ConnectionPath { connection_path: String },
+    ConnectionUUID { connection_uuid: String },
+}
+
+impl ConnectionIdentifier {
+    pub async fn into_connection<'a>(
+        &'a self,
+        conn: &Arc<SyncConnection>,
+    ) -> Result<Option<ConnectionWrapper<'a>>> {
+        match self {
+            ConnectionIdentifier::ConnectionPath { connection_path } => {
+                let connection = ConnectionWrapper::from_path(conn.clone(), connection_path).await;
+
+                Ok(Some(connection))
+            }
+            ConnectionIdentifier::ConnectionUUID { connection_uuid } => {
+                let manager_settings = ManagerSettingsWrapper::from_connection(conn).await;
+                let connection = manager_settings
+                    .get_connection_from_uuid(&connection_uuid)
+                    .await?;
+
+                Ok(connection)
+            }
+        }
+    }
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+#[serde(tag = "type")]
+pub enum ActiveConnectionIdentifier {
+    ConnectionUUID { connection_uuid: String },
+    ConnectionPath { connection_path: String },
+    DeviceInterface { device_interface: String },
+}
+
+impl ActiveConnectionIdentifier {
+    pub async fn into_active_connection<'a>(
+        &'a self,
+        conn: &Arc<SyncConnection>,
+    ) -> Result<Option<ActiveConnectionWrapper<'a>>> {
+        match self {
+            ActiveConnectionIdentifier::ConnectionUUID { connection_uuid } => {
+                let manager = ManagerWrapper::from_connection(conn).await;
+                let active_connection = manager
+                    .get_active_connection_from_uuid(&connection_uuid)
+                    .await?;
+
+                Ok(active_connection)
+            }
+            ActiveConnectionIdentifier::ConnectionPath { connection_path } => {
+                let active_connection =
+                    ActiveConnectionWrapper::from_path(conn.clone(), connection_path).await;
+
+                Ok(Some(active_connection))
+            }
+            ActiveConnectionIdentifier::DeviceInterface { device_interface } => {
+                let manager = ManagerWrapper::from_connection(conn).await;
+                let device_path = manager
+                    .get_device_path_by_ip_iface(&device_interface)
+                    .await?;
+
+                let device = DeviceWrapper::from_path(conn.clone(), device_path).await;
+
+                device.get_active_connection().await
+            }
+        }
+    }
+}
diff --git a/src/main.rs b/src/main.rs
index d8a4dc07a654dde4c4c36fd464983e55aefd45f2..7d3db2170d12df4e3531993a729a80ecc68ccc5a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,88 +1,100 @@
-use std::{
-    env,
-    io::{self, Write},
-    process::Command,
+use collective::cli::{AppOpts, ConfigurableAppOpts};
+use dbus_tokio::connection;
+use std::{path::PathBuf, sync::Arc};
+use tokio::{
+    signal,
+    sync::{broadcast, mpsc},
 };
 
 use anyhow::Result;
+use clap::Parser;
 
-fn main() -> Result<()> {
-    pretty_env_logger::init();
-
-    let args: Vec<String> = env::args().collect();
-
-    let wireguard_interface = "wg0";
-    let target_interfaces = ["wlp3s0"];
-    let excluded_connections = ["481d0de5-0ba5-4181-99b2-386687be4055"];
-
-    match &args[..] {
-        [_, interface, status] => {
-            if target_interfaces.iter().any(|x| x == interface) {
-                match status.as_str() {
-                    "up" => {
-                        let connection_uuid = env::var("CONNECTION_UUID")?;
-
-                        if excluded_connections.iter().any(|x| x == &connection_uuid) {
-                            apply_interface_connection(
-                                wireguard_interface,
-                                ConnectionAction::Down,
-                            )?;
-                        } else {
-                            apply_interface_connection(wireguard_interface, ConnectionAction::Up)?;
-                        }
-
-                        Ok(())
-                    }
-                    "down" => {
-                        apply_interface_connection(wireguard_interface, ConnectionAction::Down)?;
-
-                        Ok(())
-                    }
-                    status => {
-                        log::warn!("Got an unexpected connection status: {}", status);
-
-                        Ok(())
-                    }
-                }
-            } else {
-                log::info!("Ignoring interface: {}", interface);
-
-                Ok(())
-            }
-        }
-        _ => {
-            log::warn!("Got an unexpected number of arguments");
+use crate::event::handle_events;
+
+mod action;
+mod condition;
+mod config;
+mod dbus_codegen;
+mod dbus_wrappers;
+mod device_watcher;
+mod event;
+mod identifier;
+mod rule;
+mod watcher;
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    inner_main().await?;
+
+    Ok(())
+}
+
+#[derive(Parser)]
+#[clap(version = "1.0", author = "Eduardo T. <ed@trujillo.io>")]
+struct Opts {
+    /// Sets a custom config file.
+    #[clap(short, long)]
+    config: Option<PathBuf>,
+    /// A level of verbosity, and can be used multiple times
+    #[clap(short, long, parse(from_occurrences))]
+    verbose: i32,
+}
 
-            Ok(())
+impl AppOpts for Opts {
+    fn get_log_level_filter(&self) -> Option<log::LevelFilter> {
+        match self.verbose {
+            3 => Some(log::LevelFilter::Trace),
+            2 => Some(log::LevelFilter::Debug),
+            1 => Some(log::LevelFilter::Info),
+            _ => None,
         }
     }
 }
 
-enum ConnectionAction {
-    Up,
-    Down,
+impl ConfigurableAppOpts<config::Config> for Opts {
+    fn get_additional_config_paths(&self) -> Vec<PathBuf> {
+        if let Some(config_path) = &self.config {
+            vec![config_path.clone()]
+        } else {
+            vec![]
+        }
+    }
 }
 
-fn apply_interface_connection(interface: &str, action: ConnectionAction) -> Result<()> {
-    let action = match action {
-        ConnectionAction::Up => {
-            log::info!("Bringing up: {}", interface);
+async fn inner_main() -> anyhow::Result<()> {
+    let (opts, config) = Opts::try_init_with_config()?;
+    let config = Arc::new(config);
 
-            "up"
-        }
-        ConnectionAction::Down => {
-            log::info!("Bringing down: {}", interface);
+    log::info!("Parsed config and {} rules", config.rules.len());
 
-            "down"
-        }
-    };
+    let (resource, conn) = connection::new_system_sync()?;
+
+    log::info!("Connected to DBus");
+
+    let _handle = tokio::spawn(async {
+        let err = resource.await;
+        panic!("Lost connection to D-Bus: {}", err);
+    });
+
+    let (stop_signal_tx, _stop_signal_rx) = broadcast::channel(1);
+    let (event_tx, event_rx) = mpsc::channel(50);
 
-    let output = Command::new("nmcli")
-        .args(&["connection", action, interface])
-        .output()?;
+    let mut task_handles = vec![];
 
-    io::stdout().write_all(&output.stdout)?;
-    io::stderr().write_all(&output.stderr)?;
+    let watcher_handle = watcher::watch(&conn, stop_signal_tx.subscribe(), event_tx).await?;
+    let event_handler_handle =
+        handle_events(conn.clone(), config, stop_signal_tx.subscribe(), event_rx).await?;
+
+    task_handles.push(watcher_handle);
+    task_handles.push(event_handler_handle);
+
+    signal::ctrl_c().await?;
+
+    stop_signal_tx.send(())?;
+
+    for task_handle in task_handles {
+        task_handle.await??;
+    }
 
     Ok(())
 }
diff --git a/src/rule.rs b/src/rule.rs
new file mode 100644
index 0000000000000000000000000000000000000000..65a2109db08fd6322f4b67865d204096c39ed03b
--- /dev/null
+++ b/src/rule.rs
@@ -0,0 +1,73 @@
+use std::sync::Arc;
+
+use anyhow::Result;
+use dbus::nonblock::SyncConnection;
+use futures::stream::{self, StreamExt};
+use serde_derive::{Deserialize, Serialize};
+
+use crate::{
+    action::Action,
+    condition::Condition,
+    event::{Event, Trigger},
+};
+
+#[derive(Serialize, Deserialize, Clone, Debug)]
+pub struct Rule {
+    // Defines which events should trigger this rule.
+    //
+    // Multiple triggers are evaluated using an OR operator.
+    pub triggers: Vec<Trigger>,
+    // Defines the conditions that should pass in order to perform any actions.
+    //
+    // Multiple conditions are evaluated using an AND operator.
+    pub conditions: Vec<Condition>,
+    // Defines actions that should be performed when the rule is triggered and
+    // conditions pass.
+    //
+    // Actions are executed serially.
+    pub actions: Vec<Action>,
+}
+
+impl Rule {
+    pub async fn evaluate(&self, conn: &Arc<SyncConnection>, event: &Event) -> Result<()> {
+        let any_trigger_matches = stream::iter(self.triggers.iter())
+            .any(|trigger| async move {
+                match trigger.matches_event(conn, event).await {
+                    Ok(matches) => matches,
+                    Err(err) => {
+                        log::error!("Got an error while evaluating a trigger: {:?}", err);
+
+                        false
+                    }
+                }
+            })
+            .await;
+
+        if any_trigger_matches == false {
+            return Ok(());
+        }
+
+        let all_conditions_pass = stream::iter(self.conditions.iter())
+            .all(|condition| async move {
+                match condition.evaluate(conn).await {
+                    Ok(passes) => passes,
+                    Err(err) => {
+                        log::error!("Got an error while evaluating a condition: {:?}", err);
+
+                        false
+                    }
+                }
+            })
+            .await;
+
+        if all_conditions_pass == false {
+            return Ok(());
+        }
+
+        for action in &self.actions {
+            action.execute(conn).await?;
+        }
+
+        Ok(())
+    }
+}
diff --git a/src/watcher.rs b/src/watcher.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c5f42467754ad85c21bee53aaca5a2efd42f37fe
--- /dev/null
+++ b/src/watcher.rs
@@ -0,0 +1,96 @@
+use std::{collections::HashMap, sync::Arc};
+
+use anyhow::{Error, Result};
+use dbus::{Path, nonblock::SyncConnection};
+use tokio::{sync::{broadcast, mpsc}, task::JoinHandle};
+
+use crate::{dbus_wrappers::manager::ManagerWrapper, device_watcher::watch_device, event::Event};
+
+struct WatchedDevice {
+    handle: tokio::task::JoinHandle<Result<(), Error>>,
+    stop_signal_tx: broadcast::Sender<()>,
+}
+
+pub async fn watch(conn: &Arc<SyncConnection>, mut stop_signal_rx: broadcast::Receiver<()>, event_tx: mpsc::Sender<Event>) -> Result<JoinHandle<Result<()>>> {
+    let conn2 = conn.clone();
+
+    log::info!("Starting NetworkManager watcher task");
+
+    let handler_handle: JoinHandle<Result<()>> = tokio::spawn(async move {
+        let manager = ManagerWrapper::from_connection(&conn2).await;
+
+        let mut watched_devices = HashMap::new();
+
+        log::debug!("Setting up signal handlers");
+
+        let mut device_added_signal = manager.device_added_signal_stream().await?;
+        let mut device_removed_signal = manager.device_removed_signal_stream().await?;
+
+        log::debug!("Looking for existing devices");
+
+        for device_path in manager.get_all_device_paths().await? {
+            let (stop_signal_tx, _stop_signal_rx) = broadcast::channel(1);
+            let device_watcher_handle = watch_device(&conn2, stop_signal_tx.subscribe(), event_tx.clone(), device_path.clone()).await?;
+
+            watched_devices.insert(device_path, WatchedDevice {
+                handle: device_watcher_handle,
+                stop_signal_tx
+            });
+        }
+
+        loop {
+            tokio::select! {
+                Some((msg, signal)) = device_added_signal.next() => {
+                    log::debug!("Got DeviceAdded signal for {}: {:?}", signal.device_path, &msg);
+
+                    event_tx.send(Event::DeviceAdded {
+                        device_path: Path::from(signal.device_path.to_string()),
+                    }).await?;
+
+                    let (stop_signal_tx, _stop_signal_rx) = broadcast::channel(1);
+                    let device_watcher_handle = watch_device(&conn2, stop_signal_tx.subscribe(), event_tx.clone(), signal.device_path.clone()).await?;
+
+                    watched_devices.insert(signal.device_path, WatchedDevice {
+                        handle: device_watcher_handle,
+                        stop_signal_tx
+                    });
+                },
+                Some((msg, signal)) = device_removed_signal.next() => {
+                    log::debug!("Got DeviceRemove signal for {}: {:?}", signal.device_path, &msg);
+
+                    event_tx.send(Event::DeviceRemoved {
+                        device_path: Path::from(signal.device_path.to_string()),
+                    }).await?;
+
+                    if watched_devices.contains_key(&signal.device_path) {
+                        watched_devices[&signal.device_path].stop_signal_tx.send(())?;
+
+                        watched_devices.remove(&signal.device_path);
+                    }
+                },
+                _ = stop_signal_rx.recv() => {
+                    log::info!("Stoping NetworkManager watcher task");
+
+                    break;
+                }
+            }
+        }
+
+        log::debug!("Removing signal handlers");
+
+        device_added_signal.dispose().await?;
+        device_removed_signal.dispose().await?;
+
+        log::debug!("Stopping any remaining child tasks");
+
+        for (_path, watched_device) in watched_devices.drain() {
+            watched_device.stop_signal_tx.send(())?;
+
+            watched_device.handle.await??;
+        }
+
+        Ok(())
+    });
+
+    Ok(handler_handle)
+}
diff --git a/third_party/NetworkManager b/third_party/NetworkManager
new file mode 160000
index 0000000000000000000000000000000000000000..0d4840c4841586dcae704c8474d6fc8c3c5b6fa1
--- /dev/null
+++ b/third_party/NetworkManager
@@ -0,0 +1 @@
+Subproject commit 0d4840c4841586dcae704c8474d6fc8c3c5b6fa1