commit e80d4caec9e9fedd0081f925b9f839352ed7dc79 Author: DarkCat09 Date: Fri Oct 18 18:40:04 2024 +0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4834252 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,431 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "proc-macro2" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_test" +version = "1.0.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed" +dependencies = [ + "serde", +] + +[[package]] +name = "smp-socks" +version = "0.1.0" +dependencies = [ + "serde", + "serde_test", + "socks5-server", + "thiserror", + "tokio", + "toml", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "socks5-proto" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d91431c4672e25e372ef46bc554be8f315068c03608f99267a71ad32a12e8c4" +dependencies = [ + "bytes", + "thiserror", + "tokio", +] + +[[package]] +name = "socks5-server" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5223c26981806584cc38c74fddf58808dbdcf4724890471ced69e7a2e8d86345" +dependencies = [ + "async-trait", + "bytes", + "socks5-proto", + "tokio", +] + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1824767 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "smp-socks" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1.0.210", features = ["derive"] } +socks5-server = "0.10.1" +thiserror = "1.0.64" +tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread", "io-util"] } +toml = { version = "0.8.19", default-features = false, features = ["parse"] } + +[dev-dependencies] +serde_test = "1.0.177" diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..aa831a9 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,31 @@ +use std::{collections::HashSet, io::Read, net::Ipv4Addr}; + +use crate::{error::AppResult, serde_addr::TargetAddr}; + +#[derive(serde::Deserialize)] +pub struct Config { + pub host: String, + pub port: u16, + pub local: HashSet, + pub remote: HashSet, +} + +pub fn parse() -> AppResult { + if let Some(mut file) = std::env::var_os("CONFIG") + .and_then(|path| std::fs::File::open(path).ok()) + .or_else(|| std::fs::File::open("config.toml").ok()) + { + let mut buf = String::new(); + file.read_to_string(&mut buf)?; + Ok(toml::from_str(&buf)?) + } else { + let mut local = HashSet::new(); + local.insert((Ipv4Addr::new(127, 0, 0, 1).into(), 5232).into()); + Ok(Config { + host: "0.0.0.0".to_owned(), + port: 5233, + local, + remote: HashSet::new(), + }) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..a175f45 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,79 @@ +use socks5_server::proto::Error as SocksProtoError; +use tokio::net::TcpStream; + +pub type AppResult = core::result::Result; + +#[derive(Debug)] +pub enum AppError { + IoError(std::io::Error), + InvalidConfig(toml::de::Error), + SocksError(SocksProtoError), + DataNotUtf8(std::string::FromUtf8Error), + ThirdPartyHost, +} + +impl From for AppError { + #[inline] + fn from(value: std::io::Error) -> Self { + Self::IoError(value) + } +} + +impl From for AppError { + #[inline] + fn from(value: toml::de::Error) -> Self { + Self::InvalidConfig(value) + } +} + +impl From for AppError { + #[inline] + fn from(value: SocksProtoError) -> Self { + Self::SocksError(value) + } +} + +impl From for AppError { + #[inline] + fn from(value: std::string::FromUtf8Error) -> Self { + Self::DataNotUtf8(value) + } +} + +pub type HandlerResult = core::result::Result; + +#[derive(Debug)] +pub struct HandlerError { + inner: AppError, + stream: TcpStream, +} + +impl From<(AppError, TcpStream)> for HandlerError { + #[inline] + fn from(value: (AppError, TcpStream)) -> Self { + Self { + inner: value.0, + stream: value.1, + } + } +} + +impl From<(SocksProtoError, TcpStream)> for HandlerError { + #[inline] + fn from(value: (SocksProtoError, TcpStream)) -> Self { + Self { + inner: AppError::SocksError(value.0), + stream: value.1, + } + } +} + +impl From<(std::io::Error, TcpStream)> for HandlerError { + #[inline] + fn from(value: (std::io::Error, TcpStream)) -> Self { + Self { + inner: AppError::IoError(value.0), + stream: value.1, + } + } +} diff --git a/src/handler.rs b/src/handler.rs new file mode 100644 index 0000000..b6dd21f --- /dev/null +++ b/src/handler.rs @@ -0,0 +1,58 @@ +use std::sync::Arc; + +use crate::{ + config::Config, + error::{AppError, HandlerError, HandlerResult}, +}; + +use socks5_server::{ + connection::state::NeedAuthenticate, + proto::{Address, Reply}, + Command, IncomingConnection, +}; +use tokio::net::TcpStream; + +pub async fn handler( + conn: IncomingConnection<(), NeedAuthenticate>, + config: Arc, +) -> HandlerResult<()> { + let conn = conn.authenticate().await?.0; + + match conn.wait().await? { + Command::Associate(cmd, _addr) => { + let _ = cmd + .reply(Reply::CommandNotSupported, Address::unspecified()) + .await? + .close() + .await; + } + + Command::Bind(cmd, _addr) => { + let _ = cmd + .reply(Reply::CommandNotSupported, Address::unspecified()) + .await? + .close() + .await; + } + + Command::Connect(cmd, addr) => { + let target = match addr { + Address::DomainAddress(host, port) => { + let Ok(host) = String::from_utf8(host) else { + let conn = cmd + .reply(Reply::GeneralFailure, Address::unspecified()) + .await? + .into_inner(); + return Err( + (std::io::Error::from(std::io::ErrorKind::InvalidData), conn).into(), + ); + }; + TcpStream::connect((host.as_ref(), port)).await + } + Address::SocketAddress(addr) => TcpStream::connect(addr).await, + }; + } + } + + Ok(()) +} diff --git a/src/host.rs b/src/host.rs new file mode 100644 index 0000000..28c5d4d --- /dev/null +++ b/src/host.rs @@ -0,0 +1,9 @@ +use std::{net::SocketAddr, sync::Arc}; + +use crate::{config::Config, error::AppResult}; + +pub fn parse_str(value: Vec, config: Arc) -> AppResult { + let host = String::from_utf8(value)?; + + // +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c0922d0 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,36 @@ +mod config; +mod error; +mod handler; +// mod host; +mod serde_addr; + +use std::sync::Arc; + +use error::AppResult; +use handler::handler; + +use socks5_server as socks; +use tokio::net::TcpListener; + +#[tokio::main] +async fn main() -> AppResult<()> { + let config = Arc::new(config::parse()?); + let auth = Arc::new(socks::auth::NoAuth); + + let listener = TcpListener::bind((config.host.as_str(), config.port)).await?; + let server = socks::Server::new(listener, auth); + + while let Ok((conn, _addr)) = server.accept().await { + let config = config.clone(); + tokio::spawn(async move { + match handler(conn, config).await { + Ok(()) => {} + Err(e) => { + // + } + } + }); + } + + Ok(()) +} diff --git a/src/serde_addr.rs b/src/serde_addr.rs new file mode 100644 index 0000000..496e9b2 --- /dev/null +++ b/src/serde_addr.rs @@ -0,0 +1,186 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs}; + +use serde::{de::Visitor, Deserialize}; + +pub const DEFAULT_PORT: u16 = 5232; + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum TargetAddr { + DomainName(String, u16), + IpAddress(SocketAddr), +} + +impl<'de> Deserialize<'de> for TargetAddr { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(AddrVisitor) + } +} + +impl ToSocketAddrs for TargetAddr { + type Iter = AddrIter; + + fn to_socket_addrs(&self) -> std::io::Result { + Ok(match self { + Self::DomainName(host, port) => (host.as_str(), *port).to_socket_addrs()?.into(), + Self::IpAddress(sockaddr) => sockaddr.clone().into(), + }) + } +} + +impl From<(IpAddr, u16)> for TargetAddr { + #[inline] + fn from((addr, port): (IpAddr, u16)) -> Self { + Self::IpAddress(SocketAddr::new(addr, port)) + } +} + +pub struct AddrVisitor; + +impl<'de> Visitor<'de> for AddrVisitor { + type Value = TargetAddr; + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + let v = v.trim(); + + let ipv6 = v.starts_with('['); + let mut port_idx = v.len(); + + let port = if ipv6 { + v.rfind("]:").and_then(|pos| { + port_idx = pos; + v.get(pos + 2..) + }) + } else { + v.rfind(':').and_then(|pos| { + port_idx = pos; + v.get(pos + 1..) + }) + } + .and_then(|port| { + port.parse::() + .map_err(|_| { + port_idx = v.len(); + }) + .ok() + }) + .unwrap_or(DEFAULT_PORT); + + let host = if ipv6 { + v.get(1..port_idx) + } else { + v.get(..port_idx) + } + .ok_or(serde::de::Error::missing_field("host"))?; + + if ipv6 { + let addr = IpAddr::V6(host.parse().map_err(serde::de::Error::custom)?); + Ok((addr, port).into()) + } else { + if let Ok(addr) = host.parse::() { + Ok((IpAddr::V4(addr), port).into()) + } else if let Ok(addr) = host.parse::() { + Ok((IpAddr::V6(addr), port).into()) + } else { + Ok(TargetAddr::DomainName(host.to_owned(), port)) + } + } + } + + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + concat!( + "host:port ", + "where host is a domain or an ipv4/ipv6 addr, ", + "and optional port is u16", + ), + ) + } +} + +pub enum AddrIter { + HostResolved(std::vec::IntoIter), + OneAddress(std::iter::Once), +} + +impl Iterator for AddrIter { + type Item = SocketAddr; + + #[inline] + fn next(&mut self) -> Option { + match self { + Self::HostResolved(iter) => iter.next(), + Self::OneAddress(iter) => iter.next(), + } + } +} + +impl From> for AddrIter { + #[inline] + fn from(value: std::vec::IntoIter) -> Self { + Self::HostResolved(value) + } +} + +impl From for AddrIter { + #[inline] + fn from(value: SocketAddr) -> Self { + Self::OneAddress(std::iter::once(value)) + } +} + +#[cfg(test)] +mod tests { + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + + use serde_test::{assert_de_tokens, Token}; + + use super::{TargetAddr, DEFAULT_PORT}; + + const LOCALHOST_V4: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + const TEST_V6: IpAddr = IpAddr::V6(Ipv6Addr::new( + 0xfe80, 0x0, 0x0, 0x0, 0x721e, 0xd21f, 0x29a3, 0xf396, + )); + + #[test] + pub fn ipv4_without_port() { + let exp: TargetAddr = (LOCALHOST_V4, DEFAULT_PORT).into(); + assert_de_tokens(&exp, &[Token::Str("127.0.0.1")]); + } + + #[test] + pub fn ipv4_with_port() { + let exp: TargetAddr = (LOCALHOST_V4, 443).into(); + assert_de_tokens(&exp, &[Token::Str("127.0.0.1:443")]); + } + + #[test] + pub fn ipv6_without_port() { + let exp: TargetAddr = (TEST_V6, DEFAULT_PORT).into(); + assert_de_tokens(&exp, &[Token::Str("fe80::721e:d21f:29a3:f396")]); + } + + #[test] + pub fn ipv6_with_port() { + let exp: TargetAddr = (TEST_V6, 443).into(); + assert_de_tokens(&exp, &[Token::Str("[fe80::721e:d21f:29a3:f396]:443")]); + } + + #[test] + pub fn domain_without_port() { + let exp = TargetAddr::DomainName("smp.dc09.ru".to_owned(), DEFAULT_PORT); + assert_de_tokens(&exp, &[Token::Str("smp.dc09.ru")]); + } + + #[test] + pub fn domain_with_port() { + let exp = TargetAddr::DomainName("xftp.dc09.ru".to_owned(), 443); + assert_de_tokens(&exp, &[Token::Str("xftp.dc09.ru:443")]); + } +}