From 2b58f2fd0836490001a456d555afc8bd42424016 Mon Sep 17 00:00:00 2001 From: Kraemii Date: Thu, 24 Oct 2024 12:32:15 +0200 Subject: [PATCH] Refactor sockets Split socket into different modules for handling tcp and udp connections. This reduces complexety of the socket module. --- rust/Cargo.lock | 6 - rust/examples/socket/ftp_login.nasl | 16 + rust/src/nasl/builtin/network/README.md | 2 +- rust/src/nasl/builtin/network/mod.rs | 5 +- .../src/nasl/builtin/network/network_utils.rs | 21 +- rust/src/nasl/builtin/network/socket.rs | 658 ++++++------------ rust/src/nasl/builtin/network/tcp.rs | 154 ++++ rust/src/nasl/builtin/network/tls.rs | 105 +++ rust/src/nasl/builtin/network/udp.rs | 100 +++ 9 files changed, 583 insertions(+), 484 deletions(-) create mode 100644 rust/examples/socket/ftp_login.nasl create mode 100644 rust/src/nasl/builtin/network/tcp.rs create mode 100644 rust/src/nasl/builtin/network/tls.rs create mode 100644 rust/src/nasl/builtin/network/udp.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 6d596400b..2ba732341 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2465,12 +2465,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "portable-atomic" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" - [[package]] name = "powerfmt" version = "0.2.0" diff --git a/rust/examples/socket/ftp_login.nasl b/rust/examples/socket/ftp_login.nasl new file mode 100644 index 000000000..f553fdc1a --- /dev/null +++ b/rust/examples/socket/ftp_login.nasl @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2024 Greenbone AG +# +# SPDX-License-Identifier: GPL-2.0-or-later WITH x11vnc-openssl-exception + +display("Start"); +display("is function open_sock_tcp defined: ", defined_func("open_sock_tcp")); +sock = open_sock_tcp(21, transport: 1); +display("was socket created: ", !isnull(sock)); +display("fd: ", sock); +display("is function ftp_log_in defined: ", defined_func("ftp_log_in")); +# Login data for ftp://ftp.dlptest.com/ provided by https://dlptest.com/ftp-test/ +user = "dlpuser"; +pass = "rNrKYTX9g7z3RgJRmxWuGHbeu"; +display("login succeeded: ", ftp_log_in(user: user, pass: pass, socket: sock)); +close(sock); +display("end"); diff --git a/rust/src/nasl/builtin/network/README.md b/rust/src/nasl/builtin/network/README.md index 5a8cbfe15..6a72c11ad 100644 --- a/rust/src/nasl/builtin/network/README.md +++ b/rust/src/nasl/builtin/network/README.md @@ -14,11 +14,11 @@ - scanner_add_port - recv_line - get_source_port +- ftp_log_in ## Missing - ftp_get_pasv_port -- ftp_log_in - get_host_open_port - get_port_state - get_port_transport diff --git a/rust/src/nasl/builtin/network/mod.rs b/rust/src/nasl/builtin/network/mod.rs index f6155d880..38befd08f 100644 --- a/rust/src/nasl/builtin/network/mod.rs +++ b/rust/src/nasl/builtin/network/mod.rs @@ -12,6 +12,9 @@ use crate::storage::{Field, Retrieve}; pub mod network; pub mod network_utils; pub mod socket; +pub mod tcp; +pub mod tls; +pub mod udp; // 512 Bytes are typically supported by network devices. The ip header maximum size is 60 and a UDP // header contains 8 bytes, which must be subtracted from the max size for UDP packages. @@ -80,7 +83,7 @@ pub fn get_retry(context: &Context) -> u8 { match val { NaslValue::String(val) => val.parse::().unwrap_or(2), NaslValue::Number(val) => { - if val < 1 || val > 255 { + if !(1..=255).contains(&val) { 2 } else { val as u8 diff --git a/rust/src/nasl/builtin/network/network_utils.rs b/rust/src/nasl/builtin/network/network_utils.rs index 7074ccc36..995ca8ca3 100644 --- a/rust/src/nasl/builtin/network/network_utils.rs +++ b/rust/src/nasl/builtin/network/network_utils.rs @@ -4,6 +4,7 @@ //! This module provides utility functions for IP handling. use std::{ + io, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, UdpSocket}, ptr, str::FromStr, @@ -25,24 +26,16 @@ pub fn ipstr2ipaddr(ip_addr: &str) -> Result { /// Convert timeout pub fn convert_timeout(timeout: Option) -> Option { - timeout.and_then(|timeout| { - if timeout < 1 { - None - } else { - Some(Duration::from_secs(timeout as u64)) - } - }) + timeout + .filter(|timeout| *timeout >= 1) + .map(|timeout| Duration::from_secs(timeout as u64)) } /// Bind a local UDP socket to a V4 or V6 address depending on the given destination address -pub fn bind_local_socket(dst: &SocketAddr) -> Result { - let fe = Err(FunctionErrorKind::Diagnostic( - "Error binding".to_string(), - None, - )); +pub fn bind_local_socket(dst: &SocketAddr) -> io::Result { match dst { - SocketAddr::V4(_) => UdpSocket::bind("0.0.0.0:0").or(fe), - SocketAddr::V6(_) => UdpSocket::bind("[::]:0").or(fe), + SocketAddr::V4(_) => UdpSocket::bind("0.0.0.0:0"), + SocketAddr::V6(_) => UdpSocket::bind("[::]:0"), } } diff --git a/rust/src/nasl/builtin/network/socket.rs b/rust/src/nasl/builtin/network/socket.rs index aa5a0c1bc..0032729a8 100644 --- a/rust/src/nasl/builtin/network/socket.rs +++ b/rust/src/nasl/builtin/network/socket.rs @@ -3,11 +3,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later use std::{ - fs, - io::{self, BufReader, Read, Write}, - net::{IpAddr, SocketAddr, TcpStream, UdpSocket}, - os::fd::AsRawFd, - sync::{Arc, RwLock}, + io::{BufRead, Read, Write}, + sync::RwLock, thread::sleep, time::{Duration, SystemTime}, }; @@ -17,21 +14,16 @@ use crate::nasl::syntax::NaslValue; use crate::nasl::utils::{error::FunctionErrorKind, Context}; use dns_lookup::lookup_host; use nasl_function_proc_macro::nasl_function; -use pkcs8::der::Decode; -use rustls::{ - pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer, ServerName}, - ClientConfig, ClientConnection, RootCertStore, Stream, -}; use super::{ - get_kb_item, get_retry, mtu, - network_utils::{bind_local_socket, convert_timeout, ipstr2ipaddr}, + get_kb_item, get_retry, + network_utils::{convert_timeout, ipstr2ipaddr}, + tcp::TcpConnection, + tls::create_tls_client, + udp::UdpConnection, verify_port, OpenvasEncaps, }; -// Number of times to resend a UDP packet, when no response is received -const NUM_TIMES_TO_RESEND: usize = 5; - pub struct Interval { interval: Duration, last_tick: SystemTime, @@ -48,92 +40,18 @@ impl Interval { } } -struct TLSConfig { +struct TlsConfig { cert_path: String, key_path: String, password: String, cafile_path: String, } -/// A TLS Connection consists of the underlying socket, an optional TLS Connection and an optional buffer -struct TCPConnection { - socket: TcpStream, - // Those values are currently unused, but needed for functions currently not implemented - tls_connection: Option, - // Buffer data for recv_line, to be able to read received messages line by line - buffer: Vec, - buffer_size: usize, - buffer_pos: usize, -} - -impl TCPConnection { - /// Send data on a TCP connection using the libc send function. - /// To ensure safety of the function, the caller must ensure, that the given length does not - /// exceed the length of the given data data. - unsafe fn send( - &self, - mut data: &[u8], - len: usize, - flags: i32, - ) -> Result { - let fd = self.socket.as_raw_fd(); - let mut ret = 0; - while !data.is_empty() { - let n = - unsafe { libc::send(fd, data.as_ptr() as *const libc::c_void, len - ret, flags) }; - if n < 0 { - return Err(io::Error::last_os_error().into()); - } - ret += n as usize; - data = &data[n as usize..]; - } - Ok(NaslValue::Number(ret as i64)) - } -} - -struct UDPConnection { - socket: UdpSocket, - buffer: Vec, -} - -impl UDPConnection { - /// Send data on a UDP connection using the libc send function. - /// To ensure safety of the function, the caller must ensure, that the given length does not - /// exceed the length of the given data data. - unsafe fn send( - &mut self, - data: &[u8], - len: usize, - flags: i32, - ) -> Result { - let fd = self.socket.as_raw_fd(); - - let ip = self.socket.peer_addr()?.ip(); - - let mtu = mtu(ip); - - if len > mtu { - return Err(FunctionErrorKind::Diagnostic( - format!( - "udp data of size {} exceeds the maximum length of {}", - len, mtu - ), - None, - )); - } - - let n = libc::send(fd, data.as_ptr() as *const libc::c_void, len, flags); - - self.buffer = data.to_vec(); - Ok(NaslValue::Number(n as i64)) - } -} - enum NaslSocket { - // The TCP Connection is boxed, because it uses allot of space + // The TCP Connection is boxed, because it uses a lot of space // This way the size of the enum is reduced - Tcp(Box), - Udp(UDPConnection), + Tcp(Box), + Udp(UdpConnection), Close, } @@ -150,14 +68,6 @@ pub struct NaslSockets { } impl NaslSockets { - fn open_udp(addr: IpAddr, port: u16) -> Result { - let sock_addr = SocketAddr::new(addr, port); - let socket = bind_local_socket(&sock_addr)?; - socket.connect(sock_addr)?; - socket.set_read_timeout(Some(Duration::from_secs(1)))?; - Ok(socket) - } - fn add(&self, socket: NaslSocket) -> usize { let mut handles = self .handles @@ -212,14 +122,14 @@ impl NaslSockets { /// - option: is the flags for the send() system call. You should not use a raw numeric value here. /// /// On success the number of sent bytes is returned. - #[nasl_function(named(socket, data, flags, len))] + #[nasl_function(named(socket, data, option, len))] fn send( &self, socket: usize, data: &[u8], - flags: Option, + option: Option, len: Option, - ) -> Result { + ) -> Result { let len = if let Some(len) = len { if len < 1 || len > data.len() { data.len() @@ -230,7 +140,7 @@ impl NaslSockets { data.len() }; - let mut data = &data[0..len]; + let data = &data[0..len]; match self .handles @@ -242,136 +152,26 @@ impl NaslSockets { "the given socket FD {socket} does not exist" )))? { NaslSocket::Tcp(conn) => { - // TCP self.wait_before_next_probe(); - if let Some(tls) = conn.tls_connection.as_mut() { - // TLS - let mut stream = Stream::new(tls, &mut conn.socket); - let mut ret = 0; - while !data.is_empty() { - let n = stream.write(data)?; - ret += n; - data = &data[n..]; + if !conn.is_tls() { + if let Some(flags) = option { + conn.set_flags(flags as i32); } - Ok(NaslValue::Number(ret as i64)) - } else { - unsafe { conn.send(data, len, flags.unwrap_or(0) as i32) } } - } - NaslSocket::Udp(conn) => unsafe { conn.send(data, len, flags.unwrap_or(0) as i32) }, - NaslSocket::Close => Err(FunctionErrorKind::WrongArgument( - "the given socket FD is already closed".to_string(), - )), - } - } - fn socket_read( - socket: &mut S, - data: &mut [u8], - min: usize, - ) -> Result { - let mut ret = 0; - while ret < data.len() && ret < min { - let n = socket.read(&mut data[ret..]).or_else(|e| match e.kind() { - io::ErrorKind::TimedOut => Ok(0), - _ => Err(e), - })?; - if n == 0 { - break; + Ok(conn.write(data)?) } - ret += n; - } - Ok(ret) - } - - fn read_buffer(socket: &mut TCPConnection, data: &mut [u8], min: usize) -> usize { - if socket.buffer_pos < socket.buffer.len() { - let read_length = std::cmp::min(data.len(), socket.buffer.len() - socket.buffer_pos); - let read_length = std::cmp::min(read_length, min); - if read_length > 0 { - data[..read_length].copy_from_slice( - &socket.buffer[socket.buffer_pos..socket.buffer_pos + read_length], - ); - socket.buffer_pos += read_length; - return read_length; - } - } - 0 - } - - fn socket_recv( - socket: &mut TCPConnection, - data: &mut [u8], - min: usize, - ) -> Result { - let read_length = Self::read_buffer(socket, data, min); - if read_length >= min { - return Ok(read_length); - } - - // Split scope to release stream after usage, so we can use the socket again - { - // Create stream depending on TLS Connection - let mut stream: Box = match &mut socket.tls_connection { - Some(tls) => Box::new(Stream::new(tls, &mut socket.socket)), - None => Box::new(&mut socket.socket), - }; - - // Receiving message is larger than buffer - if min - read_length > socket.buffer_size { - let read = - Self::socket_read(&mut stream, &mut data[read_length..], min - read_length)?; - return Ok(read_length + read); - } - - // Receive message in buffer - let mut buffer = vec![0; socket.buffer_size]; - let read = Self::socket_read(&mut stream, &mut buffer, socket.buffer_size)?; - socket.buffer = buffer[..read].to_vec(); - socket.buffer_pos = 0; - - // Nothing to read - if read == 0 { - return Ok(read_length); - } - } - - let read_length = - read_length + Self::read_buffer(socket, &mut data[read_length..], min - read_length); - - Ok(read_length) - } - - fn socket_recv_line( - socket: &mut TCPConnection, - data: &mut [u8], - timeout: Option, - ) -> Result { - let timeout = convert_timeout(timeout); - let start = if timeout.is_some() { - Some(std::time::Instant::now()) - } else { - None - }; - let mut bytes_read = 0; - loop { - let ret = Self::socket_recv(socket, &mut data[bytes_read..], 1)?; - - if ret == 0 { - if let (Some(start), Some(timeout)) = (start, timeout) { - if start.elapsed() > timeout { - break; - } + NaslSocket::Udp(conn) => { + if let Some(flags) = option { + conn.set_flags(flags as i32); } - continue; - } - if data[bytes_read] == b'\n' { - break; + Ok(conn.write(data)?) } - bytes_read += 1; + NaslSocket::Close => Err(FunctionErrorKind::WrongArgument( + "the given socket FD is already closed".to_string(), + )), } - Ok(bytes_read) } /// Receives data from a TCP or UDP socket. For a UDP socket, if it cannot read data, NASL will @@ -394,8 +194,6 @@ impl NaslSockets { .unwrap_or(length); let mut data = vec![0; length]; - let mut ret = Ok(NaslValue::Null); - match self .handles .write() @@ -406,56 +204,22 @@ impl NaslSockets { "the given socket FD {socket} does not exist" )))? { NaslSocket::Tcp(conn) => { - let mut old = None; - if let Some(timeout) = timeout { - old = conn.socket.read_timeout().unwrap(); - conn.socket - .set_read_timeout(Some(Duration::from_secs(timeout as u64)))?; + let mut pos = 0; + while pos < min { + pos += match convert_timeout(timeout) { + Some(timeout) => conn.read_with_timeout(&mut data[pos..], timeout), + None => conn.read(&mut data[pos..]), + }?; } - let pos = Self::socket_recv(conn, &mut data, min)?; - - if let Some(timeout) = old { - conn.socket.set_read_timeout(Some(timeout))?; - } - Ok(NaslValue::Data(data[..pos].to_vec())) } NaslSocket::Udp(conn) => { - let mut old = None; - if let Some(timeout) = timeout { - old = conn.socket.read_timeout().unwrap(); - conn.socket - .set_read_timeout(Some(Duration::from_secs(timeout as u64)))?; - } - - let mut result = conn.socket.recv_from(data.as_mut_slice()); - - for _ in 0..NUM_TIMES_TO_RESEND { - match result { - Ok((size, origin)) => { - if conn.socket.peer_addr()? == origin { - data.truncate(size); - ret = Ok(NaslValue::Data(data)); - break; - } - } - Err(e) => match e.kind() { - io::ErrorKind::TimedOut => { - conn.socket.send(&conn.buffer)?; - } - kind => { - ret = Err(FunctionErrorKind::IOError(kind)); - break; - } - }, - }; + let pos = match convert_timeout(timeout) { + Some(timeout) => conn.read_with_timeout(&mut data, timeout), + None => conn.read(&mut data), + }?; - result = conn.socket.recv_from(data.as_mut_slice()); - } - if let Some(timeout) = old { - conn.socket.set_read_timeout(Some(timeout))?; - } - ret + Ok(NaslValue::Data(data[..pos].to_vec())) } NaslSocket::Close => Err(FunctionErrorKind::WrongArgument( "the given socket FD is already closed".to_string(), @@ -467,10 +231,10 @@ impl NaslSockets { fn recv_line( &self, socket: usize, - length: usize, + #[allow(unused_variables)] length: usize, timeout: Option, ) -> Result { - let mut data = vec![0; length]; + let mut data = String::new(); match self .handles .write() @@ -481,12 +245,11 @@ impl NaslSockets { "the given socket FD {socket} does not exist" )))? { NaslSocket::Tcp(conn) => { - if conn.buffer_size == 0 { - conn.buffer_size = length; - } - Self::socket_recv_line(conn, &mut data, timeout)?; - - Ok(NaslValue::Data(data)) + let pos = match convert_timeout(timeout) { + Some(timeout) => conn.read_line_with_timeout(&mut data, timeout), + None => conn.read_line(&mut data), + }?; + Ok(NaslValue::Data(data.as_bytes()[..pos].to_vec())) } NaslSocket::Udp(_) => Err(FunctionErrorKind::Diagnostic( "This function is only available for TCP connections".to_string(), @@ -551,20 +314,18 @@ impl NaslSockets { .unwrap_or(false); let socket = if use_tcp { - let tcp = Self::open_tcp_stream(ip, port, Duration::from_secs(30), get_retry(context))?; - NaslSocket::Tcp(Box::new(TCPConnection { - socket: tcp, - tls_connection: None, - buffer: vec![], - buffer_size: 0, - buffer_pos: 0, - })) + let tcp = TcpConnection::connect( + ip, + port, + None, + Duration::from_secs(30), + None, + get_retry(context), + )?; + NaslSocket::Tcp(Box::new(tcp)) } else { - let udp = Self::open_udp(ip, port)?; - NaslSocket::Udp(UDPConnection { - socket: udp, - buffer: vec![], - }) + let udp = UdpConnection::new(ip, port)?; + NaslSocket::Udp(udp) }; let ret = self.add(socket); @@ -591,13 +352,13 @@ impl NaslSockets { #[nasl_function(named(timeout, transport, bufsz))] fn open_sock_tcp( &self, + context: &Context, port: i64, timeout: Option, transport: Option, bufsz: Option, // TODO: Extract information from custom priority string // priority: Option<&str>, - context: &Context, ) -> Result { // Get port let port = verify_port(port)?; @@ -609,21 +370,22 @@ impl NaslSockets { let mut fds = vec![]; - // TODO: set timeout to global recv timeout * 2 when available - let timeout = if let Some(sec) = timeout { - if sec < 1 { - Duration::from_secs(10) + let bufsz = if let Some(bufsz) = bufsz { + if bufsz < 0 { + None } else { - Duration::from_secs(sec as u64) + Some(bufsz as usize) } } else { - Duration::from_secs(10) + None }; + // TODO: set timeout to global recv timeout * 2 when available + let timeout = convert_timeout(timeout).unwrap_or(Duration::from_secs(10)); // TODO: for every vhost let vhosts = vec!["localhost"]; for vhost in vhosts { - let (tcp, tls) = if transport < 0 { + let tcp = if transport < 0 { // TODO: Get port transport and open connection depending on it todo!() } else { @@ -632,26 +394,43 @@ impl NaslSockets { Some(OpenvasEncaps::Auto) => { // Try SSL/TLS first let tls = if let Ok(conf) = Self::get_tls_conf(context) { - Self::create_tls_client(vhost, conf).ok() + create_tls_client( + vhost, + &conf.cert_path, + &conf.key_path, + &conf.password, + &conf.cafile_path, + ) + .ok() } else { None }; - if let Ok(tcp) = - Self::open_tcp_stream(addr, port, timeout, get_retry(context)) - { + if let Ok(tcp) = TcpConnection::connect( + addr, + port, + tls, + timeout, + bufsz, + get_retry(context), + ) { // TODO: Set port transport - (tcp, tls) + tcp } else { continue; } } // IP Some(OpenvasEncaps::Ip) => { - if let Ok(tcp) = - Self::open_tcp_stream(addr, port, timeout, get_retry(context)) - { + if let Ok(tcp) = TcpConnection::connect( + addr, + port, + None, + timeout, + bufsz, + get_retry(context), + ) { // TODO: Set port transport - (tcp, None) + tcp } else { continue; } @@ -665,13 +444,26 @@ impl NaslSockets { // TLS/SSL Some(tls_version) => match tls_version { OpenvasEncaps::Tls12 | OpenvasEncaps::Tls13 => { - if let Ok(tls) = - Self::create_tls_client(vhost, Self::get_tls_conf(context)?) - { - if let Ok(tcp) = - Self::open_tcp_stream(addr, port, timeout, get_retry(context)) - { - (tcp, Some(tls)) + if let Ok(tls_conf) = Self::get_tls_conf(context) { + if let Ok(tls) = create_tls_client( + vhost, + &tls_conf.cert_path, + &tls_conf.key_path, + &tls_conf.password, + &tls_conf.cafile_path, + ) { + if let Ok(tcp) = TcpConnection::connect( + addr, + port, + Some(tls), + timeout, + bufsz, + get_retry(context), + ) { + tcp + } else { + continue; + } } else { continue; } @@ -687,13 +479,7 @@ impl NaslSockets { }, } }; - let fd = self.add(NaslSocket::Tcp(Box::new(TCPConnection { - socket: tcp, - tls_connection: tls, - buffer: vec![], - buffer_size: bufsz.unwrap_or(0) as usize, - buffer_pos: 0, - }))); + let fd = self.add(NaslSocket::Tcp(Box::new(tcp))); fds.push(fd); } @@ -704,75 +490,7 @@ impl NaslSockets { )) } - fn open_tcp_stream( - addr: IpAddr, - port: u16, - timeout: Duration, - retry: u8, - ) -> Result { - for _ in 0..retry { - match Self::open_tcp(addr, port, timeout) { - Ok(socket) => return Ok(socket), - Err(err) => { - if !matches!(err, FunctionErrorKind::IOError(io::ErrorKind::TimedOut)) { - return Err(err); - } - } - } - } - // TODO: - // 1. Close port, when max attempts is reached - // 2. Log too many timeouts - // 3. Create result of type error with: - // ERRMSG|||||||||/tcp||| ||| Too many timeouts. The port was set to closed - Err(FunctionErrorKind::IOError(io::ErrorKind::TimedOut)) - } - - fn open_tcp( - addr: IpAddr, - port: u16, - timeout: Duration, - ) -> Result { - // Resolve Address and Port to SocketAddr - let sock_addr = SocketAddr::new(addr, port); - - let socket = TcpStream::connect_timeout(&sock_addr, timeout)?; - - // Unwrap, because it cannot fail - socket - .set_read_timeout(Some(Duration::from_secs(20))) - .unwrap(); - socket - .set_write_timeout(Some(Duration::from_secs(20))) - .unwrap(); - - Ok(socket) - } - - fn load_private_key(filename: &str) -> Result, FunctionErrorKind> { - let keyfile = fs::File::open(filename)?; - let mut reader = BufReader::new(keyfile); - - loop { - match rustls_pemfile::read_one(&mut reader)? { - Some(rustls_pemfile::Item::Pkcs1Key(key)) => return Ok(key.into()), - Some(rustls_pemfile::Item::Pkcs8Key(key)) => return Ok(key.into()), - Some(rustls_pemfile::Item::Sec1Key(key)) => return Ok(key.into()), - None => break, - _ => {} - } - } - - Err(FunctionErrorKind::Diagnostic( - format!( - "no keys found in {:?} (encrypted keys not supported)", - filename - ), - None, - )) - } - - fn get_tls_conf(context: &Context) -> Result { + fn get_tls_conf(context: &Context) -> Result { let cert_path = match get_kb_item(context, "Secret/tls_cert")? { Some(x) => Ok(x.to_string()), None => Err(FunctionErrorKind::Diagnostic( @@ -805,7 +523,7 @@ impl NaslSockets { )), }?; - Ok(TLSConfig { + Ok(TlsConfig { cert_path, key_path, password, @@ -813,76 +531,13 @@ impl NaslSockets { }) } - fn create_tls_client( - hostname: &str, - conf: TLSConfig, - ) -> Result { - // TODO: From vhost name - let server = ServerName::try_from(hostname.to_owned()).map_err(|_| { - FunctionErrorKind::Dirty(format!("Given vHost Name {hostname} is not valid")) - })?; - - let mut root_store = RootCertStore::empty(); - let ca_file = fs::File::open(conf.cafile_path)?; - let mut reader = BufReader::new(ca_file); - root_store.add_parsable_certificates( - rustls_pemfile::certs(&mut reader).map(|result| result.unwrap()), - ); - - let cert_file = fs::File::open(conf.cert_path)?; - let mut reader = BufReader::new(cert_file); - let cert = rustls_pemfile::certs(&mut reader) - .map(|result| result.unwrap()) - .collect(); - - let mut key = Self::load_private_key(&conf.key_path)?; - - if !conf.password.is_empty() { - let encrypted_key = pkcs8::EncryptedPrivateKeyInfo::from_der(key.secret_der()) - .map_err(|_| { - FunctionErrorKind::Diagnostic( - format!( - "Unable to decrypt private key {} with given password", - conf.key_path - ), - None, - ) - })?; - let decrypted_key = encrypted_key.decrypt(conf.password).map_err(|_| { - FunctionErrorKind::Diagnostic( - format!( - "Unable to decrypt private key {} with given password", - conf.key_path - ), - None, - ) - })?; - - key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from( - decrypted_key.as_bytes().to_owned(), - )); - } - let config = ClientConfig::builder() - .with_root_certificates(root_store) - .with_client_auth_cert(cert, key) - .map_err(|_| FunctionErrorKind::WrongArgument("Invalid Key".to_string()))?; - - ClientConnection::new(Arc::new(config), server).map_err(|e| { - FunctionErrorKind::Diagnostic(format!("Unable to establish TLS connection: {e}"), None) - }) - } - /// Open a UDP socket to the target host #[nasl_function] - fn open_sock_udp(&self, port: i64, context: &Context) -> Result { + fn open_sock_udp(&self, context: &Context, port: i64) -> Result { let port = verify_port(port)?; let addr = ipstr2ipaddr(context.target())?; - let socket = Self::open_udp(addr, port)?; - let socket = NaslSocket::Udp(UDPConnection { - socket, - buffer: vec![], - }); + let socket = NaslSocket::Udp(UdpConnection::new(addr, port)?); let fd = self.add(socket); Ok(NaslValue::Number(fd as i64)) @@ -899,8 +554,8 @@ impl NaslSockets { "the given socket FD does not exist".to_string(), ))?; let port = match socket { - NaslSocket::Tcp(conn) => conn.socket.local_addr()?.port(), - NaslSocket::Udp(conn) => conn.socket.local_addr()?.port(), + NaslSocket::Tcp(conn) => conn.local_addr()?.port(), + NaslSocket::Udp(conn) => conn.local_addr()?.port(), NaslSocket::Close => { return Err(FunctionErrorKind::WrongArgument( "the given socket FD is already closed".to_string(), @@ -909,6 +564,84 @@ impl NaslSockets { }; Ok(NaslValue::Number(port as i64)) } + + pub fn check_ftp_response( + mut conn: impl BufRead, + expected_code: &[usize], + ) -> Result { + let mut line = String::with_capacity(5); + conn.read_line(&mut line)?; + + if line.len() < 5 { + return Err(FunctionErrorKind::Diagnostic( + "could not read reply code".to_owned(), + None, + )); + } + + let code: usize = line[0..3].parse().map_err(|err| { + FunctionErrorKind::Diagnostic(format!("could not parse reply code: {}", err), None) + })?; + + // multiple line reply + // loop while the line does not begin with the code and a space + let expected = format!("{} ", &line[0..3]); + while line.len() < 5 || line[0..4] != expected { + line.clear(); + conn.read_line(&mut line)?; + } + + line = String::from(line.trim()); + + if expected_code.iter().any(|ec| code == *ec) { + Ok(code) + } else { + Err(FunctionErrorKind::Diagnostic( + format!("Expected code {:?}, got response: {}", expected_code, line), + None, + )) + } + } + + /// *any* **ftp_log_in**(user: *string*, pass: *string*, socket: *int*); + + /// **ftp_log_in** takes three named arguments: + /// - user: is the user name (it has no default value like “anonymous” or “ftp”) + /// - pass: is the password (again, no default value like the user e-mail address) + /// - socket: an open socket. + #[nasl_function(named(user, pass, socket))] + fn ftp_log_in(&self, user: &str, pass: &str, socket: usize) -> Result { + match self + .handles + .write() + .unwrap() + .handles + .get_mut(socket) + .ok_or(FunctionErrorKind::WrongArgument(format!( + "the given socket FD {socket} does not exist" + )))? { + NaslSocket::Tcp(conn) => { + Self::check_ftp_response(&mut *conn, &[220])?; + let data = format!("USER {}\r\n", user); + conn.write_all(data.as_bytes())?; + + let code = Self::check_ftp_response(&mut *conn, &[230, 331])?; + if code == 331 { + let data = format!("PASS {}\r\n", pass); + conn.write_all(data.as_bytes())?; + Self::check_ftp_response(&mut *conn, &[230])?; + } + Ok(true) + } + NaslSocket::Udp(_) => Err(FunctionErrorKind::Diagnostic( + "This function is only available for TCP connections".to_string(), + None, + )), + NaslSocket::Close => Err(FunctionErrorKind::WrongArgument( + "the given socket FD is already closed".to_string(), + )), + } + } } function_set! { @@ -923,5 +656,6 @@ function_set! { (NaslSockets::recv, "recv"), (NaslSockets::recv_line, "recv_line"), (NaslSockets::get_source_port, "get_source_port"), + (NaslSockets::ftp_log_in, "ftp_log_in"), ) } diff --git a/rust/src/nasl/builtin/network/tcp.rs b/rust/src/nasl/builtin/network/tcp.rs new file mode 100644 index 000000000..42002a9a3 --- /dev/null +++ b/rust/src/nasl/builtin/network/tcp.rs @@ -0,0 +1,154 @@ +use std::{ + io::{self, BufRead, BufReader, Read, Write}, + net::{IpAddr, SocketAddr, TcpStream}, + os::fd::AsRawFd, + time::Duration, +}; + +use rustls::{ClientConnection, Stream}; + +struct TcpDataStream { + tcp: TcpStream, + tls: Option, +} + +impl Read for TcpDataStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + if let Some(tls) = &mut self.tls { + let mut stream = Stream::new(tls, &mut self.tcp); + stream.read(buf) + } else { + self.tcp.read(buf) + } + } +} + +pub struct TcpConnection { + stream: BufReader, + flags: Option, +} + +impl Read for TcpConnection { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.stream.get_mut().read(buf) + } +} + +impl BufRead for TcpConnection { + fn fill_buf(&mut self) -> std::io::Result<&[u8]> { + self.stream.fill_buf() + } + + fn consume(&mut self, amt: usize) { + self.stream.consume(amt) + } +} + +impl Write for TcpConnection { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let stream = self.stream.get_mut(); + let ret = if let Some(tls) = &mut stream.tls { + let mut stream = Stream::new(tls, &mut stream.tcp); + stream.write(buf) + } else { + let n = unsafe { + libc::send( + stream.tcp.as_raw_fd(), + buf.as_ptr() as *const libc::c_void, + buf.len(), + self.flags.unwrap_or_default(), + ) + }; + self.flags = None; + if n < 0 { + return Err(io::Error::last_os_error()); + } + Ok(n as usize) + }; + + self.flags = None; + ret + } + + fn flush(&mut self) -> std::io::Result<()> { + let stream = self.stream.get_mut(); + if let Some(tls) = &mut stream.tls { + let mut stream = Stream::new(tls, &mut stream.tcp); + stream.flush() + } else { + stream.tcp.flush() + } + } +} + +impl TcpConnection { + fn new(stream: TcpDataStream, bufsz: Option) -> Self { + if let Some(bufsz) = bufsz { + Self { + stream: BufReader::with_capacity(bufsz, stream), + flags: None, + } + } else { + Self { + stream: BufReader::new(stream), + flags: None, + } + } + } + + pub fn is_tls(&self) -> bool { + self.stream.get_ref().tls.is_some() + } + + pub fn set_flags(&mut self, flags: i32) { + self.flags = Some(flags); + } + + pub fn connect( + addr: IpAddr, + port: u16, + tls: Option, + timeout: Duration, + bufsz: Option, + retry: u8, + ) -> io::Result { + let mut i = 0; + let tcp = loop { + match TcpStream::connect_timeout(&(addr, port).into(), timeout) { + Ok(tcp) => break tcp, + Err(e) if e.kind() == io::ErrorKind::TimedOut => { + if i == retry - 1 { + return Err(e); + } + i += 1; + } + Err(e) => return Err(e), + } + }; + Ok(Self::new(TcpDataStream { tcp, tls }, bufsz)) + } + + pub fn local_addr(&self) -> io::Result { + self.stream.get_ref().tcp.local_addr() + } + + pub fn read_with_timeout(&mut self, buf: &mut [u8], timeout: Duration) -> io::Result { + let old = self.stream.get_ref().tcp.read_timeout()?; + self.stream.get_ref().tcp.set_read_timeout(Some(timeout))?; + let ret = self.read(buf); + self.stream.get_ref().tcp.set_read_timeout(old)?; + ret + } + + pub fn read_line_with_timeout( + &mut self, + buf: &mut String, + timeout: Duration, + ) -> io::Result { + let old = self.stream.get_ref().tcp.read_timeout()?; + self.stream.get_ref().tcp.set_read_timeout(Some(timeout))?; + let ret = self.stream.read_line(buf); + self.stream.get_ref().tcp.set_read_timeout(old)?; + ret + } +} diff --git a/rust/src/nasl/builtin/network/tls.rs b/rust/src/nasl/builtin/network/tls.rs new file mode 100644 index 000000000..4744898eb --- /dev/null +++ b/rust/src/nasl/builtin/network/tls.rs @@ -0,0 +1,105 @@ +use core::fmt; +use std::{ + fmt::{Display, Formatter}, + fs, + io::{self, BufReader}, + sync::Arc, +}; + +use pkcs8::der::Decode; +use rustls::{ + pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer, ServerName}, + ClientConfig, ClientConnection, RootCertStore, +}; + +pub enum TLSError { + Io(io::Error), + Rustls(rustls::Error), + KeyError(String), +} + +impl From for TLSError { + fn from(err: io::Error) -> Self { + TLSError::Io(err) + } +} + +impl From for TLSError { + fn from(err: rustls::Error) -> Self { + TLSError::Rustls(err) + } +} + +impl Display for TLSError { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + TLSError::Io(err) => write!(f, "{err}"), + TLSError::Rustls(err) => write!(f, "Rustls error: {}", err), + TLSError::KeyError(err) => write!(f, "Key error: {}", err), + } + } +} + +fn load_private_key(filename: &str) -> Result, TLSError> { + let keyfile = fs::File::open(filename)?; + let mut reader = BufReader::new(keyfile); + + loop { + match rustls_pemfile::read_one(&mut reader)? { + Some(rustls_pemfile::Item::Pkcs1Key(key)) => return Ok(key.into()), + Some(rustls_pemfile::Item::Pkcs8Key(key)) => return Ok(key.into()), + Some(rustls_pemfile::Item::Sec1Key(key)) => return Ok(key.into()), + None => break, + _ => {} + } + } + + Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("No private key found in {}", filename), + ))? +} + +pub fn create_tls_client( + hostname: &str, + cert_path: &str, + key_path: &str, + password: &str, + cafile_path: &str, +) -> Result { + let server = ServerName::try_from(hostname.to_owned()).unwrap(); + + let mut root_store = RootCertStore::empty(); + let ca_file = fs::File::open(cafile_path)?; + let mut reader = BufReader::new(ca_file); + root_store.add_parsable_certificates( + rustls_pemfile::certs(&mut reader).map(|result| result.unwrap()), + ); + let cert_file = fs::File::open(cert_path)?; + let mut reader = BufReader::new(cert_file); + let cert = rustls_pemfile::certs(&mut reader) + .map(|result| result.unwrap()) + .collect(); + + let mut key = load_private_key(key_path)?; + + if !password.is_empty() { + let encrypted_key = pkcs8::EncryptedPrivateKeyInfo::from_der(key.secret_der()) + .map_err(|e| TLSError::KeyError(format!("Failed to parse encrypted key: {}", e)))?; + let decrypted_key = encrypted_key.decrypt(password).map_err(|e| { + TLSError::KeyError(format!( + "Failed to decrypt key with provided password: {}", + e + )) + })?; + + key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from( + decrypted_key.as_bytes().to_owned(), + )); + } + let config = ClientConfig::builder() + .with_root_certificates(root_store) + .with_client_auth_cert(cert, key) + .unwrap(); + ClientConnection::new(Arc::new(config), server).map_err(|e| e.into()) +} diff --git a/rust/src/nasl/builtin/network/udp.rs b/rust/src/nasl/builtin/network/udp.rs new file mode 100644 index 000000000..4fd840899 --- /dev/null +++ b/rust/src/nasl/builtin/network/udp.rs @@ -0,0 +1,100 @@ +use std::{ + io::{self, Read, Write}, + net::{IpAddr, SocketAddr, UdpSocket}, + os::fd::AsRawFd, + time::Duration, +}; + +use super::{mtu, network_utils::bind_local_socket}; + +pub struct UdpConnection { + socket: UdpSocket, + buffer: Vec, + flags: Option, +} + +const NUM_TIMES_TO_RESEND: usize = 5; + +impl Read for UdpConnection { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + for i in 0..NUM_TIMES_TO_RESEND { + let result = self.socket.recv_from(buf); + match result { + Ok((size, origin)) => { + if self.socket.peer_addr()? == origin { + return Ok(size); + } + } + Err(e) if e.kind() == io::ErrorKind::TimedOut && i != NUM_TIMES_TO_RESEND - 1 => { + self.socket.send(&self.buffer)?; + } + Err(e) => return Err(e), + }; + } + unreachable!() + } +} + +impl Write for UdpConnection { + fn write(&mut self, buf: &[u8]) -> io::Result { + let mtu = mtu(self.socket.peer_addr()?.ip()); + if buf.len() < mtu { + let result = unsafe { + libc::send( + self.socket.as_raw_fd(), + buf.as_ptr() as *const libc::c_void, + buf.len(), + self.flags.unwrap_or_default(), + ) + }; + self.flags = None; + if result < 0 { + return Err(io::Error::last_os_error()); + } + Ok(result as usize) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + format!( + "UDP data of size {} exceeds the maximum length of {}", + buf.len(), + mtu + ), + )) + } + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +impl UdpConnection { + pub fn new(addr: IpAddr, port: u16) -> io::Result { + let sock_addr = SocketAddr::new(addr, port); + let socket = bind_local_socket(&sock_addr)?; + socket.connect(sock_addr)?; + socket.set_read_timeout(Some(Duration::from_secs(1)))?; + Ok(Self { + socket, + buffer: vec![], + flags: None, + }) + } + + pub fn set_flags(&mut self, flags: i32) { + self.flags = Some(flags); + } + + pub fn local_addr(&self) -> io::Result { + self.socket.local_addr() + } + + pub fn read_with_timeout(&mut self, buf: &mut [u8], timeout: Duration) -> io::Result { + let old = self.socket.read_timeout()?; + self.socket.set_read_timeout(Some(timeout))?; + let ret = self.read(buf); + self.socket.set_read_timeout(old)?; + ret + } +}