Testing a small http-client on Linux using no_std & embedded reqwless.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Decouple DNS mode from transport backend selection

Refs: reqwl-dns-mode-features

rektide 81ede90f 166e6695

+148 -199
+4
README.md
··· 22 22 - Uses `reqwless` with `default-features = false` 23 23 - Default feature set is `backend-posix-libc` + `dns-getaddrinfo` 24 24 - `backend-rustix` is available as an alternate transport backend 25 + - DNS mode is selected independently via `dns-getaddrinfo` or `dns-ip-only` 25 26 - Alternate DNS mode `dns-ip-only` rejects hostnames and only accepts literal IP hosts 26 27 - HTTPS is optional via `--features https` and currently uses `TlsVerify::None` (insecure baseline) 27 28 - Uses fixed-size stack buffers for request/response and output chunks ··· 41 42 42 43 # Rustix backend with getaddrinfo DNS mode 43 44 cargo run --no-default-features --features backend-rustix,dns-getaddrinfo -- http://example.com 45 + 46 + # Rustix backend with IP-only DNS mode 47 + cargo run --no-default-features --features backend-rustix,dns-ip-only -- http://93.184.216.34 44 48 ``` 45 49 46 50 ## Validation
+11 -4
doc/portable.md
··· 76 76 app/ 77 77 main.rs 78 78 download.rs 79 + dns/ 80 + mod.rs 81 + getaddrinfo.rs 82 + ip_only.rs 79 83 net/ 80 84 mod.rs 81 85 traits.rs ··· 99 103 ``` 100 104 101 105 - `app/`: URL handling, request flow, stdout streaming. 106 + - `dns/`: DNS mode implementations selected independently from transport backend. 102 107 - `net/`: shared adapter interfaces and error mapping. 103 - - `platform/`: backend-specific networking + DNS + address conversion. 108 + - `platform/`: backend-specific transport + address conversion. 104 109 - `runtime/`: executor and FD utilities. 105 110 106 111 ## Feature Model ··· 125 130 CLI[app/main] --> DL[app/download] 126 131 DL --> NAL[embedded-nal-async traits] 127 132 NAL --> SEL{backend feature} 133 + NAL --> DNSSEL{dns feature} 128 134 SEL --> LIBC[platform/posix_libc] 129 135 SEL --> RUSTIX[platform/rustix] 130 - LIBC --> DNS{dns mode} 131 - RUSTIX --> DNS 132 - DNS --> OUT[stdout stream] 136 + DNSSEL --> DNSMODE[dns resolver] 137 + LIBC --> OUT[stdout stream] 138 + RUSTIX --> OUT 139 + DNSMODE --> OUT 133 140 ``` 134 141 135 142 ## Practical Notes
+3 -37
src/app/download.rs
··· 1 - #[cfg(feature = "dns-ip-only")] 2 - use core::net::IpAddr; 3 - 4 1 use embedded_io_async::Read; 5 2 use reqwless::client::HttpClient; 6 3 #[cfg(feature = "https")] ··· 8 5 use reqwless::request::Method; 9 6 10 7 use crate::app::main::AppError; 11 - #[cfg(feature = "dns-ip-only")] 12 - use crate::net::errors::NetError; 8 + use crate::dns::{new_dns, validate_url_for_mode}; 13 9 use crate::net::traits::{NetDns, NetTcp}; 14 - use crate::platform::{new_dns, new_tcp}; 10 + use crate::platform::new_tcp; 15 11 use crate::runtime::fd_io::write_all_fd; 16 12 17 13 #[cfg(feature = "https")] ··· 20 16 const TLS_DUMMY_SEED: u64 = 0xA17E_5EED_D00D_F00D; 21 17 22 18 pub async fn download_to_stdout(url: &str) -> Result<(), AppError> { 23 - #[cfg(feature = "dns-ip-only")] 24 - ensure_url_uses_ip_literal_host(url)?; 19 + validate_url_for_mode(url)?; 25 20 26 21 if url.starts_with("https://") { 27 22 return download_https_to_stdout(url).await; ··· 78 73 79 74 Ok(()) 80 75 } 81 - 82 - #[cfg(feature = "dns-ip-only")] 83 - fn ensure_url_uses_ip_literal_host(url: &str) -> Result<(), AppError> { 84 - let after_scheme = if let Some(rest) = url.strip_prefix("http://") { 85 - rest 86 - } else if let Some(rest) = url.strip_prefix("https://") { 87 - rest 88 - } else { 89 - return Ok(()); 90 - }; 91 - 92 - let authority = after_scheme.split_once('/').map_or(after_scheme, |(prefix, _)| prefix); 93 - let authority = authority.split_once('?').map_or(authority, |(prefix, _)| prefix); 94 - let authority = authority.split_once('#').map_or(authority, |(prefix, _)| prefix); 95 - 96 - let host_port = authority.rsplit_once('@').map_or(authority, |(_, hp)| hp); 97 - let host = if host_port.starts_with('[') { 98 - let end = host_port.find(']').ok_or(NetError::DnsModeIpOnly)?; 99 - &host_port[1..end] 100 - } else { 101 - host_port.split_once(':').map_or(host_port, |(h, _)| h) 102 - }; 103 - 104 - if host.parse::<IpAddr>().is_err() { 105 - return Err(NetError::DnsModeIpOnly.into()); 106 - } 107 - 108 - Ok(()) 109 - }
+52
src/dns/ip_only.rs
··· 1 + use core::net::IpAddr; 2 + 3 + use embedded_nal_async::{AddrType, Dns}; 4 + 5 + use crate::dns::matches_addr_type; 6 + use crate::net::errors::NetError; 7 + 8 + pub struct IpOnlyDns; 9 + 10 + impl Dns for IpOnlyDns { 11 + type Error = NetError; 12 + 13 + async fn get_host_by_name(&self, host: &str, addr_type: AddrType) -> Result<IpAddr, Self::Error> { 14 + let parsed = host.parse::<IpAddr>().map_err(|_| NetError::DnsModeIpOnly)?; 15 + if !matches_addr_type(parsed, addr_type) { 16 + return Err(NetError::AddrTypeMismatch); 17 + } 18 + Ok(parsed) 19 + } 20 + 21 + async fn get_host_by_address(&self, _addr: IpAddr, _result: &mut [u8]) -> Result<usize, Self::Error> { 22 + Err(NetError::DnsLookupFailed) 23 + } 24 + } 25 + 26 + pub fn ensure_url_uses_ip_literal_host(url: &str) -> Result<(), NetError> { 27 + let after_scheme = if let Some(rest) = url.strip_prefix("http://") { 28 + rest 29 + } else if let Some(rest) = url.strip_prefix("https://") { 30 + rest 31 + } else { 32 + return Ok(()); 33 + }; 34 + 35 + let authority = after_scheme.split_once('/').map_or(after_scheme, |(prefix, _)| prefix); 36 + let authority = authority.split_once('?').map_or(authority, |(prefix, _)| prefix); 37 + let authority = authority.split_once('#').map_or(authority, |(prefix, _)| prefix); 38 + 39 + let host_port = authority.rsplit_once('@').map_or(authority, |(_, hp)| hp); 40 + let host = if host_port.starts_with('[') { 41 + let end = host_port.find(']').ok_or(NetError::DnsModeIpOnly)?; 42 + &host_port[1..end] 43 + } else { 44 + host_port.split_once(':').map_or(host_port, |(h, _)| h) 45 + }; 46 + 47 + if host.parse::<IpAddr>().is_err() { 48 + return Err(NetError::DnsModeIpOnly); 49 + } 50 + 51 + Ok(()) 52 + }
+41
src/dns/mod.rs
··· 1 + use core::net::IpAddr; 2 + 3 + use embedded_nal_async::AddrType; 4 + 5 + use crate::net::errors::NetError; 6 + 7 + #[cfg(feature = "dns-getaddrinfo")] 8 + pub mod getaddrinfo; 9 + #[cfg(feature = "dns-ip-only")] 10 + pub mod ip_only; 11 + 12 + #[cfg(feature = "dns-getaddrinfo")] 13 + pub use getaddrinfo::GetAddrInfoDns as DnsImpl; 14 + #[cfg(feature = "dns-ip-only")] 15 + pub use ip_only::IpOnlyDns as DnsImpl; 16 + 17 + pub fn new_dns() -> DnsImpl { 18 + DnsImpl 19 + } 20 + 21 + pub fn validate_url_for_mode(url: &str) -> Result<(), NetError> { 22 + #[cfg(feature = "dns-getaddrinfo")] 23 + { 24 + let _ = url; 25 + Ok(()) 26 + } 27 + 28 + #[cfg(feature = "dns-ip-only")] 29 + { 30 + ip_only::ensure_url_uses_ip_literal_host(url) 31 + } 32 + } 33 + 34 + pub(crate) fn matches_addr_type(addr: IpAddr, addr_type: AddrType) -> bool { 35 + match (addr, addr_type) { 36 + (IpAddr::V4(_), AddrType::IPv4) => true, 37 + (IpAddr::V6(_), AddrType::IPv6) => true, 38 + (_, AddrType::Either) => true, 39 + _ => false, 40 + } 41 + }
+1
src/main.rs
··· 16 16 compile_error!("no DNS mode selected; enable dns-getaddrinfo or dns-ip-only"); 17 17 18 18 mod app; 19 + mod dns; 19 20 mod net; 20 21 mod platform; 21 22 mod runtime;
+2 -12
src/platform/mod.rs
··· 4 4 pub mod rustix; 5 5 6 6 #[cfg(feature = "backend-posix-libc")] 7 - pub use posix_libc::{PosixDns as DnsImpl, PosixTcp as TcpImpl}; 7 + pub use posix_libc::PosixTcp as TcpImpl; 8 8 9 9 #[cfg(feature = "backend-rustix")] 10 - pub use rustix::{RustixDns as DnsImpl, RustixTcp as TcpImpl}; 10 + pub use rustix::RustixTcp as TcpImpl; 11 11 12 12 #[cfg(feature = "backend-posix-libc")] 13 13 pub fn new_tcp() -> TcpImpl { 14 14 TcpImpl 15 15 } 16 16 17 - #[cfg(feature = "backend-posix-libc")] 18 - pub fn new_dns() -> DnsImpl { 19 - DnsImpl 20 - } 21 - 22 17 #[cfg(feature = "backend-rustix")] 23 18 pub fn new_tcp() -> TcpImpl { 24 19 TcpImpl 25 20 } 26 - 27 - #[cfg(feature = "backend-rustix")] 28 - pub fn new_dns() -> DnsImpl { 29 - DnsImpl 30 - }
-113
src/platform/posix_libc/dns.rs
··· 1 - use core::net::IpAddr; 2 - #[cfg(feature = "dns-getaddrinfo")] 3 - use core::ffi::c_char; 4 - #[cfg(feature = "dns-getaddrinfo")] 5 - use core::net::{Ipv4Addr, Ipv6Addr}; 6 - #[cfg(feature = "dns-getaddrinfo")] 7 - use core::ptr; 8 - 9 - use embedded_nal_async::{AddrType, Dns}; 10 - 11 - use crate::net::errors::NetError; 12 - 13 - #[cfg(feature = "dns-getaddrinfo")] 14 - const DNS_HOST_MAX: usize = 255; 15 - 16 - pub struct PosixDns; 17 - 18 - impl Dns for PosixDns { 19 - type Error = NetError; 20 - 21 - async fn get_host_by_name(&self, host: &str, addr_type: AddrType) -> Result<IpAddr, Self::Error> { 22 - if let Ok(parsed) = host.parse::<IpAddr>() { 23 - if matches_addr_type(parsed, addr_type) { 24 - return Ok(parsed); 25 - } 26 - return Err(NetError::AddrTypeMismatch); 27 - } 28 - 29 - #[cfg(feature = "dns-getaddrinfo")] 30 - { 31 - return resolve_host(host, addr_type); 32 - } 33 - 34 - #[cfg(feature = "dns-ip-only")] 35 - { 36 - let _ = host; 37 - let _ = addr_type; 38 - Err(NetError::DnsModeIpOnly) 39 - } 40 - } 41 - 42 - async fn get_host_by_address(&self, _addr: IpAddr, _result: &mut [u8]) -> Result<usize, Self::Error> { 43 - Err(NetError::DnsLookupFailed) 44 - } 45 - } 46 - 47 - fn matches_addr_type(addr: IpAddr, addr_type: AddrType) -> bool { 48 - match (addr, addr_type) { 49 - (IpAddr::V4(_), AddrType::IPv4) => true, 50 - (IpAddr::V6(_), AddrType::IPv6) => true, 51 - (_, AddrType::Either) => true, 52 - _ => false, 53 - } 54 - } 55 - 56 - #[cfg(feature = "dns-getaddrinfo")] 57 - fn resolve_host(host: &str, addr_type: AddrType) -> Result<IpAddr, NetError> { 58 - if host.len() > DNS_HOST_MAX { 59 - return Err(NetError::UrlTooLong); 60 - } 61 - if host.as_bytes().contains(&0) { 62 - return Err(NetError::NulInHost); 63 - } 64 - 65 - let mut host_buf = [0_u8; DNS_HOST_MAX + 1]; 66 - host_buf[..host.len()].copy_from_slice(host.as_bytes()); 67 - host_buf[host.len()] = 0; 68 - 69 - let mut hints: libc::addrinfo = unsafe { core::mem::zeroed() }; 70 - hints.ai_socktype = libc::SOCK_STREAM; 71 - hints.ai_family = match addr_type { 72 - AddrType::IPv4 => libc::AF_INET, 73 - AddrType::IPv6 => libc::AF_INET6, 74 - AddrType::Either => libc::AF_UNSPEC, 75 - }; 76 - 77 - let mut result: *mut libc::addrinfo = ptr::null_mut(); 78 - let lookup_rc = unsafe { 79 - libc::getaddrinfo( 80 - host_buf.as_ptr().cast::<c_char>(), 81 - ptr::null(), 82 - &hints, 83 - &mut result, 84 - ) 85 - }; 86 - if lookup_rc != 0 { 87 - return Err(NetError::DnsLookupFailed); 88 - } 89 - 90 - let mut cursor = result; 91 - let mut found = None; 92 - while !cursor.is_null() { 93 - let family = unsafe { (*cursor).ai_family }; 94 - if family == libc::AF_INET { 95 - let sockaddr = unsafe { &*((*cursor).ai_addr as *const libc::sockaddr_in) }; 96 - let octets = sockaddr.sin_addr.s_addr.to_ne_bytes(); 97 - found = Some(IpAddr::V4(Ipv4Addr::from(octets))); 98 - break; 99 - } 100 - if family == libc::AF_INET6 { 101 - let sockaddr = unsafe { &*((*cursor).ai_addr as *const libc::sockaddr_in6) }; 102 - found = Some(IpAddr::V6(Ipv6Addr::from(sockaddr.sin6_addr.s6_addr))); 103 - break; 104 - } 105 - cursor = unsafe { (*cursor).ai_next }; 106 - } 107 - 108 - unsafe { 109 - libc::freeaddrinfo(result); 110 - } 111 - 112 - found.ok_or(NetError::DnsLookupFailed) 113 - }
-3
src/platform/posix_libc/mod.rs
··· 1 1 pub mod addr; 2 - pub mod dns; 3 2 pub mod errno; 4 3 pub mod tcp; 5 4 6 - #[cfg(feature = "backend-posix-libc")] 7 - pub use dns::PosixDns; 8 5 #[cfg(feature = "backend-posix-libc")] 9 6 pub use tcp::PosixTcp;
+5 -28
src/platform/rustix/dns.rs src/dns/getaddrinfo.rs
··· 1 + use core::ffi::c_char; 1 2 use core::net::IpAddr; 2 - #[cfg(feature = "dns-getaddrinfo")] 3 - use core::ffi::c_char; 4 - #[cfg(feature = "dns-getaddrinfo")] 5 3 use core::net::{Ipv4Addr, Ipv6Addr}; 6 - #[cfg(feature = "dns-getaddrinfo")] 7 4 use core::ptr; 8 5 9 6 use embedded_nal_async::{AddrType, Dns}; 10 7 8 + use crate::dns::matches_addr_type; 11 9 use crate::net::errors::NetError; 12 10 13 - #[cfg(feature = "dns-getaddrinfo")] 14 11 const DNS_HOST_MAX: usize = 255; 15 12 16 - pub struct RustixDns; 13 + pub struct GetAddrInfoDns; 17 14 18 - impl Dns for RustixDns { 15 + impl Dns for GetAddrInfoDns { 19 16 type Error = NetError; 20 17 21 18 async fn get_host_by_name(&self, host: &str, addr_type: AddrType) -> Result<IpAddr, Self::Error> { ··· 26 23 return Err(NetError::AddrTypeMismatch); 27 24 } 28 25 29 - #[cfg(feature = "dns-getaddrinfo")] 30 - { 31 - return resolve_host(host, addr_type); 32 - } 33 - 34 - #[cfg(feature = "dns-ip-only")] 35 - { 36 - let _ = host; 37 - let _ = addr_type; 38 - Err(NetError::DnsModeIpOnly) 39 - } 26 + resolve_host(host, addr_type) 40 27 } 41 28 42 29 async fn get_host_by_address(&self, _addr: IpAddr, _result: &mut [u8]) -> Result<usize, Self::Error> { ··· 44 31 } 45 32 } 46 33 47 - fn matches_addr_type(addr: IpAddr, addr_type: AddrType) -> bool { 48 - match (addr, addr_type) { 49 - (IpAddr::V4(_), AddrType::IPv4) => true, 50 - (IpAddr::V6(_), AddrType::IPv6) => true, 51 - (_, AddrType::Either) => true, 52 - _ => false, 53 - } 54 - } 55 - 56 - #[cfg(feature = "dns-getaddrinfo")] 57 34 fn resolve_host(host: &str, addr_type: AddrType) -> Result<IpAddr, NetError> { 58 35 if host.len() > DNS_HOST_MAX { 59 36 return Err(NetError::UrlTooLong);
-2
src/platform/rustix/mod.rs
··· 1 1 pub mod addr; 2 - pub mod dns; 3 2 pub mod errno; 4 3 pub mod tcp; 5 4 6 - pub use dns::RustixDns; 7 5 pub use tcp::RustixTcp;
+29
xtask/src/validate.rs
··· 25 25 RunIpOnlyReject, 26 26 #[value(help = "check no-default build with rustix backend and getaddrinfo DNS mode")] 27 27 CheckRustix, 28 + #[value(help = "check no-default build with rustix backend and IP-only DNS mode")] 29 + CheckRustixIpOnly, 28 30 #[value(help = "run downloader against example.com with rustix backend")] 29 31 RunRustixHttpExample, 32 + #[value(help = "run rustix backend in IP-only mode against hostname and confirm rejection")] 33 + RunRustixIpOnlyReject, 30 34 } 31 35 32 36 pub fn run_validate_tasks(tasks: &[ValidateTask]) -> Result<(), String> { ··· 43 47 ValidateTask::CheckHttps, 44 48 ValidateTask::CheckIpOnly, 45 49 ValidateTask::CheckRustix, 50 + ValidateTask::CheckRustixIpOnly, 46 51 ValidateTask::RunHttpExample, 47 52 ValidateTask::RunRustixHttpExample, 48 53 ValidateTask::RunIpOnlyReject, 54 + ValidateTask::RunRustixIpOnlyReject, 49 55 ]), 50 56 ValidateTask::Build => run_sequence(&[ 51 57 ValidateTask::Check, 52 58 ValidateTask::CheckHttps, 53 59 ValidateTask::CheckIpOnly, 54 60 ValidateTask::CheckRustix, 61 + ValidateTask::CheckRustixIpOnly, 55 62 ]), 56 63 ValidateTask::Runtime => run_sequence(&[ 57 64 ValidateTask::RunHttpExample, 58 65 ValidateTask::RunRustixHttpExample, 59 66 ValidateTask::RunIpOnlyReject, 67 + ValidateTask::RunRustixIpOnlyReject, 60 68 ]), 61 69 ValidateTask::Check => run_success("cargo check", &["check"]), 62 70 ValidateTask::CheckHttps => { ··· 95 103 "backend-rustix,dns-getaddrinfo", 96 104 ], 97 105 ), 106 + ValidateTask::CheckRustixIpOnly => run_success( 107 + "cargo check --no-default-features --features backend-rustix,dns-ip-only", 108 + &[ 109 + "check", 110 + "--no-default-features", 111 + "--features", 112 + "backend-rustix,dns-ip-only", 113 + ], 114 + ), 98 115 ValidateTask::RunRustixHttpExample => run_success( 99 116 "cargo run --no-default-features --features backend-rustix,dns-getaddrinfo -- http://example.com", 100 117 &[ ··· 105 122 "--", 106 123 "http://example.com", 107 124 ], 125 + ), 126 + ValidateTask::RunRustixIpOnlyReject => run_expected_failure( 127 + "cargo run --no-default-features --features backend-rustix,dns-ip-only -- http://example.com", 128 + &[ 129 + "run", 130 + "--no-default-features", 131 + "--features", 132 + "backend-rustix,dns-ip-only", 133 + "--", 134 + "http://example.com", 135 + ], 136 + DNS_IP_ONLY_REJECT_NEEDLE, 108 137 ), 109 138 } 110 139 }