From ac554b8129d2c43d87a5a0abca6bd097d870fe6d Mon Sep 17 00:00:00 2001 From: Hristo Venev Date: Mon, 18 Mar 2019 11:05:32 +0200 Subject: Initial commit. --- .gitignore | 2 + Cargo.lock | 325 ++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 16 +++ src/bin.rs | 56 ++++++++ src/config.rs | 67 ++++++++++ src/ip.rs | 403 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 203 +++++++++++++++++++++++++++++ src/proto.rs | 77 +++++++++++ src/wg.rs | 231 +++++++++++++++++++++++++++++++++ 9 files changed, 1380 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/bin.rs create mode 100644 src/config.rs create mode 100644 src/ip.rs create mode 100644 src/main.rs create mode 100644 src/proto.rs create mode 100644 src/wg.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f0e3bca --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..cec1ebc --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,325 @@ +[[package]] +name = "arrayref" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cc" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chrono" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "curl" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "curl-sys 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)", + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.42 (registry+https://github.com/rust-lang/crates.io-index)", + "schannel 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", + "socket2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "curl-sys" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", + "libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.9.42 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lazy_static" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.50" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libz-sys" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "openssl-probe" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "openssl-sys" +version = "0.9.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pkg-config" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro2" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_syscall" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ryu" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "schannel" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_derive" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_json" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "socket2" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "0.15.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "vcpkg" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "wgconfd" +version = "0.1.0" +dependencies = [ + "arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "curl 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum arrayref 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0d382e583f07208808f6b1249e60848879ba3543f57c32277bf52d69c2f0f0ee" +"checksum cc 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)" = "c9ce8bb087aacff865633f0bd5aeaed910fe2fe55b55f4739527f2e023a2e53d" +"checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4" +"checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878" +"checksum curl 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)" = "bed4741d1d4e1fc1ba6786c1313057c609259785cde2c45e34602acc45fd6ccc" +"checksum curl-sys 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7b8d8e51964f58c8053337fcef48e1c4608c7ee70c6f2e457674a97dda5a5828" +"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" +"checksum libc 0.2.50 (registry+https://github.com/rust-lang/crates.io-index)" = "aab692d7759f5cd8c859e169db98ae5b52c924add2af5fbbca11d12fefb567c1" +"checksum libz-sys 1.0.25 (registry+https://github.com/rust-lang/crates.io-index)" = "2eb5e43362e38e2bca2fd5f5134c4d4564a23a5c28e9b95411652021a8675ebe" +"checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea" +"checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" +"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" +"checksum openssl-sys 0.9.42 (registry+https://github.com/rust-lang/crates.io-index)" = "cb534d752bf98cf363b473950659ac2546517f9c6be9723771614ab3f03bbc9e" +"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" +"checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915" +"checksum quote 0.6.11 (registry+https://github.com/rust-lang/crates.io-index)" = "cdd8e04bd9c52e0342b406469d494fcb033be4bdbe5c606016defbb1681411e1" +"checksum redox_syscall 0.1.51 (registry+https://github.com/rust-lang/crates.io-index)" = "423e376fffca3dfa06c9e9790a9ccd282fafb3cc6e6397d01dbf64f9bacc6b85" +"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +"checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7" +"checksum schannel 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "f2f6abf258d99c3c1c5c2131d99d064e94b7b3dd5f416483057f308fea253339" +"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +"checksum serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "92514fb95f900c9b5126e32d020f5c6d40564c27a5ea6d1d7d9f157a96623560" +"checksum serde_derive 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6eabf4b5914e88e24eea240bb7c9f9a2cbc1bbbe8d961d381975ec3c6b806c" +"checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d" +"checksum socket2 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "c4d11a52082057d87cb5caa31ad812f4504b97ab44732cd8359df2e9ff9f48e7" +"checksum syn 0.15.29 (registry+https://github.com/rust-lang/crates.io-index)" = "1825685f977249735d510a242a6727b46efe914bb67e38d30c071b1b72b1d5c2" +"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" +"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +"checksum vcpkg 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "def296d3eb3b12371b2c7d0e83bfe1403e4db2d7a0bba324a12b21c4ee13143d" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..177c77f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "wgconfd" +version = "0.1.0" +authors = ["Hristo Venev "] +edition = "2018" + +[dependencies] +arrayref = "*" +serde = "*" +serde_derive = "*" +serde_json = "*" +chrono = "*" +curl = "*" + +[profile.release] +panic = "abort" diff --git a/src/bin.rs b/src/bin.rs new file mode 100644 index 0000000..2df3856 --- /dev/null +++ b/src/bin.rs @@ -0,0 +1,56 @@ +#[inline] +pub fn i64_to_be(v: i64) -> [u8; 8] { + u64_to_be(v as u64) +} + +pub fn i64_from_be(v: [u8; 8]) -> i64 { + u64_from_be(v) as i64 +} + +pub fn u64_to_be(v: u64) -> [u8; 8] { + [ + (v >> 56) as u8, + (v >> 48) as u8, + (v >> 40) as u8, + (v >> 32) as u8, + (v >> 24) as u8, + (v >> 16) as u8, + (v >> 8) as u8, + v as u8, + ] +} + +pub fn u64_from_be(v: [u8; 8]) -> u64 { + (u64::from(v[0]) << 56) | + (u64::from(v[1]) << 48) | + (u64::from(v[2]) << 40) | + (u64::from(v[3]) << 32) | + (u64::from(v[4]) << 24) | + (u64::from(v[5]) << 16) | + (u64::from(v[6]) << 8) | + u64::from(v[7]) +} + +pub fn u32_to_be(v: u32) -> [u8; 4] { + [ + (v >> 24) as u8, + (v >> 16) as u8, + (v >> 8) as u8, + v as u8, + ] +} + +pub fn u32_from_be(v: [u8; 4]) -> u32 { + (u32::from(v[0]) << 24) | + (u32::from(v[1]) << 16) | + (u32::from(v[2]) << 8) | + u32::from(v[3]) +} + +pub fn u16_to_be(v: u16) -> [u8; 2] { + [(v >> 8) as u8, v as u8] +} + +pub fn u16_from_be(v: [u8; 2]) -> u16 { + (u16::from(v[0]) << 8) | u16::from(v[1]) +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..6411b3a --- /dev/null +++ b/src/config.rs @@ -0,0 +1,67 @@ +use ::std::collections::HashSet; +use ::serde_derive; +use crate::ip::{Ipv4Set, Ipv6Set}; + +#[serde(deny_unknown_fields)] +#[derive(serde_derive::Serialize, serde_derive::Deserialize)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Source { + pub url: String, + pub psk: Option, + pub ipv4: Ipv4Set, + pub ipv6: Ipv6Set, +} + +#[serde(deny_unknown_fields)] +#[derive(serde_derive::Serialize, serde_derive::Deserialize)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct PeerConfig { + #[serde(default = "default_min_keepalive")] + pub min_keepalive: u32, + #[serde(default = "default_max_keepalive")] + pub max_keepalive: u32, + + pub omit_peers: HashSet, +} + +#[serde(deny_unknown_fields)] +#[derive(serde_derive::Serialize, serde_derive::Deserialize)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct UpdateConfig { + // Number of seconds between regular updates. + #[serde(default = "default_refresh")] + pub refresh_period: u32, +} + +#[serde(deny_unknown_fields)] +#[derive(serde_derive::Serialize, serde_derive::Deserialize)] +#[derive(Clone, Debug)] +pub struct Config { + pub ifname: String, + #[serde(default = "default_wg_command")] + pub wg_command: String, + + #[serde(flatten)] + pub peers: PeerConfig, + + #[serde(flatten)] + pub update: UpdateConfig, + + pub sources: Vec, +} + +fn default_wg_command() -> String { + "wg".to_owned() +} + +fn default_min_keepalive() -> u32 { + 10 +} + +fn default_max_keepalive() -> u32 { + 0 +} + +fn default_refresh() -> u32 { + 1200 +} diff --git a/src/ip.rs b/src/ip.rs new file mode 100644 index 0000000..1b21e4c --- /dev/null +++ b/src/ip.rs @@ -0,0 +1,403 @@ +use ::std::{error, fmt, iter, net}; +use ::std::net::{Ipv4Addr, Ipv6Addr}; +use ::std::iter::{IntoIterator, FromIterator}; +use ::std::str::{FromStr}; +use ::serde; +use crate::bin; + +#[derive(Debug)] +pub struct NetParseError {} + +impl error::Error for NetParseError {} +impl fmt::Display for NetParseError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Invalid IP network") + } +} + +macro_rules! per_proto { + ($nett:ident ($addrt:ident; $expecting:expr); $intt:ident($bytes:expr); $sett:ident) => { + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] + pub struct $nett { + pub address: $addrt, + pub prefix_len: u8, + } + + impl $nett { + const BITS: u8 = $bytes * 8; + + pub fn contains(&self, other: &$nett) -> bool { + if self.prefix_len > other.prefix_len { + return false; + } + if self.prefix_len == other.prefix_len { + return self.address == other.address; + } + // self.prefix_len < other.prefix_len = BITS + let shift = Self::BITS - self.prefix_len; + let v1: $intt = self.address.into(); + let v2: $intt = other.address.into(); + v1 >> shift == v2 >> shift + } + } + + impl fmt::Display for $nett { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}/{}", self.address, self.prefix_len) + } + } + + impl FromStr for $nett { + type Err = NetParseError; + fn from_str(s: &str) -> Result<$nett, NetParseError> { + let (addr, pfx) = pfx_split(s)?; + let addr = $addrt::from_str(addr).map_err(|_| NetParseError {})?; + + let r = $nett { + address: addr, + prefix_len: pfx, + }; + if !r.is_valid() { + return Err(NetParseError {}); + } + Ok(r) + } + } + + impl serde::Serialize for $nett { + fn serialize(&self, ser: S) -> Result { + if ser.is_human_readable() { + ser.serialize_str(&format!("{}", self)) + } else { + let mut buf = [0u8; $bytes + 1]; + *array_mut_ref![&mut buf, 0, $bytes] = self.address.octets(); + buf[$bytes] = self.prefix_len; + ser.serialize_bytes(&buf) + } + } + } + + impl<'de> serde::Deserialize<'de> for $nett { + fn deserialize>(de: D) -> Result { + if de.is_human_readable() { + struct NetVisitor; + impl<'de> serde::de::Visitor<'de> for NetVisitor { + type Value = $nett; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str($expecting) + } + + fn visit_str(self, s: &str) -> Result { + s.parse().map_err(E::custom) + } + } + de.deserialize_str(NetVisitor) + } else { + let buf = <[u8; $bytes+1] as serde::Deserialize>::deserialize(de)?; + let r = $nett { + address: (*array_ref![&buf, 0, $bytes]).into(), + prefix_len: buf[$bytes], + }; + if r.is_valid() { + return Err(serde::de::Error::custom(NetParseError {})); + } + Ok(r) + } + } + } + + #[derive(Clone, PartialEq, Eq, PartialOrd, Hash, Debug)] + pub struct $sett { + nets: Vec<$nett>, + } + + impl Default for $sett { + #[inline] + fn default() -> Self { + $sett::new() + } + } + + impl $sett { + #[inline] + pub fn new() -> Self { + $sett { nets: vec![] } + } + + #[inline] + fn siblings(a: &$nett, b: &$nett) -> bool { + let pfx = a.prefix_len; + if b.prefix_len != pfx || pfx == 0 { + return false; + } + let a: $intt = a.address.into(); + let b: $intt = b.address.into(); + a ^ b == 1 << ($nett::BITS - pfx) + } + + pub fn insert(&mut self, mut net: $nett) { + let mut i = match self.nets.binary_search(&net) { + Err(i) => i, + Ok(_) => { + return; + } + }; + let mut j = i; + if i != 0 && self.nets[i-1].contains(&net) { + net = self.nets[i-1]; + i -= 1; + } + while j < self.nets.len() && net.contains(&self.nets[j]) { + j += 1; + } + loop { + if j < self.nets.len() && Self::siblings(&net, &self.nets[j]) { + j += 1; + } else if i != 0 && Self::siblings(&self.nets[i-1], &net) { + net = self.nets[i-1]; + i -= 1; + } else { + break; + } + net.prefix_len -= 1; + } + self.nets.splice(i..j, iter::once(net)); + } + + pub fn contains(&self, net: &$nett) -> bool { + match self.nets.binary_search(&net) { + Err(i) => { + if i == 0 { + return false; + } + self.nets[i-1].contains(&net) + } + Ok(_) => true, + } + } + + #[inline] + pub fn iter(&self) -> std::slice::Iter<$nett> { + self.nets.iter() + } + } + + impl IntoIterator for $sett { + type Item = $nett; + type IntoIter = std::vec::IntoIter<$nett>; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.nets.into_iter() + } + } + + impl FromIterator<$nett> for $sett { + fn from_iter>(it: I) -> $sett { + let mut r = $sett::new(); + for net in it { + r.insert(net); + } + r + } + } + + impl<'a> From<$nett> for $sett { + #[inline] + fn from(v: $nett) -> $sett { + $sett { nets: vec![v] } + } + } + + impl<'a> From<[$nett; 1]> for $sett { + #[inline] + fn from(v: [$nett; 1]) -> $sett { + $sett { nets: vec![v[0]] } + } + } + + impl From<$sett> for Vec<$nett> { + fn from(v: $sett) -> Vec<$nett> { + v.nets + } + } + + impl From> for $sett { + fn from(nets: Vec<$nett>) -> $sett { + let mut s = $sett { nets }; + let len = s.nets.len(); + if len == 0 { + return s; + } + s.nets.sort(); + let mut i = 1; + for j in 1..len { + let mut net = s.nets[j]; + if i != 0 && s.nets[i-1].contains(&net) { + net = s.nets[i-1]; + i -= 1; + } + while i != 0 && Self::siblings(&s.nets[i-1], &net) { + net = s.nets[i-1]; + net.prefix_len -= 1; + i -= 1; + } + s.nets[i] = net; + i += 1; + } + s.nets.splice(i.., iter::empty()); + s + } + } + + impl<'a> From<&'a [$nett]> for $sett { + #[inline] + fn from(nets: &'a [$nett]) -> $sett { + Vec::from(nets).into() + } + } + + impl<'a> From<&'a mut [$nett]> for $sett { + #[inline] + fn from(nets: &'a mut [$nett]) -> $sett { + Vec::from(nets).into() + } + } + + impl serde::Serialize for $sett { + fn serialize(&self, ser: S) -> Result { + as serde::Serialize>::serialize(&self.nets, ser) + } + } + + impl<'de> serde::Deserialize<'de> for $sett { + fn deserialize>(de: D) -> Result { + as serde::Deserialize>::deserialize(de).map($sett::from) + } + } + }; +} + +per_proto!(Ipv4Net(Ipv4Addr; "IPv4 network"); u32(4); Ipv4Set); +per_proto!(Ipv6Net(Ipv6Addr; "IPv6 network"); u128(16); Ipv6Set); + +impl Ipv4Net { + pub fn is_valid(&self) -> bool { + let pfx = self.prefix_len; + if pfx > 32 { + return false; + } + if pfx == 32 { + return true; + } + let val: u32 = self.address.into(); + val & (u32::max_value() >> pfx) == 0 + } +} + +impl Ipv6Net { + pub fn is_valid(&self) -> bool { + let pfx = self.prefix_len; + if pfx > 128 { + return false; + } + if pfx == 128 { + return true; + } + + let val: u128 = self.address.into(); + let val: [u64; 2] = [(val >> 64) as u64, val as u64]; + if pfx >= 64 { + return val[1] & (u64::max_value() >> (pfx - 64)) == 0; + } + if val[1] != 0 { + return false; + } + val[0] & (u64::max_value() >> pfx) == 0 + } +} + +fn pfx_split(s: &str) -> Result<(&str, u8), NetParseError> { + let i = match s.find("/") { + Some(i) => i, + None => { + return Err(NetParseError {}); + } + }; + let (addr, pfx) = s.split_at(i); + let pfx = u8::from_str(&pfx[1..]).map_err(|_| NetParseError {})?; + Ok((addr, pfx)) +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +pub struct Endpoint { + pub address: Ipv6Addr, + pub port: u16, +} + +impl fmt::Display for Endpoint { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if self.address.segments()[5] == 0xffff { + write!(f, "{}:", self.address.to_ipv4().unwrap())?; + } else { + write!(f, "[{}]:", self.address)?; + } + write!(f, "{}", self.port) + } +} + +impl FromStr for Endpoint { + type Err = net::AddrParseError; + fn from_str(s: &str) -> Result { + net::SocketAddr::from_str(s) + .map(|v| Endpoint { + address: match v.ip() { + net::IpAddr::V4(a) => a.to_ipv6_mapped(), + net::IpAddr::V6(a) => a, + }, + port: v.port(), + }) + } +} + +impl serde::Serialize for Endpoint { + fn serialize(&self, ser: S) -> Result { + if ser.is_human_readable() { + ser.serialize_str(&format!("{}", self)) + } else { + let mut buf = [0u8; 16 + 2]; + let (buf_addr, buf_port) = mut_array_refs![&mut buf, 16, 2]; + *buf_addr = self.address.octets(); + *buf_port = crate::bin::u16_to_be(self.port); + ser.serialize_bytes(&buf) + } + } +} + +impl<'de> serde::Deserialize<'de> for Endpoint { + fn deserialize>(de: D) -> Result { + if de.is_human_readable() { + struct EndpointVisitor; + impl<'de> serde::de::Visitor<'de> for EndpointVisitor { + type Value = Endpoint; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("ip:port") + } + + fn visit_str(self, s: &str) -> Result { + s.parse().map_err(E::custom) + } + } + de.deserialize_str(EndpointVisitor) + } else { + let buf = <[u8; 16 + 2] as serde::Deserialize>::deserialize(de)?; + let (buf_addr, buf_port) = array_refs![&buf, 16, 2]; + Ok(Endpoint { + address: (*buf_addr).into(), + port: bin::u16_from_be(*buf_port), + }) + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..ad57895 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,203 @@ +#[macro_use] +extern crate arrayref; + +use ::std::io; +use ::std::time::{Duration, SystemTime, Instant}; + +mod bin; +mod ip; +mod proto; +mod config; +mod wg; + +struct Source { + config: config::Source, + data: Option, + next_update: Instant, + backoff: Option, +} + +impl Source { + fn new(config: config::Source) -> Source { + Source { + config, + data: None, + next_update: Instant::now(), + backoff: None, + } + } +} + +pub struct Device { + dev: wg::Device, + peer_config: config::PeerConfig, + update_config: config::UpdateConfig, + sources: Vec, + current: wg::Config, +} + +impl Device { + pub fn new(c: config::Config) -> Device { + Device { + dev: wg::Device::new(c.ifname, c.wg_command), + peer_config: c.peers, + update_config: c.update, + sources: c.sources.into_iter().map(Source::new).collect(), + current: wg::Config::new(), + } + } + + fn make_config(&self, ts: SystemTime) -> (wg::Config, Vec, SystemTime) { + let mut cfg = wg::Config::new(); + let mut next_update = ts + Duration::from_secs(3600); + let mut errs = vec![]; + for src in self.sources.iter() { + if let Some(data) = &src.data { + let sc = data.next.as_ref().and_then(|next| { + if ts >= next.update_at { + Some(&next.config) + } else { + next_update = next_update.min(next.update_at); + None + } + }).unwrap_or(&data.config); + for peer in sc.peers.iter() { + cfg.add_peer(&mut errs, &self.peer_config, &src.config, peer); + } + } + } + (cfg, errs, next_update) + } + + pub fn update(&mut self) -> io::Result { + let now = Instant::now(); + let refresh = self.update_config.refresh_period; + let after_refresh = now + Duration::from_secs(u64::from(refresh)); + let mut next_update = after_refresh; + for src in self.sources.iter_mut() { + if now < src.next_update { + next_update = next_update.min(src.next_update); + continue; + } + + let r = fetch_source(&src.config.url); + let r = match r { + Ok(r) => { + eprintln!("<6>Updated [{}]", &src.config.url); + src.data = Some(r); + src.backoff = None; + src.next_update = after_refresh; + continue; + } + Err(r) => r, + }; + + eprintln!("<3>Failed to update [{}]: {}", &src.config.url, &r); + + let b = src.backoff.unwrap_or(if src.data.is_some() { + refresh / 3 + } else { + u32::min(10, refresh / 10) + }); + let b = (b + b / 3).min(refresh); + src.backoff = Some(b); + src.next_update = now + Duration::from_secs(u64::from(b)); + next_update = next_update.min(src.next_update); + } + + let sysnow = SystemTime::now(); + let (config, errors, upd_time) = self.make_config(sysnow); + let time_to_upd = upd_time.duration_since(sysnow).unwrap_or(Duration::from_secs(0)); + next_update = next_update.min(now + time_to_upd); + + if config != self.current { + eprintln!("<5>Applying configuration update"); + for err in errors.iter() { + eprintln!("<{}>{}", if err.important { '4' } else { '5' }, err); + } + self.dev.apply_diff(&self.current, &config)?; + self.current = config; + } + eprintln!("<6>Next configuration update after {:?}", time_to_upd); + + Ok(next_update) + } +} + +fn fetch_source(url: &str) -> io::Result { + use ::curl::easy::Easy; + + let mut res = Vec::::new(); + + { + let mut req = Easy::new(); + req.url(url)?; + + { + let mut tr = req.transfer(); + tr.write_function(|data| { + res.extend_from_slice(data); + Ok(data.len()) + })?; + tr.perform()?; + } + + let code = req.response_code()?; + if code != 0 && code != 200 { + return Err(io::Error::new(io::ErrorKind::Other, format!("HTTP error {}", code))); + } + } + + let mut de = serde_json::Deserializer::from_slice(&res); + let r = serde::Deserialize::deserialize(&mut de)?; + Ok(r) +} + +fn load_config(path: &str) -> io::Result { + use std::fs; + use ::serde_json; + + let config_file = fs::File::open(path)?; + let rd = io::BufReader::new(config_file); + let mut de = serde_json::Deserializer::from_reader(rd); + Ok(serde::Deserialize::deserialize(&mut de)?) +} + +fn main() { + use ::std::{env, thread, process}; + + let args: Vec = env::args().into_iter().collect(); + if args.len() != 2 { + let arg0 = if args.len() >= 1 { + &args[0] + } else { + "wgconf" + }; + eprintln!("<1>Usage:"); + eprintln!("<1> {} CONFIG", arg0); + process::exit(1); + } + + let config = match load_config(&args[1]) { + Ok(c) => c, + Err(e) => { + eprintln!("<1>Failed to load config: {}", e); + process::exit(1); + } + }; + + let mut dev = Device::new(config); + loop { + let tm = match dev.update() { + Ok(t) => t, + Err(e) => { + eprintln!("<1>{}", e); + process::exit(1); + } + }; + let now = Instant::now(); + let sleep = tm.duration_since(now); + println!("Sleeping for {:?}", sleep); + thread::sleep(sleep); + } +} diff --git a/src/proto.rs b/src/proto.rs new file mode 100644 index 0000000..e6759a1 --- /dev/null +++ b/src/proto.rs @@ -0,0 +1,77 @@ +use ::std::time::SystemTime; +use ::serde_derive; + +use crate::ip::{Ipv4Net, Ipv6Net, Endpoint}; + +#[serde(deny_unknown_fields)] +#[derive(serde_derive::Serialize, serde_derive::Deserialize)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Peer { + pub public_key: String, + pub endpoint: Endpoint, + #[serde(default = "default_peer_keepalive")] + pub keepalive: u32, + pub ipv4: Vec, + pub ipv6: Vec, +} + +fn default_peer_keepalive() -> u32 { + 0 +} + +#[derive(serde_derive::Serialize, serde_derive::Deserialize)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct SourceConfig { + pub peers: Vec, +} + +#[derive(serde_derive::Serialize, serde_derive::Deserialize)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct SourceNextConfig { + #[serde(with = "serde_utc")] + pub update_at: SystemTime, + #[serde(flatten)] + pub config: SourceConfig, +} + +#[derive(serde_derive::Serialize, serde_derive::Deserialize)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Source { + #[serde(flatten)] + pub config: SourceConfig, + pub next: Option, +} + +mod serde_utc { + use ::std::time::SystemTime; + use ::chrono::{DateTime, TimeZone, Utc, SecondsFormat}; + use ::serde::*; + use crate::bin; + + pub fn serialize(t: &SystemTime, ser: S) -> Result { + let t = DateTime::::from(*t); + if ser.is_human_readable() { + ser.serialize_str(&t.to_rfc3339_opts(SecondsFormat::Nanos, true)) + } else { + let mut buf = [0u8; 12]; + let (buf_secs, buf_nanos) = mut_array_refs![&mut buf, 8, 4]; + *buf_secs = bin::i64_to_be(t.timestamp()); + *buf_nanos = bin::u32_to_be(t.timestamp_subsec_nanos()); + ser.serialize_bytes(&buf) + } + } + + pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result { + if de.is_human_readable() { + let s: String = String::deserialize(de)?; + let t = DateTime::parse_from_rfc3339(&s).map_err(de::Error::custom)?; + Ok(t.into()) + } else { + let mut buf = <[u8; 12]>::deserialize(de)?; + let (buf_secs, buf_nanos) = array_refs![&mut buf, 8, 4]; + let secs = bin::i64_from_be(*buf_secs); + let nanos = bin::u32_from_be(*buf_nanos); + Ok(Utc.timestamp(secs, nanos).into()) + } + } +} diff --git a/src/wg.rs b/src/wg.rs new file mode 100644 index 0000000..d5a03ff --- /dev/null +++ b/src/wg.rs @@ -0,0 +1,231 @@ +use ::std::{error, io, fmt}; +use ::std::collections::hash_map; +use hash_map::HashMap; +use crate::ip::{Ipv4Net, Ipv6Net, Endpoint}; +use crate::{proto, config}; + +#[derive(Clone, PartialEq, Eq, Debug)] +struct Peer { + endpoint: Endpoint, + psk: Option, + keepalive: u32, + ipv4: Vec, + ipv6: Vec, +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Config { + peers: HashMap, +} + +#[derive(Debug)] +pub struct ConfigError { + pub url: String, + pub peer: String, + pub important: bool, + err: &'static str, +} + +impl error::Error for ConfigError {} +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Invalid peer [{}] from [{}]: {}", self.peer, self.url, self.err) + } +} + +impl Config { + pub fn new() -> Config { + Config { + peers: HashMap::new(), + } + } + + pub fn add_peer(&mut self, errors: &mut Vec, c: &config::PeerConfig, s: &config::Source, p: &proto::Peer) { + if !valid_key(&p.public_key) { + errors.push(ConfigError { + url: s.url.clone(), + peer: p.public_key.clone(), + important: true, + err: "Invalid public key", + }); + return; + } + + if let Some(ref psk) = s.psk { + if !valid_key(psk) { + errors.push(ConfigError { + url: s.url.clone(), + peer: p.public_key.clone(), + important: true, + err: "Invalid preshared key", + }); + return; + } + } + + if c.omit_peers.contains(&p.public_key) { + return; + } + + let ent = match self.peers.entry(p.public_key.clone()) { + hash_map::Entry::Occupied(_) => { + errors.push(ConfigError { + url: s.url.clone(), + peer: p.public_key.clone(), + important: true, + err: "Duplicate public key", + }); + return; + }, + hash_map::Entry::Vacant(ent) => ent, + }; + + let mut keepalive = p.keepalive; + if c.max_keepalive != 0 && (keepalive == 0 || keepalive > c.max_keepalive) { + keepalive = c.max_keepalive; + } + if keepalive != 0 && keepalive < c.min_keepalive { + keepalive = c.min_keepalive; + } + + let mut removed = false; + + let mut ipv4 = p.ipv4.clone(); + ipv4.retain(|i| { + let r = s.ipv4.contains(i); + if !r { removed = true; } + r + }); + + let mut ipv6 = p.ipv6.clone(); + ipv6.retain(|i| { + let r = s.ipv6.contains(i); + if !r { removed = true; } + r + }); + + let r = ent.insert(Peer { + endpoint: p.endpoint.clone(), + psk: s.psk.clone(), + keepalive, ipv4, ipv6, + }); + + if removed { + let all = r.ipv4.is_empty() && r.ipv6.is_empty(); + errors.push(ConfigError { + url: s.url.clone(), + peer: p.public_key.clone(), + important: all, + err: if all { "All IPs removed" } else {"Some IPs removed"}, + }); + } + } +} + +impl Default for Config { + #[inline] + fn default() -> Self { + Config::new() + } +} + +pub struct Device { + ifname: String, + wg_command: String, +} + +impl Device { + pub fn new(ifname: String, wg_command: String) -> Self { + Device { ifname, wg_command } + } + + pub fn apply_diff(&mut self, old: &Config, new: &Config) -> io::Result<()> { + use ::std::process::{Command, Stdio}; + + let mut proc = Command::new(&self.wg_command); + proc.stdin(Stdio::piped()); + proc.stdout(Stdio::null()); + proc.arg("set"); + proc.arg(&self.ifname); + + let mut psks = Vec::<&str>::new(); + + for (pubkey, conf) in new.peers.iter() { + if let Some(old_peer) = old.peers.get(pubkey) { + if *old_peer == *conf { + continue; + } + } + proc.arg("peer"); + proc.arg(pubkey); + + // TODO: maybe skip endpoint? + proc.arg("endpoint"); + proc.arg(format!("{}", conf.endpoint)); + + if let Some(psk) = &conf.psk { + proc.arg("preshared-key"); + proc.arg("/dev/stdin"); + psks.push(psk); + } + + let mut ips = String::new(); + { + use std::fmt::Write; + for ip in conf.ipv4.iter() { + if !ips.is_empty() { ips.push(','); } + write!(ips, "{}", ip).unwrap(); + } + for ip in conf.ipv6.iter() { + if !ips.is_empty() { ips.push(','); } + write!(ips, "{}", ip).unwrap(); + } + } + + proc.arg("allowed-ips"); + proc.arg(ips); + } + + for pubkey in old.peers.keys() { + if new.peers.contains_key(pubkey) { + continue; + } + proc.arg("peer"); + proc.arg(pubkey); + proc.arg("remove"); + } + + let mut proc = proc.spawn()?; + { + use std::io::Write; + let stdin = proc.stdin.as_mut().unwrap(); + for psk in psks { + write!(stdin, "{}\n", psk)?; + } + } + + let r = proc.wait()?; + if !r.success() { + return Err(io::Error::new(io::ErrorKind::Other, "Child process failed")); + } + Ok(()) + } +} + +fn valid_key(s: &str) -> bool { + let s = s.as_bytes(); + if s.len() != 44 { + return false; + } + if s[43] != b'=' { + return false; + } + for c in s[0..42].iter().cloned() { + if c >= b'0' && c <= b'9' { continue; } + if c >= b'A' && c <= b'Z' { continue; } + if c >= b'a' && c <= b'z' { continue; } + if c == b'+' || c <= b'/' { continue; } + return false; + } + b"048AEIMQUYcgkosw".contains(&s[42]) +} -- cgit