├── .gitignore ├── Cargo.toml ├── LICENSE ├── letsencrypt └── letsencryptauthorityx3.pem.txt ├── README.md └── src ├── syslog.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rslogd" 3 | version = "0.6.0" 4 | authors = ["Tom Pusateri "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | bytes = "0.4.12" 9 | socket2 = { version = "0.3.9", features = ["reuseport"] } 10 | mio = "^0.6" 11 | chrono = "0.4.6" 12 | rustls = "0.15.2" 13 | docopt = "~1.0" 14 | serde_derive = "1.0" 15 | serde = "1.0" 16 | index-pool = "1.0" 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tom Pusateri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /letsencrypt/letsencryptauthorityx3.pem.txt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFjTCCA3WgAwIBAgIRANOxciY0IzLc9AUoUSrsnGowDQYJKoZIhvcNAQELBQAw 3 | TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh 4 | cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTYxMDA2MTU0MzU1 5 | WhcNMjExMDA2MTU0MzU1WjBKMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg 6 | RW5jcnlwdDEjMCEGA1UEAxMaTGV0J3MgRW5jcnlwdCBBdXRob3JpdHkgWDMwggEi 7 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCc0wzwWuUuR7dyXTeDs2hjMOrX 8 | NSYZJeG9vjXxcJIvt7hLQQWrqZ41CFjssSrEaIcLo+N15Obzp2JxunmBYB/XkZqf 9 | 89B4Z3HIaQ6Vkc/+5pnpYDxIzH7KTXcSJJ1HG1rrueweNwAcnKx7pwXqzkrrvUHl 10 | Npi5y/1tPJZo3yMqQpAMhnRnyH+lmrhSYRQTP2XpgofL2/oOVvaGifOFP5eGr7Dc 11 | Gu9rDZUWfcQroGWymQQ2dYBrrErzG5BJeC+ilk8qICUpBMZ0wNAxzY8xOJUWuqgz 12 | uEPxsR/DMH+ieTETPS02+OP88jNquTkxxa/EjQ0dZBYzqvqEKbbUC8DYfcOTAgMB 13 | AAGjggFnMIIBYzAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADBU 14 | BgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEEAYLfEwEBATAwMC4GCCsGAQUFBwIB 15 | FiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2VuY3J5cHQub3JnMB0GA1UdDgQWBBSo 16 | SmpjBH3duubRObemRWXv86jsoTAzBgNVHR8ELDAqMCigJqAkhiJodHRwOi8vY3Js 17 | LnJvb3QteDEubGV0c2VuY3J5cHQub3JnMHIGCCsGAQUFBwEBBGYwZDAwBggrBgEF 18 | BQcwAYYkaHR0cDovL29jc3Aucm9vdC14MS5sZXRzZW5jcnlwdC5vcmcvMDAGCCsG 19 | AQUFBzAChiRodHRwOi8vY2VydC5yb290LXgxLmxldHNlbmNyeXB0Lm9yZy8wHwYD 20 | VR0jBBgwFoAUebRZ5nu25eQBc4AIiMgaWPbpm24wDQYJKoZIhvcNAQELBQADggIB 21 | ABnPdSA0LTqmRf/Q1eaM2jLonG4bQdEnqOJQ8nCqxOeTRrToEKtwT++36gTSlBGx 22 | A/5dut82jJQ2jxN8RI8L9QFXrWi4xXnA2EqA10yjHiR6H9cj6MFiOnb5In1eWsRM 23 | UM2v3e9tNsCAgBukPHAg1lQh07rvFKm/Bz9BCjaxorALINUfZ9DD64j2igLIxle2 24 | DPxW8dI/F2loHMjXZjqG8RkqZUdoxtID5+90FgsGIfkMpqgRS05f4zPbCEHqCXl1 25 | eO5HyELTgcVlLXXQDgAWnRzut1hFJeczY1tjQQno6f6s+nMydLN26WuU4s3UYvOu 26 | OsUxRlJu7TSRHqDC3lSE5XggVkzdaPkuKGQbGpny+01/47hfXXNB7HntWNZ6N2Vw 27 | p7G6OfY+YQrZwIaQmhrIqJZuigsrbe3W+gdn5ykE9+Ky0VgVUsfxo52mwFYs1JKY 28 | 2PGDuWx8M6DlS6qQkvHaRUo0FMd8TsSlbF0/v965qGFKhSDeQoMpYnwcmQilRh/0 29 | ayLThlHLN81gSkJjVrPI0Y8xCVPB4twb1PFUd2fPM3sA1tJ83sZ5v8vgFv2yofKR 30 | PB0t6JzUA81mSqM3kxl5e+IZwhYAyO0OTg3/fs8HqGTNKd9BqoUwSRBzp06JMg5b 31 | rUCGwbCUDI0mxadJ3Bz4WxR6fyNpBK2yAinWEsikxqEt 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rslogd 2 | syslog server written in Rust as an introduction to [mio](https://github.com/tokio-rs/mio) for [Triangle Rustaceans](https://www.meetup.com/triangle-rustaceans/events/mfglwpyzlbjc/) August 2019 Meetup. 3 | 4 | Each stage provides a few more features to discuss. To run the syslog server, you'll need to be root or sudo in order to open port 514 and port 601. Use the following commands to build and run: 5 | 6 | ``` 7 | cargo build 8 | sudo target/debug/rslogd --certs ./my.server.com/cert.pem --key ./my.server.com/privkey.pem 9 | ``` 10 | 11 | Test Commands 12 | ============= 13 | For testing over udp, the following clients will work: 14 | 15 | ``` 16 | # Linux 17 | logger -s -i -n 127.0.0.1 testing 18 | 19 | # macOS 20 | syslog -s -r 127.0.0.1 testing 21 | ``` 22 | 23 | For testing over TCP, use the following command: 24 | 25 | ``` 26 | # Linux 27 | logger -s -T -P 601 -i -n 127.0.0.1 test TCP message 28 | ``` 29 | 30 | For testing with TLS, use the gnutls-cli command to encapsualte the syslog message: 31 | 32 | ``` 33 | # Linux, FreeBSD, or macOS 34 | gnutls-cli my.server.com --port=6514 --x509cafile=./letsencrypt/letsencryptauthorityx3.pem.txt 35 | ``` 36 | 37 | Then paste in the preformatted syslog line terminating with Ctl-D: 38 | 39 | ``` 40 | <7>May 29 09:20:57 client.example.com syslog[32674]: testing 41 | ^D 42 | ``` 43 | 44 | Stage 1 45 | ======= 46 | Stage 1 is the initial UDP only version over IPv4 ([RFC 5426](https://tools.ietf.org/html/rfc5426)). It prints a line for each received syslog packet to port 514 but does not decode it. To see Stage 1, use: 47 | 48 | ``` 49 | git checkout stage1 50 | ``` 51 | 52 | Stage 2 53 | ======= 54 | Stage 2 adds UDP over IPv6 and adds syslog packet decoding. It supports 3 types of syslog packets: 55 | 56 | 1. Original BSD syslog ([RFC 3164](https://tools.ietf.org/html/rfc3164)) 57 | 2. syslog Version 1 ([RFC 5424](https://tools.ietf.org/html/rfc5424)) 58 | 3. Apple System Logger (asl) 59 | 60 | To see Stage 2, use: 61 | 62 | ``` 63 | git checkout stage2 64 | ``` 65 | 66 | Stage 3 67 | ======= 68 | Stage 3 adds TCP over IPv4 and IPv6 ([RFC 6587](https://tools.ietf.org/html/rfc6587)). Syslog over TCP shouldn't be used anymore (deprecated in favor of TLS). But adding it as a stage gives us understanding about using mio with TCP. 69 | 70 | To see Stage 3, use: 71 | 72 | ``` 73 | git checkout stage3 74 | ``` 75 | 76 | Stage 4 77 | ======= 78 | Stage 4 adds TLS over IPv4 support to syslog as described in [RFC 5425](https://tools.ietf.org/html/rfc5425). Adds command line options to provide certificates and private key. 79 | 80 | To see Stage 4, use: 81 | 82 | ``` 83 | git checkout stage4 84 | ``` 85 | 86 | Stage 5 87 | ======= 88 | Stage 5 adds TLS over IPv6 support and an index pool for tokens. 89 | 90 | To see Stage 5, use: 91 | 92 | ``` 93 | git checkout stage5 94 | ``` 95 | -------------------------------------------------------------------------------- /src/syslog.rs: -------------------------------------------------------------------------------- 1 | use chrono::prelude::*; 2 | use std::collections::HashMap; 3 | use std::net::SocketAddr; 4 | use std::str; 5 | 6 | // ASL facility codes 7 | const LOG_KERN: u8 = 0; 8 | const LOG_USER: u8 = 1; 9 | const LOG_MAIL: u8 = 2; 10 | const LOG_DAEMON: u8 = 3; 11 | const LOG_AUTH: u8 = 4; 12 | const LOG_SYSLOG: u8 = 5; 13 | const LOG_LPR: u8 = 6; 14 | const LOG_NEWS: u8 = 7; 15 | const LOG_UUCP: u8 = 8; 16 | const LOG_CRON: u8 = 9; 17 | const LOG_AUTHPRIV: u8 = 10; 18 | const LOG_FTP: u8 = 11; 19 | const LOG_NETINFO: u8 = 12; 20 | const LOG_REMOTEAUTH: u8 = 13; 21 | const LOG_INSTALL: u8 = 14; 22 | // unused const LOG_RAS: u8 = 15; 23 | const LOG_LOCAL0: u8 = 16; 24 | const LOG_LOCAL1: u8 = 17; 25 | const LOG_LOCAL2: u8 = 18; 26 | const LOG_LOCAL3: u8 = 19; 27 | const LOG_LOCAL4: u8 = 20; 28 | const LOG_LOCAL5: u8 = 21; 29 | const LOG_LOCAL6: u8 = 22; 30 | const LOG_LOCAL7: u8 = 23; 31 | const LOG_LAUNCHD: u8 = 24; 32 | 33 | trait SliceExt { 34 | fn slice_until_space(&self) -> &Self; 35 | fn slice_between_arrows(&self) -> &Self; 36 | fn slice_between_brackets(&self) -> &Self; 37 | } 38 | 39 | impl SliceExt for [u8] { 40 | fn slice_until_space(&self) -> &[u8] { 41 | fn is_whitespace(c: &u8) -> bool { 42 | *c == b' ' 43 | } 44 | 45 | fn is_not_whitespace(c: &u8) -> bool { 46 | !is_whitespace(c) 47 | } 48 | 49 | if let Some(first) = self.iter().position(is_not_whitespace) { 50 | if let Some(space) = self.iter().position(is_whitespace) { 51 | &self[first..space] 52 | } else { 53 | &self[first..] 54 | } 55 | } else { 56 | &[] 57 | } 58 | } 59 | 60 | fn slice_between_arrows(&self) -> &[u8] { 61 | fn is_left_arrow(c: &u8) -> bool { 62 | *c == b'<' 63 | } 64 | 65 | fn is_right_arrow(c: &u8) -> bool { 66 | *c == b'>' 67 | } 68 | 69 | if let Some(left) = self.iter().position(is_left_arrow) { 70 | if let Some(right) = self.iter().position(is_right_arrow) { 71 | &self[left..right + 1] 72 | } else { 73 | &[] 74 | } 75 | } else { 76 | &[] 77 | } 78 | } 79 | 80 | fn slice_between_brackets(&self) -> &[u8] { 81 | fn is_left_bracket(c: &u8) -> bool { 82 | *c == b'[' 83 | } 84 | 85 | fn is_right_bracket(c: &u8) -> bool { 86 | *c == b']' 87 | } 88 | 89 | if let Some(left) = self.iter().position(is_left_bracket) { 90 | if let Some(right) = self.iter().position(is_right_bracket) { 91 | &self[left..right + 1] 92 | } else { 93 | &[] 94 | } 95 | } else { 96 | &[] 97 | } 98 | } 99 | } 100 | 101 | #[derive(Debug)] 102 | pub struct SyslogMsg { 103 | from: SocketAddr, 104 | facility: u8, 105 | severity: u8, 106 | version: u8, 107 | timestamp: Option>, 108 | hostname: Option, 109 | appname: Option, 110 | procid: Option, 111 | msgid: Option, 112 | sdata: Option>, 113 | msg: Option, 114 | } 115 | 116 | impl SyslogMsg { 117 | fn from_version_1( 118 | from: SocketAddr, 119 | len: usize, 120 | buf: &[u8], 121 | mut first: usize, 122 | vstr: &str, 123 | facility: u8, 124 | severity: u8, 125 | ) -> Option { 126 | first += vstr.len(); 127 | let version = vstr.parse::().expect("version parse"); 128 | 129 | while buf[first] == b' ' { 130 | first += 1; 131 | } 132 | 133 | let mut timestamp: Option> = None; 134 | if let Some(tstr) = syslog_parse_opt_string(buf, &mut first, &len) { 135 | timestamp = match DateTime::parse_from_rfc3339(&tstr) { 136 | Ok(ts) => Some(ts.with_timezone(&Utc)), 137 | Err(_why) => { 138 | if tstr == "_" { 139 | None 140 | } else { 141 | return None; 142 | } 143 | } 144 | }; 145 | }; 146 | 147 | let hostname = syslog_parse_opt_string(buf, &mut first, &len); 148 | let appname = syslog_parse_opt_string(buf, &mut first, &len); 149 | let procid = syslog_parse_opt_string(buf, &mut first, &len); 150 | let msgid = syslog_parse_opt_string(buf, &mut first, &len); 151 | 152 | // structured data may be missing ("-") or will be enclosed in "[", "]" 153 | let sd: Option> = if buf[first] == b'-' { 154 | first += 2; 155 | None 156 | } else { 157 | let kv_str = buf[first..len].slice_between_brackets(); 158 | let kv_len = kv_str.len(); 159 | if kv_len != 0 { 160 | first += kv_len; 161 | eprintln!("todo: parse sd"); 162 | } 163 | None 164 | }; 165 | 166 | // the remainder of buf 167 | let msg = match String::from_utf8(buf[first..len].to_vec()) { 168 | Ok(m) => Some(m), 169 | Err(_why) => None, 170 | }; 171 | Some(SyslogMsg { 172 | from: from, 173 | facility: facility, 174 | severity: severity, 175 | version: version, 176 | timestamp: timestamp, 177 | hostname: hostname, 178 | appname: appname, 179 | procid: procid, 180 | msgid: msgid, 181 | sdata: sd, 182 | msg: msg, 183 | }) 184 | } 185 | 186 | fn from_bsd( 187 | from: SocketAddr, 188 | len: usize, 189 | buf: &[u8], 190 | mut first: usize, 191 | facility: u8, 192 | severity: u8, 193 | ) -> Option { 194 | let local: DateTime = Local::now(); 195 | let ts = format!( 196 | "{} {}", 197 | local.format("%z %Y"), 198 | str::from_utf8(&buf[first..first + 15]).unwrap() 199 | ); 200 | first += 15; 201 | let timestamp = match DateTime::parse_from_str(&ts, "%z %Y %b %e %H:%M:%S") { 202 | Ok(ts) => ts, 203 | Err(_why) => return None, 204 | }; 205 | 206 | while buf[first] == b' ' { 207 | first += 1; 208 | } 209 | let hostname = syslog_parse_opt_string(buf, &mut first, &len); 210 | 211 | // the remainder of buf 212 | let msg = match String::from_utf8(buf[first..len].to_vec()) { 213 | Ok(m) => Some(m), 214 | Err(_why) => None, 215 | }; 216 | 217 | Some(SyslogMsg { 218 | from: from, 219 | facility: facility, 220 | severity: severity, 221 | version: 0, 222 | timestamp: Some(timestamp.with_timezone(&Utc)), 223 | hostname: hostname, 224 | appname: None, 225 | procid: None, 226 | msgid: None, 227 | sdata: None, 228 | msg: msg, 229 | }) 230 | } 231 | 232 | fn from_asl(from: SocketAddr, len: usize, buf: &[u8]) -> Option { 233 | fn syslog_parse_asl_facility_name(name: &str) -> u8 { 234 | match name { 235 | "auth" => LOG_AUTH, 236 | "authpriv" => LOG_AUTHPRIV, 237 | "cron" => LOG_CRON, 238 | "daemon" => LOG_DAEMON, 239 | "ftp" => LOG_FTP, 240 | "install" => LOG_INSTALL, 241 | "kern" => LOG_KERN, 242 | "lpr" => LOG_LPR, 243 | "mail" => LOG_MAIL, 244 | "netinfo" => LOG_NETINFO, 245 | "remoteauth" => LOG_REMOTEAUTH, 246 | "news" => LOG_NEWS, 247 | "security" => LOG_AUTH, 248 | "syslog" => LOG_SYSLOG, 249 | "user" => LOG_USER, 250 | "uucp" => LOG_UUCP, 251 | "local0" => LOG_LOCAL0, 252 | "local1" => LOG_LOCAL1, 253 | "local2" => LOG_LOCAL2, 254 | "local3" => LOG_LOCAL3, 255 | "local4" => LOG_LOCAL4, 256 | "local5" => LOG_LOCAL5, 257 | "local6" => LOG_LOCAL6, 258 | "local7" => LOG_LOCAL7, 259 | "launchd" => LOG_LAUNCHD, 260 | _ => LOG_USER, 261 | } 262 | } 263 | 264 | let mut first = 0; 265 | let lenstr = match str::from_utf8(&buf[0..10]) { 266 | Ok(s) => s, 267 | Err(_why) => return None, 268 | }; 269 | let mut msglen: usize = match lenstr.trim().parse() { 270 | Ok(m) => m, 271 | Err(_why) => return None, 272 | }; 273 | first += lenstr.len() + 1; 274 | 275 | let mut sdata: HashMap = HashMap::new(); 276 | while msglen > 0 { 277 | let kv_str = buf[first..len].slice_between_brackets(); 278 | let kv_len = kv_str.len(); 279 | if kv_len == 0 { 280 | break; 281 | } 282 | msglen -= kv_len; 283 | first += kv_len; 284 | 285 | while buf[first] == b' ' { 286 | msglen -= 1; 287 | first += 1; 288 | } 289 | let pair: Vec<&str> = str::from_utf8(&kv_str[1..kv_len - 1]) 290 | .unwrap() 291 | .splitn(2, ' ') 292 | .collect(); 293 | sdata.insert(pair[0].to_string(), pair[1].to_string()); 294 | } 295 | let severity = match sdata.remove("Level") { 296 | Some(val) => match val.parse() { 297 | Ok(num) => num, 298 | Err(_e) => return None, 299 | }, 300 | None => 7, 301 | }; 302 | let facility = match sdata.remove("Facility") { 303 | Some(val) => syslog_parse_asl_facility_name(&val), 304 | None => LOG_USER, 305 | }; 306 | let time_sec: i64 = match sdata.remove("Time") { 307 | Some(val) => match val.parse() { 308 | Ok(num) => num, 309 | Err(_e) => return None, 310 | }, 311 | None => 0, 312 | }; 313 | let time_nanosec: u32 = match sdata.remove("TimeNanoSec") { 314 | Some(val) => match val.parse() { 315 | Ok(num) => num, 316 | Err(_e) => return None, 317 | }, 318 | None => 0, 319 | }; 320 | let hostname: Option = match sdata.remove("Host") { 321 | Some(val) => Some(val.to_string()), 322 | None => None, 323 | }; 324 | let appname: Option = match sdata.remove("Sender") { 325 | Some(val) => Some(val.to_string()), 326 | None => None, 327 | }; 328 | let procid: Option = match sdata.remove("PID") { 329 | Some(val) => Some(val.to_string()), 330 | None => None, 331 | }; 332 | let msg: Option = match sdata.remove("Message") { 333 | Some(val) => Some(val.to_string()), 334 | None => None, 335 | }; 336 | let timestamp = Utc.timestamp(time_sec, time_nanosec); 337 | return Some(SyslogMsg { 338 | from: from, 339 | facility: facility, 340 | severity: severity, 341 | version: 0, 342 | timestamp: Some(timestamp), 343 | hostname: hostname, 344 | appname: appname, 345 | procid: procid, 346 | msgid: None, 347 | msg: msg, 348 | sdata: Some(sdata), 349 | }); 350 | } 351 | } 352 | 353 | fn syslog_parse_pri(pri_with_arrows: &[u8]) -> Option<(u8, u8)> { 354 | let len = pri_with_arrows.len(); 355 | if len < 3 || len > 5 { 356 | return None; 357 | } 358 | let pri_str = str::from_utf8(&pri_with_arrows[1..len - 1]).unwrap(); 359 | let num: i32 = pri_str.parse().unwrap(); 360 | let facility = num / 8; 361 | let severity = num % 8; 362 | Some((facility as u8, severity as u8)) 363 | } 364 | 365 | fn syslog_parse_opt_string(buf: &[u8], first: &mut usize, len: &usize) -> Option { 366 | let val = match String::from_utf8(buf[*first..*len].slice_until_space().to_vec()) { 367 | Ok(v) => { 368 | let vlen = v.len(); 369 | *first += vlen; 370 | if vlen == 1 && v == "-" { 371 | None 372 | } else { 373 | Some(v) 374 | } 375 | } 376 | Err(_why) => return None, 377 | }; 378 | 379 | while *first < *len && buf[*first] == b' ' { 380 | *first += 1; 381 | } 382 | val 383 | } 384 | 385 | fn syslog_version_1(version: &str) -> bool { 386 | version == "1" 387 | } 388 | 389 | // In old BSD syle, a three letter abreviation of the month capitalized follows the priority. Test for this. 390 | fn syslog_bsd_style(month: &str) -> bool { 391 | let months = [ 392 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 393 | ]; 394 | months.contains(&month) 395 | } 396 | 397 | // decode packet 398 | pub fn parse(from: SocketAddr, len: usize, buf: &[u8]) -> Option { 399 | let mut first = 0; 400 | let pri_str = buf[first..len].slice_between_arrows(); 401 | let pri_len = pri_str.len(); 402 | 403 | // check for Apple Syslog Log (asl) format 404 | if pri_len == 0 { 405 | // the first 10 characters should be length of the asl message. 406 | if len < 10 { 407 | return None; 408 | } 409 | return SyslogMsg::from_asl(from, len, buf); 410 | } 411 | 412 | let (facility, severity) = match syslog_parse_pri(pri_str) { 413 | Some((f, s)) => (f, s), 414 | None => return None, 415 | }; 416 | 417 | first += pri_len; 418 | 419 | let vstr = match str::from_utf8(buf[first..len].slice_until_space()) { 420 | Ok(s) => s, 421 | Err(_why) => return None, 422 | }; 423 | if syslog_version_1(vstr) { 424 | // assume RFC 5424 format 425 | SyslogMsg::from_version_1(from, len, buf, first, vstr, facility, severity) 426 | } else if syslog_bsd_style(vstr) { 427 | // assume RFC 3164 format 428 | SyslogMsg::from_bsd(from, len, buf, first, facility, severity) 429 | } else { 430 | None 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! Stage 5 - syslog server rslogd 2 | //! 3 | //! MUST run as root or use sudo 4 | //! 5 | //! ``` 6 | //! cargo build 7 | //! sudo target/debug/rslogd --certs ./fullchain.pem --key ./privkey.pem 8 | //! ``` 9 | //! 10 | //! # panics 11 | //! 12 | //! If socket cannot bind to syslog UDP port 514 (permissions or already in use) 13 | //! 14 | 15 | use docopt::Docopt; 16 | use index_pool::IndexPool; 17 | use mio::net::{TcpListener, TcpStream, UdpSocket}; 18 | use mio::{Events, Poll, PollOpt, Ready, Token}; 19 | use rustls; 20 | use rustls::Session; 21 | use socket2::{Domain, Protocol, Socket, Type}; 22 | use std::collections::HashMap; 23 | use std::fs; 24 | use std::io::BufReader; 25 | use std::io::Read; 26 | use std::io::{Error, ErrorKind}; 27 | use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; 28 | use std::sync::Arc; 29 | 30 | #[macro_use] 31 | extern crate serde_derive; 32 | 33 | mod syslog; 34 | 35 | const SYSLOG_UDP_PORT: u16 = 514; 36 | const SYSLOG_TCP_PORT: u16 = 601; 37 | const SYSLOG_TLS_PORT: u16 = 6514; 38 | 39 | const UDP4: Token = Token(0); 40 | const UDP6: Token = Token(1); 41 | const TCP4: Token = Token(2); 42 | const TCP6: Token = Token(3); 43 | const TLS4: Token = Token(4); 44 | const TLS6: Token = Token(5); 45 | 46 | struct ClientConnection { 47 | stream: TcpStream, 48 | session: Option, 49 | sa: SocketAddr, 50 | token_index: usize, 51 | } 52 | 53 | // from https://github.com/ctz/rustls/blob/master/rustls-mio/examples/tlsserver.rs 54 | fn load_certs(filename: &str) -> Vec { 55 | let certfile = fs::File::open(filename).expect("cannot open certificate file"); 56 | let mut reader = BufReader::new(certfile); 57 | rustls::internal::pemfile::certs(&mut reader).unwrap() 58 | } 59 | 60 | // from https://github.com/ctz/rustls/blob/master/rustls-mio/examples/tlsserver.rs 61 | fn load_private_key(filename: &str) -> rustls::PrivateKey { 62 | let rsa_keys = { 63 | let keyfile = fs::File::open(filename).expect("cannot open private key file"); 64 | let mut reader = BufReader::new(keyfile); 65 | rustls::internal::pemfile::rsa_private_keys(&mut reader) 66 | .expect("file contains invalid rsa private key") 67 | }; 68 | 69 | let pkcs8_keys = { 70 | let keyfile = fs::File::open(filename).expect("cannot open private key file"); 71 | let mut reader = BufReader::new(keyfile); 72 | rustls::internal::pemfile::pkcs8_private_keys(&mut reader) 73 | .expect("file contains invalid pkcs8 private key (encrypted keys not supported)") 74 | }; 75 | 76 | // prefer to load pkcs8 keys 77 | if !pkcs8_keys.is_empty() { 78 | pkcs8_keys[0].clone() 79 | } else { 80 | assert!(!rsa_keys.is_empty()); 81 | rsa_keys[0].clone() 82 | } 83 | } 84 | 85 | const USAGE: &'static str = " 86 | Syslog server that supports UDP, TCP (deprecated), and TLS over IPv4 and IPv6. 87 | 88 | `--certs' names the full certificate chain, 89 | `--key' provides the RSA private key. 90 | 91 | Usage: 92 | rslogd [--verbose] --certs CERTFILE --key KEYFILE 93 | rslogd (--version | -v) 94 | rslogd (--help | -h) 95 | 96 | Options: 97 | --certs CERTFILE Read server certificates from CERTFILE. 98 | This should contain PEM-format certificates 99 | in the right order (the first certificate should 100 | certify KEYFILE, the last should be a root CA). 101 | --key KEYFILE Read private key from KEYFILE. This should be a RSA 102 | private key or PKCS8-encoded private key in PEM format. 103 | --verbose Monitor progress. 104 | --version, -v Show version. 105 | --help, -h Show this screen. 106 | "; 107 | 108 | #[derive(Debug, Deserialize)] 109 | struct Args { 110 | flag_verbose: bool, 111 | flag_certs: Option, 112 | flag_key: Option, 113 | } 114 | 115 | fn main() -> Result<(), Error> { 116 | let version = env!("CARGO_PKG_NAME").to_string() + ", version: " + env!("CARGO_PKG_VERSION"); 117 | 118 | let args: Args = Docopt::new(USAGE) 119 | .and_then(|d| Ok(d.help(true))) 120 | .and_then(|d| Ok(d.version(Some(version)))) 121 | .and_then(|d| d.deserialize()) 122 | .unwrap_or_else(|e| e.exit()); 123 | 124 | let mut events = Events::with_capacity(256); 125 | let poll = Poll::new()?; 126 | let mut buffer = [0; 4096]; 127 | 128 | // UDP IPv4 129 | let udp4_server_s = Socket::new(Domain::ipv4(), Type::dgram(), Some(Protocol::udp()))?; 130 | let sa_udp4 = SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), SYSLOG_UDP_PORT); 131 | 132 | #[cfg(unix)] 133 | udp4_server_s.set_reuse_port(true)?; 134 | udp4_server_s.set_reuse_address(true)?; 135 | udp4_server_s.bind(&sa_udp4.into())?; 136 | let udp4_server_mio = UdpSocket::from_socket(udp4_server_s.into_udp_socket())?; 137 | 138 | poll.register(&udp4_server_mio, UDP4, Ready::readable(), PollOpt::edge())?; 139 | 140 | // UDP IPv6 141 | let udp6_server_s = Socket::new(Domain::ipv6(), Type::dgram(), Some(Protocol::udp()))?; 142 | let sa6 = SocketAddr::new( 143 | Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0).into(), 144 | SYSLOG_UDP_PORT, 145 | ); 146 | 147 | #[cfg(unix)] 148 | udp6_server_s.set_reuse_port(true)?; 149 | udp6_server_s.set_reuse_address(true)?; 150 | udp6_server_s.set_only_v6(true)?; 151 | udp6_server_s.bind(&sa6.into())?; 152 | let udp6_server_mio = UdpSocket::from_socket(udp6_server_s.into_udp_socket())?; 153 | 154 | poll.register(&udp6_server_mio, UDP6, Ready::readable(), PollOpt::edge())?; 155 | 156 | // TCP IPv4 157 | let tcp4_server_s = Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp()))?; 158 | let sa_tcp4 = SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), SYSLOG_TCP_PORT); 159 | tcp4_server_s.set_reuse_address(true)?; 160 | 161 | #[cfg(unix)] 162 | tcp4_server_s.set_reuse_port(true)?; 163 | tcp4_server_s.bind(&sa_tcp4.into())?; 164 | tcp4_server_s.listen(128)?; 165 | let tcp4_listener = TcpListener::from_std(tcp4_server_s.into_tcp_listener())?; 166 | poll.register(&tcp4_listener, TCP4, Ready::readable(), PollOpt::edge())?; 167 | 168 | // TCP IPv6 169 | let tcp6_server_s = Socket::new(Domain::ipv6(), Type::stream(), Some(Protocol::tcp()))?; 170 | let sa_tcp6 = SocketAddr::new( 171 | Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0).into(), 172 | SYSLOG_TCP_PORT, 173 | ); 174 | tcp6_server_s.set_reuse_address(true)?; 175 | 176 | #[cfg(unix)] 177 | tcp6_server_s.set_reuse_port(true)?; 178 | tcp6_server_s.set_only_v6(true)?; 179 | tcp6_server_s.bind(&sa_tcp6.into())?; 180 | tcp6_server_s.listen(128)?; 181 | let tcp6_listener = TcpListener::from_std(tcp6_server_s.into_tcp_listener())?; 182 | poll.register(&tcp6_listener, TCP6, Ready::readable(), PollOpt::edge())?; 183 | 184 | // general TLS setup 185 | let mut tls_conf = rustls::ServerConfig::new(rustls::NoClientAuth::new()); 186 | let certs = load_certs(args.flag_certs.as_ref().expect("--certs option missing")); 187 | let privkey = load_private_key(args.flag_key.as_ref().expect("--key option missing")); 188 | tls_conf 189 | .set_single_cert(certs, privkey) 190 | .expect("bad certificates/private key"); 191 | let tls_config = Arc::new(tls_conf); 192 | 193 | // TLS IPv4 194 | let tls4_server_s = Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp()))?; 195 | let sa_tls4 = SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), SYSLOG_TLS_PORT); 196 | tls4_server_s.set_reuse_address(true)?; 197 | #[cfg(unix)] 198 | tls4_server_s.set_reuse_port(true)?; 199 | tls4_server_s.bind(&sa_tls4.into())?; 200 | tls4_server_s.listen(128)?; 201 | let tls4_listener = TcpListener::from_std(tls4_server_s.into_tcp_listener())?; 202 | poll.register(&tls4_listener, TLS4, Ready::readable(), PollOpt::edge())?; 203 | 204 | // TLS IPv6 205 | let tls6_server_s = Socket::new(Domain::ipv6(), Type::stream(), Some(Protocol::tcp()))?; 206 | let sa_tls6 = SocketAddr::new( 207 | Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0).into(), 208 | SYSLOG_TLS_PORT, 209 | ); 210 | tls6_server_s.set_reuse_address(true)?; 211 | #[cfg(unix)] 212 | tls6_server_s.set_reuse_port(true)?; 213 | tls6_server_s.bind(&sa_tls6.into())?; 214 | tls6_server_s.listen(128)?; 215 | let tls6_listener = TcpListener::from_std(tls6_server_s.into_tcp_listener())?; 216 | poll.register(&tls6_listener, TLS6, Ready::readable(), PollOpt::edge())?; 217 | 218 | let mut tokens: HashMap = HashMap::new(); 219 | let mut pool = IndexPool::with_initial_index(6); // allocate unused index for accepted sockets 220 | loop { 221 | poll.poll(&mut events, None)?; 222 | for event in events.iter() { 223 | match event.token() { 224 | UDP4 => match receive_udp(&udp4_server_mio, &mut buffer) { 225 | Ok(()) => continue, 226 | Err(e) => { 227 | eprintln!("IPv4 receive {}", e); 228 | } 229 | }, 230 | UDP6 => match receive_udp(&udp6_server_mio, &mut buffer) { 231 | Ok(()) => continue, 232 | Err(e) => { 233 | eprintln!("IPv6 receive {}", e); 234 | } 235 | }, 236 | TCP4 => match tcp4_listener.accept() { 237 | Ok((stream, sa)) => { 238 | let idx = pool.new_id(); 239 | let key = Token(idx); 240 | poll.register(&stream, key, Ready::readable(), PollOpt::edge())?; 241 | let conn = ClientConnection { 242 | stream: stream, 243 | session: None, 244 | sa: sa, 245 | token_index: idx, 246 | }; 247 | tokens.insert(key, conn); 248 | } 249 | Err(_e) => eprintln!("tcp4 connection error"), 250 | }, 251 | TCP6 => match tcp6_listener.accept() { 252 | Ok((stream, sa)) => { 253 | let idx = pool.new_id(); 254 | let key = Token(idx); 255 | poll.register(&stream, key, Ready::readable(), PollOpt::edge())?; 256 | let conn = ClientConnection { 257 | stream: stream, 258 | session: None, 259 | sa: sa, 260 | token_index: idx, 261 | }; 262 | tokens.insert(key, conn); 263 | } 264 | Err(_e) => eprintln!("tcp6 connection error"), 265 | }, 266 | TLS4 => match tls4_listener.accept() { 267 | Ok((stream, sa)) => { 268 | let tls_session = rustls::ServerSession::new(&tls_config); 269 | let idx = pool.new_id(); 270 | let key = Token(idx); 271 | poll.register(&stream, key, Ready::readable(), PollOpt::edge())?; 272 | let conn = ClientConnection { 273 | stream: stream, 274 | session: Some(tls_session), 275 | sa: sa, 276 | token_index: idx, 277 | }; 278 | tokens.insert(key, conn); 279 | } 280 | Err(_e) => eprintln!("tls4 connection error"), 281 | }, 282 | TLS6 => match tls6_listener.accept() { 283 | Ok((stream, sa)) => { 284 | let tls_session = rustls::ServerSession::new(&tls_config); 285 | let idx = pool.new_id(); 286 | let key = Token(idx); 287 | poll.register(&stream, key, Ready::readable(), PollOpt::edge())?; 288 | let conn = ClientConnection { 289 | stream: stream, 290 | session: Some(tls_session), 291 | sa: sa, 292 | token_index: idx, 293 | }; 294 | tokens.insert(key, conn); 295 | } 296 | Err(_e) => eprintln!("tls6 connection error"), 297 | }, 298 | tok => { 299 | match tokens.get_mut(&tok) { 300 | Some(conn_ref) => { 301 | // if we have a TLS session, use TLS methods 302 | if let Some(ref mut session) = conn_ref.session { 303 | if session.is_handshaking() { 304 | if session.wants_read() { 305 | let rc = session.read_tls(&mut conn_ref.stream); 306 | if rc.is_err() { 307 | continue; 308 | } 309 | if rc.unwrap() == 0 { 310 | continue; 311 | } 312 | let rc2 = session.process_new_packets(); 313 | if rc2.is_err() { 314 | continue; 315 | } 316 | } 317 | while session.wants_write() { 318 | let rc = session.write_tls(&mut conn_ref.stream); 319 | if rc.is_err() { 320 | continue; 321 | } 322 | if rc.unwrap() == 0 { 323 | continue; 324 | } 325 | } 326 | continue; 327 | } 328 | // finished TLS handshake 329 | if receive_tls(conn_ref, &mut buffer) { 330 | poll.deregister(&conn_ref.stream)?; 331 | pool.return_id(conn_ref.token_index) 332 | .expect("tls pool return id"); 333 | tokens.remove(&tok); 334 | } 335 | } else { 336 | // it's TCP (no session) 337 | if receive_tcp(conn_ref, &mut buffer) { 338 | poll.deregister(&conn_ref.stream)?; 339 | pool.return_id(conn_ref.token_index) 340 | .expect("tcp pool return id"); 341 | tokens.remove(&tok); 342 | } 343 | } 344 | } 345 | None => eprintln!("missing stream for Token {:?}", tok), 346 | } 347 | } 348 | } 349 | } 350 | } 351 | } 352 | 353 | // common receive routine 354 | fn receive_udp(sock: &UdpSocket, buf: &mut [u8]) -> Result<(), Error> { 355 | loop { 356 | let (len, from) = match sock.recv_from(buf) { 357 | Ok((len, from)) => (len, from), 358 | Err(e) => { 359 | if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::Interrupted { 360 | return Ok(()); 361 | } else { 362 | return Err(e); 363 | } 364 | } 365 | }; 366 | 367 | if let Some(msg) = syslog::parse(from, len, buf) { 368 | println!("{:?}", msg); 369 | } else { 370 | match std::str::from_utf8(buf) { 371 | Ok(s) => eprintln!("error parsing: {}", s), 372 | Err(e) => eprintln!("received message not parseable and not UTF-8: {}", e), 373 | } 374 | } 375 | } 376 | } 377 | 378 | fn receive_tcp(conn_ref: &mut ClientConnection, buf: &mut [u8]) -> bool { 379 | loop { 380 | match conn_ref.stream.read(buf) { 381 | Ok(0) => { 382 | // client closed connection, cleanup 383 | return true; 384 | } 385 | Ok(len) => { 386 | // we have a message to process 387 | if let Some(msg) = syslog::parse(conn_ref.sa, len, buf) { 388 | println!("{:?}", msg); 389 | } else { 390 | println!( 391 | "error parsing: {:?}", 392 | String::from_utf8(buf[0..len].to_vec()) 393 | ); 394 | } 395 | } 396 | Err(e) => { 397 | if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::Interrupted { 398 | // nothing else to read but connection still open 399 | return false; 400 | } else { 401 | eprintln!("TCP read error: {}", e); 402 | // cleanup 403 | return true; 404 | } 405 | } 406 | } 407 | } 408 | } 409 | 410 | fn receive_tls(conn_ref: &mut ClientConnection, buf: &mut [u8]) -> bool { 411 | if let Some(ref mut session) = conn_ref.session { 412 | loop { 413 | match session.read_tls(&mut conn_ref.stream) { 414 | Ok(0) => { 415 | // client closed connection, cleanup 416 | session.send_close_notify(); 417 | return true; 418 | } 419 | Ok(_len) => { 420 | // successfully read len bytes, fall through 421 | } 422 | Err(e) => { 423 | if e.kind() == ErrorKind::WouldBlock || e.kind() == ErrorKind::Interrupted { 424 | // nothing else to read but connection still open 425 | return false; 426 | } else { 427 | eprintln!("TLS read_tls error: {}", e); 428 | // cleanup 429 | return true; 430 | } 431 | } 432 | } 433 | 434 | let processed = session.process_new_packets(); 435 | if processed.is_err() { 436 | eprintln!("tls process new packets error"); 437 | return true; 438 | } 439 | let rc = session.read(&mut buf[..2048]); 440 | match rc { 441 | Ok(0) => { 442 | eprintln!("tls session read 0 length"); 443 | return true; 444 | } 445 | Ok(len) => { 446 | if let Some(msg) = syslog::parse(conn_ref.sa, len, buf) { 447 | println!("{:?}", msg); 448 | } else { 449 | eprintln!( 450 | "error parsing {} bytes over TLS: {:?}", 451 | len, 452 | String::from_utf8(buf[0..len].to_vec()) 453 | ); 454 | } 455 | } 456 | Err(e) => { 457 | // if client didn't close connection, print error 458 | if e.kind() != ErrorKind::ConnectionAborted { 459 | eprintln!("tls session.read() error: {:?}", e.kind()); 460 | } 461 | return true; 462 | } 463 | } 464 | } 465 | } else { 466 | eprintln!("can't find tls session error"); 467 | true 468 | } 469 | } 470 | --------------------------------------------------------------------------------