diff --git a/bench/bench.sh b/bench/bench.sh index 38d2951..f355c80 100644 --- a/bench/bench.sh +++ b/bench/bench.sh @@ -1,29 +1,29 @@ #!/bin/sh -# echo "----------------------------" -# echo "Benchmark on rpxy" -# ab -c 16 -n 10000 http://127.0.0.1:8080/index.html - -# echo "----------------------------" -# echo "Benchmark on nginx" -# ab -c 16 -n 10000 http://127.0.0.1:8090/index.html - -# echo "----------------------------" -# echo "Benchmark on caddy" -# ab -c 16 -n 10000 http://127.0.0.1:8100/index.html - - echo "----------------------------" echo "Benchmark on rpxy" -#wrk -t8 -c100 -d30s http://127.0.0.1:8080/index.html -rewrk -c 256 -t 8 -d 15s -h http://127.0.0.1:8080 --pct +ab -c 100 -n 10000 http://127.0.0.1:8080/index.html echo "----------------------------" echo "Benchmark on nginx" -# wrk -t8 -c100 -d30s http://127.0.0.1:8090/index.html -rewrk -c 256 -t 8 -d 15s -h http://127.0.0.1:8090 --pct +ab -c 100 -n 10000 http://127.0.0.1:8090/index.html echo "----------------------------" echo "Benchmark on caddy" -# wrk -t8 -c100 -d30s http://127.0.0.1:8100/index.html -rewrk -c 256 -t 8 -d 15s -h http://127.0.0.1:8100 --pct +ab -c 100 -n 10000 http://127.0.0.1:8100/index.html + + +# echo "----------------------------" +# echo "Benchmark on rpxy" +# #wrk -t8 -c100 -d30s http://127.0.0.1:8080/index.html +# rewrk -c 256 -t 4 -d 15s -h http://127.0.0.1:8080 --pct + +# echo "----------------------------" +# echo "Benchmark on nginx" +# # wrk -t8 -c100 -d30s http://127.0.0.1:8090/index.html +# rewrk -c 256 -t 4 -d 15s -h http://127.0.0.1:8090 --pct + +# echo "----------------------------" +# echo "Benchmark on caddy" +# # wrk -t8 -c100 -d30s http://127.0.0.1:8100/index.html +# rewrk -c 256 -t 4 -d 15s -h http://127.0.0.1:8100 --pct diff --git a/bench/docker-compose.yml b/bench/docker-compose.yml index 83067f9..cf3d4c3 100644 --- a/bench/docker-compose.yml +++ b/bench/docker-compose.yml @@ -25,10 +25,12 @@ services: build: context: ../ restart: unless-stopped + environment: + - LOG_LEVEL=debug + - LOG_TO_FILE=false ports: - 127.0.0.1:8080:8080 tty: false - privileged: true volumes: - ./rpxy.toml:/etc/rpxy.toml:ro networks: diff --git a/docker-bin/entrypoint.sh b/docker-bin/entrypoint.sh index e0ceed5..83fdf5e 100644 --- a/docker-bin/entrypoint.sh +++ b/docker-bin/entrypoint.sh @@ -1,4 +1,14 @@ #!/usr/bin/env bash LOG_FILE=/var/log/rpxy/rpxy.log -/run.sh 2>&1 | tee $LOG_FILE +if [ -z ${LOG_TO_FILE} ]; then + LOG_TO_FILE=false +fi + +if "${LOG_TO_FILE}"; then + echo "rpxy: Start with writing log file" + /run.sh 2>&1 | tee $LOG_FILE +else + echo "rpxy: Start without writing log file" + /run.sh 2>&1 +fi diff --git a/docker-bin/run.sh b/docker-bin/run.sh index b02efcb..1b0801e 100644 --- a/docker-bin/run.sh +++ b/docker-bin/run.sh @@ -52,12 +52,9 @@ cp -p /etc/cron.daily/logrotate /etc/cron.hourly/ service cron start # debug level logging +if [ -z $LOG_LEVEL ]; then LOG_LEVEL=info - if [ ${DEBUG} ]; then - echo "Logging in debug mode" - LOG_LEVEL=debug - fi - -echo "Start rpxy" +fi +echo "rpxy: Logging with level ${LOG_LEVEL}" RUST_LOG=${LOG_LEVEL} /opt/rpxy/sbin/rpxy --config ${CONFIG_FILE} diff --git a/docker-compose.yml b/docker-compose.yml index 422c9d7..4d90965 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,8 @@ services: build: context: ./ environment: - - DEBUG=true + - LOG_LEVEL=info + - LOG_TO_FILE tty: false privileged: true volumes: diff --git a/src/main.rs b/src/main.rs index cc034f4..babea2b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ mod error; mod globals; mod log; mod proxy; +mod utils; use crate::{ config::parse_opts, diff --git a/src/proxy/mod.rs b/src/proxy/mod.rs index 5acb8f1..b466a6c 100644 --- a/src/proxy/mod.rs +++ b/src/proxy/mod.rs @@ -5,6 +5,9 @@ mod proxy_h3; mod proxy_handler; mod proxy_main; mod proxy_tls; +mod utils_headers; +mod utils_request; +mod utils_synth_response; pub use backend::*; pub use backend_opt::UpstreamOption; diff --git a/src/proxy/proxy_handler.rs b/src/proxy/proxy_handler.rs index d77c6a5..02b0bf9 100644 --- a/src/proxy/proxy_handler.rs +++ b/src/proxy/proxy_handler.rs @@ -1,27 +1,15 @@ // Highly motivated by https://github.com/felipenoris/hyper-reverse-proxy -use super::{Proxy, Upstream, UpstreamOption}; +use super::{utils_headers::*, utils_request::*, utils_synth_response::*, Proxy, Upstream}; use crate::{constants::*, error::*, log::*}; use hyper::{ client::connect::Connect, - header::{HeaderMap, HeaderValue}, + header::{self, HeaderValue}, http::uri::Scheme, Body, Request, Response, StatusCode, Uri, Version, }; use std::net::SocketAddr; use tokio::io::copy_bidirectional; -const HOP_HEADERS: &[&str] = &[ - "connection", - "te", - "trailer", - "keep-alive", - "proxy-connection", - "proxy-authenticate", - "proxy-authorization", - "transfer-encoding", - "upgrade", -]; - impl Proxy where T: Connect + Clone + Sync + Send + 'static, @@ -31,42 +19,34 @@ where mut req: Request, client_addr: SocketAddr, // アクセス制御用 ) -> Result> { - info!( - "Handling {:?} request from {}: {} {:?} {} {:?}", - req.version(), - client_addr, - req.method(), - req.headers().get("host").map_or_else( - || req.uri().host().unwrap_or(""), - |h| h.to_str().unwrap() - ), - req.uri(), - req - .headers() - .get("user-agent") - .map_or_else(|| "", |ua| ua.to_str().unwrap()) - ); + let request_log = log_request_msg(&req, client_addr); + // Here we start to handle with server_name // Find backend application for given server_name - let (server_name, _port) = if let Ok(v) = parse_host_port(&req, self.tls_enabled) { + let (server_name, _port) = if let Ok(v) = parse_host_port(&req) { v } else { - return http_error(StatusCode::SERVICE_UNAVAILABLE); - }; - let backend = if let Some(be) = self.backends.apps.get(server_name.as_str()) { - be - } else if let Some(default_be) = &self.backends.default_app { - debug!("Serving by default app: {}", default_be); - self.backends.apps.get(default_be).unwrap() - } else { - return http_error(StatusCode::SERVICE_UNAVAILABLE); + info!("{} => {}", request_log, StatusCode::BAD_REQUEST); + return http_error(StatusCode::BAD_REQUEST); }; - // Redirect to https if tls_enabled is false and redirect_to_https is true - let path_and_query = req.uri().path_and_query().unwrap().as_str().to_owned(); + if !self.backends.apps.contains_key(&server_name) && self.backends.default_app.is_none() { + info!("{} => {}", request_log, StatusCode::SERVICE_UNAVAILABLE); + return http_error(StatusCode::SERVICE_UNAVAILABLE); + } + let backend = if let Some(be) = self.backends.apps.get(&server_name) { + be + } else { + let default_be = self.backends.default_app.as_ref().unwrap(); + debug!("Serving by default app: {}", default_be); + self.backends.apps.get(default_be).unwrap() + }; + + // Redirect to https if !tls_enabled and redirect_to_https is true if !self.tls_enabled && backend.https_redirection.unwrap_or(false) { debug!("Redirect to secure connection: {}", server_name); - return secure_redirection(&server_name, self.globals.https_port, &path_and_query); + info!("{} => {}", request_log, StatusCode::PERMANENT_REDIRECT); + return secure_redirection(&server_name, self.globals.https_port, &req); } // Find reverse proxy for given path and choose one of upstream host @@ -94,7 +74,6 @@ where client_addr, req, upstream_scheme_host, - path_and_query, &upgrade_in_request, upstream, ) { @@ -117,19 +96,19 @@ where { if self.globals.http3 { if let Some(port) = self.globals.https_port { - res_backend.headers_mut().insert( - hyper::header::ALT_SVC, - format!( - "h3=\":{}\"; ma={}, h3-29=\":{}\"; ma={}", - port, H3_ALT_SVC_MAX_AGE, port, H3_ALT_SVC_MAX_AGE - ) - .parse() - .unwrap(), - ); + let alt_svc_value = HeaderValue::from_str(&format!( + "h3=\":{}\"; ma={}, h3-29=\":{}\"; ma={}", + port, H3_ALT_SVC_MAX_AGE, port, H3_ALT_SVC_MAX_AGE + )) + .unwrap(); + res_backend + .headers_mut() + .insert(header::ALT_SVC, alt_svc_value); } } } debug!("Response from backend: {:?}", res_backend.status()); + let response_log = res_backend.status().to_string(); if res_backend.status() == StatusCode::SWITCHING_PROTOCOLS { // Handle StatusCode::SWITCHING_PROTOCOLS in response @@ -153,29 +132,37 @@ where .map_err(|e| anyhow!("Coping between upgraded connections failed: {}", e))?; // TODO: any response code? Ok(()) as Result<()> }); + info!("{} => {}", request_log, response_log); Ok(res_backend) } else { error!("Request does not have an upgrade extension"); - http_error(StatusCode::BAD_GATEWAY) + info!("{} => {}", request_log, StatusCode::BAD_REQUEST); + http_error(StatusCode::BAD_REQUEST) } } else { error!( "Backend tried to switch to protocol {:?} when {:?} was requested", upgrade_in_response, upgrade_in_request ); - http_error(StatusCode::BAD_GATEWAY) + info!("{} => {}", request_log, StatusCode::SERVICE_UNAVAILABLE); + http_error(StatusCode::SERVICE_UNAVAILABLE) } } else { // Generate response to client if generate_response_forwarded(&mut res_backend).is_ok() { + info!("{} => {}", request_log, response_log); Ok(res_backend) } else { + info!("{} => {}", request_log, StatusCode::BAD_GATEWAY); http_error(StatusCode::BAD_GATEWAY) } } } } +//////////////////////////////////////////////////// +// Functions to generate messages + fn generate_response_forwarded(response: &mut Response) -> Result<()> { let headers = response.headers_mut(); remove_hop_header(headers); @@ -193,7 +180,6 @@ fn generate_request_forwarded( client_addr: SocketAddr, mut req: Request, upstream_scheme_host: &Uri, - path_and_query: String, upgrade: &Option, upstream: &Upstream, ) -> Result> { @@ -201,11 +187,8 @@ fn generate_request_forwarded( // Add te: trailer if contained in original request let te_trailers = { - if let Some(te) = req.headers().get("te") { - te.to_str() - .unwrap() - .split(',') - .any(|x| x.trim() == "trailers") + if let Some(te) = req.headers().get(header::TE) { + te.to_str()?.split(',').any(|x| x.trim() == "trailers") } else { false } @@ -222,16 +205,15 @@ fn generate_request_forwarded( // Add te: trailer if te_trailer if te_trailers { - headers.insert("te", "trailer".parse().unwrap()); + headers.insert(header::TE, "trailer".parse()?); } // add "host" header of original server_name if not exist (default) - if req.headers().get(hyper::header::HOST).is_none() { + if req.headers().get(header::HOST).is_none() { let org_host = req.uri().host().unwrap_or("none").to_owned(); - req.headers_mut().insert( - hyper::header::HOST, - HeaderValue::from_str(org_host.as_str()).unwrap(), - ); + req + .headers_mut() + .insert(header::HOST, HeaderValue::from_str(org_host.as_str())?); }; // apply upstream-specific headers given in upstream_option @@ -239,18 +221,23 @@ fn generate_request_forwarded( apply_upstream_options_to_header(headers, client_addr, upstream_scheme_host, upstream)?; // update uri in request - *req.uri_mut() = Uri::builder() + ensure!(upstream_scheme_host.authority().is_some() && upstream_scheme_host.scheme().is_some()); + let new_uri = Uri::builder() .scheme(upstream_scheme_host.scheme().unwrap().as_str()) - .authority(upstream_scheme_host.authority().unwrap().as_str()) - .path_and_query(&path_and_query) - .build()?; + .authority(upstream_scheme_host.authority().unwrap().as_str()); + let pq = req.uri().path_and_query(); + *req.uri_mut() = match pq { + None => new_uri, + Some(x) => new_uri.path_and_query(x.to_owned()), + } + .build()?; // upgrade if let Some(v) = upgrade { - req.headers_mut().insert("upgrade", v.parse().unwrap()); + req.headers_mut().insert("upgrade", v.parse()?); req .headers_mut() - .insert(hyper::header::CONNECTION, HeaderValue::from_str("upgrade")?); + .insert(header::CONNECTION, HeaderValue::from_str("upgrade")?); } // Change version to http/1.1 when destination scheme is http @@ -263,155 +250,3 @@ fn generate_request_forwarded( Ok(req) } - -fn apply_upstream_options_to_header( - headers: &mut HeaderMap, - _client_addr: SocketAddr, - upstream_scheme_host: &Uri, - upstream: &Upstream, -) -> Result<()> { - upstream.opts.iter().for_each(|opt| match opt { - UpstreamOption::OverrideHost => { - let upstream_host = upstream_scheme_host.host().unwrap(); - headers - .insert( - hyper::header::HOST, - HeaderValue::from_str(upstream_host).unwrap(), - ) - .unwrap(); - } - }); - Ok(()) -} - -fn append_header_entry(headers: &mut HeaderMap, key: &'static str, value: &str) -> Result<()> { - match headers.entry(key) { - hyper::header::Entry::Vacant(entry) => { - entry.insert(value.parse::()?); - } - hyper::header::Entry::Occupied(mut entry) => { - entry.append(value.parse::()?); - } - } - - Ok(()) -} - -fn add_forwarding_header(headers: &mut HeaderMap, client_addr: SocketAddr) -> Result<()> { - // default process - // optional process defined by upstream_option is applied in fn apply_upstream_options - append_header_entry(headers, "x-forwarded-for", &client_addr.ip().to_string())?; - - Ok(()) -} - -fn remove_connection_header(headers: &mut HeaderMap) { - if headers.get("connection").is_some() { - let v = headers.get("connection").cloned().unwrap(); - for m in v.to_str().unwrap().split(',') { - if !m.is_empty() { - headers.remove(m.trim()); - } - } - } -} - -fn remove_hop_header(headers: &mut HeaderMap) { - HOP_HEADERS.iter().for_each(|key| { - headers.remove(*key); - }); -} - -fn http_error(status_code: StatusCode) -> Result> { - let response = Response::builder() - .status(status_code) - .body(Body::empty()) - .unwrap(); - Ok(response) -} - -fn extract_upgrade(headers: &HeaderMap) -> Option { - if let Some(c) = headers.get("connection") { - if c - .to_str() - .unwrap_or("") - .split(',') - .into_iter() - .any(|w| w.trim().to_ascii_lowercase() == "upgrade") - { - if let Some(u) = headers.get("upgrade") { - let m = u.to_str().unwrap().to_string(); - debug!("Upgrade in request header: {}", m); - return Some(m); - } - } - } - None -} - -fn secure_redirection( - server_name: &str, - tls_port: Option, - path_and_query: &str, -) -> Result> { - let dest_uri: String = if let Some(tls_port) = tls_port { - if tls_port == 443 { - format!("https://{}{}", server_name, path_and_query) - } else { - format!("https://{}:{}{}", server_name, tls_port, path_and_query) - } - } else { - bail!("Internal error! TLS port is not set internally."); - }; - let response = Response::builder() - .status(StatusCode::MOVED_PERMANENTLY) - .header("Location", dest_uri) - .body(Body::empty()) - .unwrap(); - Ok(response) -} - -fn parse_host_port( - req: &Request, - tls_enabled: bool, -) -> Result<(String, u16)> { - let host_port_headers = req.headers().get("host"); - let host_uri = req.uri().host(); - let port_uri = req.uri().port_u16(); - - if host_port_headers.is_none() && host_uri.is_none() { - bail!("No host in request header"); - } - - let (host, port) = match (host_uri, host_port_headers) { - (Some(x), _) => { - let port = if let Some(p) = port_uri { - p - } else if tls_enabled { - 443 - } else { - 80 - }; - (x.to_string(), port) - } - (None, Some(x)) => { - let hp_as_uri = x.to_str().unwrap().parse::().unwrap(); - let host = hp_as_uri - .host() - .ok_or_else(|| anyhow!("Failed to parse host"))?; - let port = if let Some(p) = hp_as_uri.port() { - p.as_u16() - } else if tls_enabled { - 443 - } else { - 80 - }; - (host.to_string(), port) - } - (None, None) => { - bail!("Host unspecified in request") - } - }; - - Ok((host, port)) -} diff --git a/src/proxy/utils_headers.rs b/src/proxy/utils_headers.rs new file mode 100644 index 0000000..f751ca4 --- /dev/null +++ b/src/proxy/utils_headers.rs @@ -0,0 +1,112 @@ +use super::{Upstream, UpstreamOption}; +use crate::{error::*, log::*, utils::*}; +use hyper::{ + header::{self, HeaderMap, HeaderValue}, + Uri, +}; +use std::net::SocketAddr; + +//////////////////////////////////////////////////// +// Functions to manipulate headers + +pub(super) fn apply_upstream_options_to_header( + headers: &mut HeaderMap, + _client_addr: SocketAddr, + upstream_scheme_host: &Uri, + upstream: &Upstream, +) -> Result<()> { + for opt in upstream.opts.iter() { + match opt { + UpstreamOption::OverrideHost => { + let upstream_host = upstream_scheme_host.host().ok_or_else(|| anyhow!("none"))?; + headers + .insert(header::HOST, HeaderValue::from_str(upstream_host)?) + .ok_or_else(|| anyhow!("none"))?; + } + } + } + + Ok(()) +} + +pub(super) fn append_header_entry( + headers: &mut HeaderMap, + key: &'static str, + value: &str, +) -> Result<()> { + match headers.entry(key) { + header::Entry::Vacant(entry) => { + entry.insert(value.parse::()?); + } + header::Entry::Occupied(mut entry) => { + entry.append(value.parse::()?); + } + } + + Ok(()) +} + +pub(super) fn add_forwarding_header( + headers: &mut HeaderMap, + client_addr: SocketAddr, +) -> Result<()> { + // default process + // optional process defined by upstream_option is applied in fn apply_upstream_options + append_header_entry( + headers, + "x-forwarded-for", + &client_addr.to_canonical().ip().to_string(), + )?; + + Ok(()) +} + +pub(super) fn remove_connection_header(headers: &mut HeaderMap) { + if let Some(values) = headers.get(header::CONNECTION) { + if let Ok(v) = values.clone().to_str() { + for m in v.split(',') { + if !m.is_empty() { + headers.remove(m.trim()); + } + } + } + } +} + +const HOP_HEADERS: &[&str] = &[ + "connection", + "te", + "trailer", + "keep-alive", + "proxy-connection", + "proxy-authenticate", + "proxy-authorization", + "transfer-encoding", + "upgrade", +]; + +pub(super) fn remove_hop_header(headers: &mut HeaderMap) { + HOP_HEADERS.iter().for_each(|key| { + headers.remove(*key); + }); +} + +pub(super) fn extract_upgrade(headers: &HeaderMap) -> Option { + if let Some(c) = headers.get(header::CONNECTION) { + if c + .to_str() + .unwrap_or("") + .split(',') + .into_iter() + .any(|w| w.trim().to_ascii_lowercase() == header::UPGRADE.as_str().to_ascii_lowercase()) + { + if let Some(u) = headers.get(header::UPGRADE) { + if let Ok(m) = u.to_str() { + debug!("Upgrade in request header: {}", m); + return Some(m.to_owned()); + } + } + } + } + None +} diff --git a/src/proxy/utils_request.rs b/src/proxy/utils_request.rs new file mode 100644 index 0000000..c93322e --- /dev/null +++ b/src/proxy/utils_request.rs @@ -0,0 +1,58 @@ +use crate::{error::*, utils::*}; +use hyper::{header, Request, Uri}; +use std::net::SocketAddr; + +//////////////////////////////////////////////////// +// Functions of utils for request messages + +pub(super) fn log_request_msg(req: &Request, client_addr: SocketAddr) -> String { + let server_name = req.headers().get(header::HOST).map_or_else( + || { + req + .uri() + .authority() + .map_or_else(|| "", |au| au.as_str()) + }, + |h| h.to_str().unwrap_or(""), + ); + + return format!( + "{} <- {} -- {} {:?} {:?} ({:?})", + server_name, + client_addr.to_canonical(), + req.method(), + req.version(), + req + .uri() + .path_and_query() + .map_or_else(|| "", |v| v.as_str()), + req.headers() + ); +} + +pub(super) fn parse_host_port( + req: &Request, +) -> Result<(String, Option)> { + let headers_host = req.headers().get("host"); + let uri_host = req.uri().host(); + let uri_port = req.uri().port_u16(); + + ensure!( + !(headers_host.is_none() && uri_host.is_none()), + "No host in request header" + ); + + // prioritize server_name in uri + if let Some(v) = uri_host { + Ok((v.to_string(), uri_port)) + } else { + let uri_from_host = headers_host.unwrap().to_str()?.parse::()?; + Ok(( + uri_from_host + .host() + .ok_or_else(|| anyhow!("Failed to parse host"))? + .to_string(), + uri_from_host.port_u16(), + )) + } +} diff --git a/src/proxy/utils_synth_response.rs b/src/proxy/utils_synth_response.rs new file mode 100644 index 0000000..d5ae000 --- /dev/null +++ b/src/proxy/utils_synth_response.rs @@ -0,0 +1,35 @@ +// Highly motivated by https://github.com/felipenoris/hyper-reverse-proxy +use crate::error::*; +use hyper::{Body, Request, Response, StatusCode, Uri}; + +//////////////////////////////////////////////////// +// Functions to create response (error or redirect) + +pub(super) fn http_error(status_code: StatusCode) -> Result> { + let response = Response::builder() + .status(status_code) + .body(Body::empty())?; + Ok(response) +} + +pub(super) fn secure_redirection( + server_name: &str, + tls_port: Option, + req: &Request, +) -> Result> { + let pq = match req.uri().path_and_query() { + Some(x) => x.as_str(), + _ => "", + }; + let new_uri = Uri::builder().scheme("https").path_and_query(pq); + let dest_uri = match tls_port { + Some(443) | None => new_uri.authority(server_name), + Some(p) => new_uri.authority(format!("{}:{}", server_name, p)), + } + .build()?; + let response = Response::builder() + .status(StatusCode::MOVED_PERMANENTLY) + .header("Location", dest_uri.to_string()) + .body(Body::empty())?; + Ok(response) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..d575479 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,3 @@ +mod socket_addr; + +pub use socket_addr::ToCanonical; diff --git a/src/utils/socket_addr.rs b/src/utils/socket_addr.rs new file mode 100644 index 0000000..b36a58b --- /dev/null +++ b/src/utils/socket_addr.rs @@ -0,0 +1,63 @@ +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; + +pub trait ToCanonical { + fn to_canonical(&self) -> Self; +} + +impl ToCanonical for SocketAddr { + fn to_canonical(&self) -> Self { + match self { + SocketAddr::V4(_) => *self, + SocketAddr::V6(v6) => match v6.ip().to_ipv4() { + Some(mapped) => { + if mapped == Ipv4Addr::new(0, 0, 0, 1) { + *self + } else { + SocketAddr::new(IpAddr::V4(mapped), self.port()) + } + } + None => *self, + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::Ipv6Addr; + #[test] + fn ipv4_loopback_to_canonical() { + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080); + assert_eq!(socket.to_canonical(), socket); + } + #[test] + fn ipv6_loopback_to_canonical() { + let socket = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8080); + assert_eq!(socket.to_canonical(), socket); + } + #[test] + fn ipv4_to_canonical() { + let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080); + assert_eq!(socket.to_canonical(), socket); + } + #[test] + fn ipv6_to_canonical() { + let socket = SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x2001, 0x0db8, 0, 0, 0, 0, 0xdead, 0xbeef)), + 8080, + ); + assert_eq!(socket.to_canonical(), socket); + } + #[test] + fn ipv4_mapped_to_ipv6_to_canonical() { + let socket = SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0xffff, 0xc00a, 0x2ff)), + 8080, + ); + assert_eq!( + socket.to_canonical(), + SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 10, 2, 255)), 8080) + ); + } +}