diff --git a/lib/vey-http/src/body/chunked_decoder.rs b/lib/vey-http/src/body/chunked_decoder.rs index b21c8499e..1db8458ae 100644 --- a/lib/vey-http/src/body/chunked_decoder.rs +++ b/lib/vey-http/src/body/chunked_decoder.rs @@ -174,8 +174,15 @@ impl ChunkedDataDecodeReaderInternal { match memchr::memchr(b'\n', r_buf) { Some(p) => { - self.chunk_header.put_slice(&r_buf[0..=p]); - reader.as_mut().consume(p + 1); + let len = p + 1; + if self.chunk_header.len() + len > self.body_line_max_size { + return Poll::Ready(Err(io::Error::other(format!( + "chunk header line too long (> {})", + self.body_line_max_size + )))); + } + self.chunk_header.put_slice(&r_buf[..len]); + reader.as_mut().consume(len); break; } None => { @@ -342,4 +349,49 @@ mod test { assert_eq!(&buf[..len], b"body"); assert!(decoder.finished()); } + + #[tokio::test] + async fn chunk_line_in_one_read_respects_max_size() { + let content = b"123456789\n0\r\n\r\n"; + let stream = tokio_test::io::Builder::new().read(content).build(); + let mut buf_stream = BufReader::new(stream); + let mut decoder = ChunkedDataDecodeReader::new(&mut buf_stream, 8); + + let mut buf = [0u8; 16]; + let err = decoder.read(&mut buf).await.unwrap_err(); + assert!( + err.to_string().contains("chunk header line too long"), + "{err}" + ); + } + + #[tokio::test] + async fn chunk_line_split_across_reads_respects_max_size() { + let stream = tokio_test::io::Builder::new() + .read(b"1234") + .read(b"56789\n0\r\n\r\n") + .build(); + let mut buf_stream = BufReader::new(stream); + let mut decoder = ChunkedDataDecodeReader::new(&mut buf_stream, 8); + + let mut buf = [0u8; 16]; + let err = decoder.read(&mut buf).await.unwrap_err(); + assert!( + err.to_string().contains("chunk header line too long"), + "{err}" + ); + } + + #[tokio::test] + async fn chunk_line_at_max_size_is_accepted() { + let content = b"0\r\n\r\n"; + let stream = tokio_test::io::Builder::new().read(content).build(); + let mut buf_stream = BufReader::new(stream); + let mut decoder = ChunkedDataDecodeReader::new(&mut buf_stream, 3); + + let mut buf = [0u8; 8]; + let len = decoder.read(&mut buf).await.unwrap(); + assert_eq!(len, 0); + assert!(decoder.finished()); + } } diff --git a/lib/vey-redis-client/Cargo.toml b/lib/vey-redis-client/Cargo.toml index 3c96ec3f6..0104aa15d 100644 --- a/lib/vey-redis-client/Cargo.toml +++ b/lib/vey-redis-client/Cargo.toml @@ -9,7 +9,7 @@ description = "Redis client for VEY services" [dependencies] anyhow.workspace = true redis = { workspace = true, features = ["aio", "tokio-comp"] } -tokio = { workspace = true, features = ["net"] } +tokio = { workspace = true, features = ["macros", "net", "rt", "time"] } tokio-rustls.workspace = true rustls-pki-types.workspace = true yaml-rust = { workspace = true, optional = true } diff --git a/lib/vey-redis-client/src/lib.rs b/lib/vey-redis-client/src/lib.rs index 6c182b468..298e95df6 100644 --- a/lib/vey-redis-client/src/lib.rs +++ b/lib/vey-redis-client/src/lib.rs @@ -12,6 +12,7 @@ use redis::aio::MultiplexedConnection; use redis::{AsyncCommands, AsyncConnectionConfig, ProtocolVersion, RedisConnectionInfo}; use rustls_pki_types::ServerName; use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::TcpStream; use tokio_rustls::TlsConnector; use vey_types::net::{Host, RustlsClientConfig, RustlsClientConfigBuilder, UpstreamAddr}; @@ -138,21 +139,32 @@ impl RedisClientConfigBuilder { } impl RedisClientConfig { - async fn lookup_server(&self) -> anyhow::Result { + async fn lookup_server(&self) -> anyhow::Result> { match self.server.host() { Host::Domain(domain) => { - let mut ips = tokio::net::lookup_host((domain.as_str(), self.server.port())) - .await - .map_err(|e| anyhow!("failed to resolve domain {domain}: {e}"))?; - ips.next() - .ok_or_else(|| anyhow!("no ip address resolved for domain {domain}")) + let ips = match tokio::time::timeout( + self.connect_timeout, + tokio::net::lookup_host((domain.as_str(), self.server.port())), + ) + .await + { + Ok(Ok(ips)) => ips, + Ok(Err(e)) => { + return Err(anyhow!("failed to resolve domain {domain}: {e}")); + } + Err(_) => return Err(anyhow!("timeout to resolve domain {domain}")), + }; + let addrs: Vec = ips.collect(); + if addrs.is_empty() { + return Err(anyhow!("no ip address resolved for domain {domain}")); + } + Ok(addrs) } - Host::Ip(ip) => Ok(SocketAddr::new(*ip, self.server.port())), + Host::Ip(ip) => Ok(vec![SocketAddr::new(*ip, self.server.port())]), } } - pub async fn connect(&self) -> anyhow::Result> { - let peer = self.lookup_server().await?; + async fn tcp_connect(&self, peer: SocketAddr) -> anyhow::Result { let socket = vey_socket::tcp::new_socket_to( peer.ip(), &Default::default(), @@ -162,27 +174,47 @@ impl RedisClientConfig { ) .map_err(|e| anyhow!("failed to create new socket: {e}"))?; - let stream = match tokio::time::timeout(self.connect_timeout, socket.connect(peer)).await { - Ok(Ok(stream)) => stream, - Ok(Err(e)) => return Err(anyhow!("failed to connect to {}: {e}", self.server)), - Err(_) => return Err(anyhow!("timeout to connect to {}", self.server)), - }; + match tokio::time::timeout(self.connect_timeout, socket.connect(peer)).await { + Ok(Ok(stream)) => Ok(stream), + Ok(Err(e)) => Err(anyhow!("failed to connect to {}: {e}", self.server)), + Err(_) => Err(anyhow!("timeout to connect to {}", self.server)), + } + } - if let Some(tls_client) = &self.tls_client { - let tls_connector = TlsConnector::from(tls_client.tls_config.driver.clone()); - match tokio::time::timeout( - tls_client.tls_config.handshake_timeout, - tls_connector.connect(tls_client.tls_name.clone(), stream), - ) - .await - { - Ok(Ok(stream)) => self.redis_handshake(stream).await, - Ok(Err(e)) => Err(anyhow!("failed to tls handshake with {}: {e}", self.server)), - Err(_) => Err(anyhow!("timeout to tls handshake with {}", self.server)), - } - } else { - self.redis_handshake(stream).await + pub async fn connect(&self) -> anyhow::Result> { + let peers = self.lookup_server().await?; + self.connect_peers(peers).await + } + + async fn connect_peers(&self, peers: Vec) -> anyhow::Result { + let mut last_err = None; + for peer in peers { + let stream = match self.tcp_connect(peer).await { + Ok(stream) => stream, + Err(e) => { + last_err = Some(e); + continue; + } + }; + + return if let Some(tls_client) = &self.tls_client { + let tls_connector = TlsConnector::from(tls_client.tls_config.driver.clone()); + match tokio::time::timeout( + tls_client.tls_config.handshake_timeout, + tls_connector.connect(tls_client.tls_name.clone(), stream), + ) + .await + { + Ok(Ok(stream)) => self.redis_handshake(stream).await, + Ok(Err(e)) => Err(anyhow!("failed to tls handshake with {}: {e}", self.server)), + Err(_) => Err(anyhow!("timeout to tls handshake with {}", self.server)), + } + } else { + self.redis_handshake(stream).await + }; } + + Err(last_err.unwrap_or_else(|| anyhow!("no ip address resolved for {}", self.server))) } async fn redis_handshake(&self, stream: S) -> anyhow::Result @@ -192,10 +224,15 @@ impl RedisClientConfig { let async_config = AsyncConnectionConfig::new().set_response_timeout(Some(self.response_timeout)); - let (conn, background) = - MultiplexedConnection::new_with_config(&self.db_info, stream, async_config) - .await - .map_err(|e| anyhow!("redis handshake with {} failed: {e}", self.server))?; + let handshake = MultiplexedConnection::new_with_config(&self.db_info, stream, async_config); + let (conn, background) = match tokio::time::timeout(self.response_timeout, handshake).await + { + Ok(Ok(conn)) => conn, + Ok(Err(e)) => { + return Err(anyhow!("redis handshake with {} failed: {e}", self.server)); + } + Err(_) => return Err(anyhow!("timeout to redis handshake with {}", self.server)), + }; tokio::spawn(background); Ok(conn) } @@ -282,4 +319,67 @@ mod tests { let tls = config.tls_client.as_ref().unwrap(); assert_eq!(tls.tls_name, ServerName::try_from("10.0.0.5").unwrap()); } + + fn silent_peer_config(port: u16) -> RedisClientConfig { + let mut builder = RedisClientConfigBuilder::new(UpstreamAddr::new( + Host::Ip(IpAddr::from_str("127.0.0.1").unwrap()), + port, + )); + builder.set_connect_timeout(Duration::from_millis(500)); + builder.set_response_timeout(Duration::from_millis(200)); + builder.build().unwrap() + } + + #[tokio::test] + async fn handshake_times_out_when_peer_is_silent() { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + tokio::spawn(async move { + let (sock, _) = listener.accept().await.unwrap(); + let _hold = sock; + std::future::pending::<()>().await; + }); + + let config = silent_peer_config(addr.port()); + let started = std::time::Instant::now(); + let err = match config.connect().await { + Ok(_) => panic!("silent peer should not finish handshake"), + Err(e) => e, + }; + let elapsed = started.elapsed(); + + assert!( + err.to_string().contains("timeout to redis handshake"), + "{err}" + ); + assert!(elapsed >= Duration::from_millis(150), "{elapsed:?}"); + assert!(elapsed < Duration::from_millis(1500), "{elapsed:?}"); + } + + #[tokio::test] + async fn connect_tries_next_address_after_refusal() { + let closed = { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + drop(listener); + addr + }; + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let open = listener.local_addr().unwrap(); + tokio::spawn(async move { + let (sock, _) = listener.accept().await.unwrap(); + let _hold = sock; + std::future::pending::<()>().await; + }); + + let config = silent_peer_config(open.port()); + let err = match config.connect_peers(vec![closed, open]).await { + Ok(_) => panic!("closed then silent peer should not finish handshake"), + Err(e) => e, + }; + assert!( + err.to_string().contains("timeout to redis handshake"), + "{err}" + ); + } } diff --git a/lib/vey-reuseport/src/lib.rs b/lib/vey-reuseport/src/lib.rs index 76cecb703..8fb12e9b1 100644 --- a/lib/vey-reuseport/src/lib.rs +++ b/lib/vey-reuseport/src/lib.rs @@ -3,12 +3,27 @@ * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ +use std::net::{IpAddr, SocketAddr}; +use std::path::PathBuf; + use zerocopy::{Immutable, IntoBytes}; pub mod quic; pub mod tcp; pub mod udp; +/// Pin directory for one listener. IPv4 and IPv6 never share it: `0.0.0.0` +/// and `::` are different sockets, and a shared map collides on worker keys. +pub(crate) fn reuseport_pin_dir(protocol: &str, addr: SocketAddr) -> PathBuf { + let ip = match addr.ip() { + // '.' is not allowed in the pin path. + IpAddr::V4(ip) => format!("v4-{:08x}", u32::from(ip)), + IpAddr::V6(ip) => format!("v6-{}", ip.to_string().replace('.', "-")), + }; + let port = addr.port(); + PathBuf::from(format!("/sys/fs/bpf/vey-reuseport/{protocol}/{ip}_{port}")) +} + #[derive(IntoBytes, Immutable)] #[repr(C)] struct SocketId { @@ -43,6 +58,7 @@ struct ReadOnlyData { mod tests { use super::*; use std::mem::size_of; + use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; #[test] fn bpf_struct_layout_matches_expectations() { @@ -63,4 +79,36 @@ mod tests { assert_eq!(bytes.len(), size_of::()); assert_eq!(bytes.len(), 8); } + + #[test] + fn unspecified_v4_and_v6_do_not_share_pin_dir() { + let v4 = SocketAddr::from((Ipv4Addr::UNSPECIFIED, 443)); + let v6 = SocketAddr::from((Ipv6Addr::UNSPECIFIED, 443)); + let v4_dir = reuseport_pin_dir("tcp", v4); + let v6_dir = reuseport_pin_dir("tcp", v6); + + assert_ne!(v4_dir, v6_dir); + assert!(!v4_dir.display().to_string().contains('.')); + assert!(!v6_dir.display().to_string().contains('.')); + assert!(v4_dir.ends_with("v4-00000000_443")); + assert!(v6_dir.ends_with("v6-::_443")); + } + + #[test] + fn ipv4_localhost_does_not_share_pin_dir_with_ipv6_localhost() { + let v4 = SocketAddr::from((Ipv4Addr::new(0, 0, 0, 1), 443)); + let v6 = SocketAddr::from((Ipv6Addr::LOCALHOST, 443)); + assert_ne!(reuseport_pin_dir("udp", v4), reuseport_pin_dir("udp", v6)); + } + + #[test] + fn ipv4_does_not_share_pin_dir_with_mapped_ipv6() { + let v4 = SocketAddr::from((Ipv4Addr::LOCALHOST, 53)); + let mapped = SocketAddr::from((Ipv4Addr::LOCALHOST.to_ipv6_mapped(), 53)); + let v4_dir = reuseport_pin_dir("quic", v4); + let v6_dir = reuseport_pin_dir("quic", mapped); + + assert_ne!(v4_dir, v6_dir); + assert!(!v6_dir.display().to_string().contains('.')); + } } diff --git a/lib/vey-reuseport/src/quic.rs b/lib/vey-reuseport/src/quic.rs index ab9b1722d..1d5aa5fd0 100644 --- a/lib/vey-reuseport/src/quic.rs +++ b/lib/vey-reuseport/src/quic.rs @@ -3,7 +3,7 @@ * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ -use std::net::{IpAddr, SocketAddr}; +use std::net::SocketAddr; use std::os::fd::AsFd; use std::path::{Path, PathBuf}; use std::{fs, ptr}; @@ -93,12 +93,7 @@ impl QuicSocketSelector { } pub fn new(pid: i32, generation: u16, addr: SocketAddr) -> anyhow::Result { - let ip = match addr.ip() { - IpAddr::V4(ip) => ip.to_ipv6_compatible(), // IPv4 "." is not allowed in path - IpAddr::V6(ip) => ip, - }; - let dir = format!("/sys/fs/bpf/vey-reuseport/quic/{ip}_{}", addr.port()); - let pin_dir = PathBuf::from(dir); + let pin_dir = super::reuseport_pin_dir("quic", addr); let quic_conn_track_path = pin_dir.join(NAME_QUIC_CONN_TRACK); let socket_map_path = pin_dir.join(NAME_SOCKET_MAP); diff --git a/lib/vey-reuseport/src/tcp.rs b/lib/vey-reuseport/src/tcp.rs index 2306cd01c..b28db030c 100644 --- a/lib/vey-reuseport/src/tcp.rs +++ b/lib/vey-reuseport/src/tcp.rs @@ -3,7 +3,7 @@ * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ -use std::net::{IpAddr, SocketAddr}; +use std::net::SocketAddr; use std::os::fd::AsFd; use std::path::{Path, PathBuf}; use std::{fs, ptr}; @@ -40,12 +40,7 @@ impl TcpSocketSelector { } pub fn new(pid: i32, generation: u16, addr: SocketAddr) -> anyhow::Result { - let ip = match addr.ip() { - IpAddr::V4(ip) => ip.to_ipv6_compatible(), // IPv4 "." is not allowed in path - IpAddr::V6(ip) => ip, - }; - let dir = format!("/sys/fs/bpf/vey-reuseport/tcp/{ip}_{}", addr.port()); - let pin_dir = PathBuf::from(dir); + let pin_dir = super::reuseport_pin_dir("tcp", addr); fs::create_dir_all(&pin_dir) .map_err(|e| anyhow!("failed to create pin directory {}: {e}", pin_dir.display()))?; diff --git a/lib/vey-reuseport/src/udp.rs b/lib/vey-reuseport/src/udp.rs index df7a5385e..d1831c0f4 100644 --- a/lib/vey-reuseport/src/udp.rs +++ b/lib/vey-reuseport/src/udp.rs @@ -3,7 +3,7 @@ * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ -use std::net::{IpAddr, SocketAddr}; +use std::net::SocketAddr; use std::num::NonZeroU32; use std::os::fd::AsFd; use std::path::{Path, PathBuf}; @@ -79,12 +79,7 @@ impl UdpSocketSelector { addr: SocketAddr, conn_track_max_entries: NonZeroU32, ) -> anyhow::Result { - let ip = match addr.ip() { - IpAddr::V4(ip) => ip.to_ipv6_compatible(), // IPv4 "." is not allowed in path - IpAddr::V6(ip) => ip, - }; - let dir = format!("/sys/fs/bpf/vey-reuseport/udp/{ip}_{}", addr.port()); - let pin_dir = PathBuf::from(dir); + let pin_dir = super::reuseport_pin_dir("udp", addr); let socket_map_path = pin_dir.join(NAME_SOCKET_MAP); fs::create_dir_all(&pin_dir) diff --git a/lib/vey-smtp-proto/src/command/hello.rs b/lib/vey-smtp-proto/src/command/hello.rs index 16fba1269..a161abbe9 100644 --- a/lib/vey-smtp-proto/src/command/hello.rs +++ b/lib/vey-smtp-proto/src/command/hello.rs @@ -56,7 +56,16 @@ mod tests { let result = parse_host(b"[192.168.1]").unwrap_err(); assert!(matches!(result, CommandLineError::InvalidClientHost)); - let result = parse_host(b"IPv6:2001:db8::1").unwrap_err(); - assert!(matches!(result, CommandLineError::InvalidClientHost)); + let result = parse_host(b"IPv6:2001:db8::1").unwrap(); + assert_eq!( + result, + Host::Ip(IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1))) + ); + + let result = parse_host(b"[IPv6:2001:db8::1]").unwrap(); + assert_eq!( + result, + Host::Ip(IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1))) + ); } } diff --git a/lib/vey-types/src/net/host.rs b/lib/vey-types/src/net/host.rs index bccf53057..baf175452 100644 --- a/lib/vey-types/src/net/host.rs +++ b/lib/vey-types/src/net/host.rs @@ -66,24 +66,16 @@ impl Host { if buf[end] != b']' { return None; } - let Ok(s) = std::str::from_utf8(&buf[1..end]) else { + let inner = &buf[1..end]; + let Ok(s) = std::str::from_utf8(inner) else { return None; }; - Ipv4Addr::from_str(s) - .map(|v4| Host::Ip(IpAddr::V4(v4))) - .ok() - } else if let Some(d) = memchr::memchr(b':', buf) { - match &buf[0..d] { - b"Ipv6" => { - let Ok(s) = std::str::from_utf8(&buf[d + 1..]) else { - return None; - }; - Ipv6Addr::from_str(s) - .map(|v6| Host::Ip(IpAddr::V6(v6))) - .ok() - } - _ => None, + if let Ok(v4) = Ipv4Addr::from_str(s) { + return Some(Host::Ip(IpAddr::V4(v4))); } + parse_smtp_ipv6_literal(inner) + } else if memchr::memchr(b':', buf).is_some() { + parse_smtp_ipv6_literal(buf) } else { let Ok(s) = std::str::from_utf8(buf) else { return None; @@ -93,6 +85,19 @@ impl Host { } } +fn parse_smtp_ipv6_literal(buf: &[u8]) -> Option { + let d = memchr::memchr(b':', buf)?; + if !buf[..d].eq_ignore_ascii_case(b"IPv6") { + return None; + } + let Ok(s) = std::str::from_utf8(&buf[d + 1..]) else { + return None; + }; + Ipv6Addr::from_str(s) + .map(|v6| Host::Ip(IpAddr::V6(v6))) + .ok() +} + impl fmt::Display for Host { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -201,5 +206,13 @@ mod tests { let host = Host::parse_smtp_host_address(b"Ipv6:2001:db8::1").unwrap(); assert_eq!(host, Host::Ip(IpAddr::from_str("2001:db8::1").unwrap())); + + let host = Host::parse_smtp_host_address(b"[IPv6:2001:db8::1]").unwrap(); + assert_eq!(host, Host::Ip(IpAddr::from_str("2001:db8::1").unwrap())); + + let host = Host::parse_smtp_host_address(b"[ipv6:2001:db8::1]").unwrap(); + assert_eq!(host, Host::Ip(IpAddr::from_str("2001:db8::1").unwrap())); + + assert!(Host::parse_smtp_host_address(b"[IPv6:not-an-ip]").is_none()); } } diff --git a/lib/vey-types/src/net/openssl/server/ticket_key.rs b/lib/vey-types/src/net/openssl/server/ticket_key.rs index 5499e4e4c..9d77f6cc9 100644 --- a/lib/vey-types/src/net/openssl/server/ticket_key.rs +++ b/lib/vey-types/src/net/openssl/server/ticket_key.rs @@ -1,6 +1,7 @@ /* * SPDX-License-Identifier: Apache-2.0 * SPDX-FileCopyrightText: 2024-2025 ByteDance and/or its affiliates. + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ use std::cell::RefCell; @@ -210,17 +211,17 @@ impl OpensslTicketKey { return Ok(None); }; + let Some(tag_at) = ciphertext.len().checked_sub(SHA256_DIGEST_LENGTH) else { + return Ok(None); + }; + TICKET_CONTEXT.with_borrow_mut(|ctx| { ctx.cipher.reset()?; ctx.hmac.reset()?; self.decrypt_init(iv, &mut ctx.cipher, &mut ctx.hmac)?; - let Some((encrypted, hmac_tag)) = - ciphertext.split_at_checked(ciphertext.len() - SHA256_DIGEST_LENGTH) - else { - return Ok(None); - }; + let (encrypted, hmac_tag) = ciphertext.split_at(tag_at); ctx.hmac.hmac_update(key_name)?; ctx.hmac.hmac_update(iv)?; @@ -288,4 +289,12 @@ mod test { let decrypted = key.decrypt(&encrypted).unwrap().unwrap(); assert_eq!(msg.as_bytes(), decrypted); } + + #[test] + fn decrypt_short_ticket_returns_none() { + let key = OpensslTicketKey::new_random(30).unwrap(); + assert!(key.decrypt(&[]).unwrap().is_none()); + let short = vec![0u8; TICKET_KEY_NAME_LENGTH + TICKET_AES_IV_LENGTH + 31]; + assert!(key.decrypt(&short).unwrap().is_none()); + } } diff --git a/lib/vey-types/src/net/tcp/listen.rs b/lib/vey-types/src/net/tcp/listen.rs index 806566ec9..e9ff7ad67 100644 --- a/lib/vey-types/src/net/tcp/listen.rs +++ b/lib/vey-types/src/net/tcp/listen.rs @@ -262,11 +262,19 @@ impl TcpListenConfig { Ok(()) } - pub fn set_fraction_scale(&mut self, numerator: usize, denominator: usize) { + pub fn set_fraction_scale( + &mut self, + numerator: usize, + denominator: usize, + ) -> anyhow::Result<()> { + if denominator == 0 { + return Err(anyhow!("scale denominator is 0")); + } if let Ok(p) = std::thread::available_parallelism() { let v = p.get() * numerator / denominator; self.scale = v; } + Ok(()) } #[inline] diff --git a/lib/vey-types/src/net/udp/listen.rs b/lib/vey-types/src/net/udp/listen.rs index ab55d639c..8e245176c 100644 --- a/lib/vey-types/src/net/udp/listen.rs +++ b/lib/vey-types/src/net/udp/listen.rs @@ -241,11 +241,19 @@ impl UdpListenConfig { Ok(()) } - pub fn set_fraction_scale(&mut self, numerator: usize, denominator: usize) { + pub fn set_fraction_scale( + &mut self, + numerator: usize, + denominator: usize, + ) -> anyhow::Result<()> { + if denominator == 0 { + return Err(anyhow!("scale denominator is 0")); + } if let Ok(p) = std::thread::available_parallelism() { let v = p.get() * numerator / denominator; self.non_reloadable.scale = v; } + Ok(()) } #[cfg(target_os = "linux")] diff --git a/lib/vey-yaml/src/value/dpi/inspect.rs b/lib/vey-yaml/src/value/dpi/inspect.rs index 97bd7299b..71c7d8eeb 100644 --- a/lib/vey-yaml/src/value/dpi/inspect.rs +++ b/lib/vey-yaml/src/value/dpi/inspect.rs @@ -1,6 +1,7 @@ /* * SPDX-License-Identifier: Apache-2.0 * SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates. + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ use anyhow::{Context, anyhow}; @@ -44,6 +45,7 @@ pub fn parse_inspect_size_limit( config.set_ldap_request_msg(size); Ok(()) } + // Kept so old configs still load. SMTP reply lines are fixed at 512 bytes. "smtp_greeting_msg" | "smtp_server_greeting_msg" => Ok(()), _ => Err(anyhow!("invalid key {k}")), }) diff --git a/lib/vey-yaml/src/value/metrics.rs b/lib/vey-yaml/src/value/metrics.rs index 770900226..0a054a058 100644 --- a/lib/vey-yaml/src/value/metrics.rs +++ b/lib/vey-yaml/src/value/metrics.rs @@ -1,6 +1,7 @@ /* * SPDX-License-Identifier: Apache-2.0 * SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates. + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ use std::str::FromStr; @@ -75,7 +76,7 @@ pub fn as_weighted_metric_node_name(value: &Yaml) -> anyhow::Result Ok(()), + _ => Err(anyhow!("invalid key {k}")), })?; if name.is_empty() { @@ -298,5 +299,13 @@ mod tests { "# ); assert!(as_weighted_metric_node_name(&yaml).is_err()); + + let yaml = yaml_doc!( + r#" + name: node + weigth: 9 + "# + ); + assert!(as_weighted_metric_node_name(&yaml).is_err()); } } diff --git a/lib/vey-yaml/src/value/net/http.rs b/lib/vey-yaml/src/value/net/http.rs index 1518f9de6..fb890d2d9 100644 --- a/lib/vey-yaml/src/value/net/http.rs +++ b/lib/vey-yaml/src/value/net/http.rs @@ -70,11 +70,10 @@ pub fn as_http_forward_capability(value: &Yaml) -> anyhow::Result { - if let Ok(v) = crate::hash_get_required(map, "forward_ftp") { - let enable = - crate::value::as_bool(v).context("invalid bool value for key forward_ftp")?; - cap.set_forward_ftp_all(enable); - } + let mut forward_ftp = None; + let mut forward_ftp_get = None; + let mut forward_ftp_put = None; + let mut forward_ftp_del = None; crate::foreach_kv(map, |k, v| match crate::key::normalize(k).as_str() { "forward_https" => { @@ -82,22 +81,22 @@ pub fn as_http_forward_capability(value: &Yaml) -> anyhow::Result { + forward_ftp = Some(crate::value::as_bool(v)?); + Ok(()) + } "forward_ftp_get" => { - let enable = crate::value::as_bool(v)?; - cap.set_forward_ftp_get(enable); + forward_ftp_get = Some(crate::value::as_bool(v)?); Ok(()) } "forward_ftp_put" => { - let enable = crate::value::as_bool(v)?; - cap.set_forward_ftp_put(enable); + forward_ftp_put = Some(crate::value::as_bool(v)?); Ok(()) } "forward_ftp_del" => { - let enable = crate::value::as_bool(v)?; - cap.set_forward_ftp_del(enable); + forward_ftp_del = Some(crate::value::as_bool(v)?); Ok(()) } - "forward_ftp" => Ok(()), "session_based_auth" => { let enable = crate::value::as_bool(v)?; cap.set_session_auth(enable); @@ -105,6 +104,20 @@ pub fn as_http_forward_capability(value: &Yaml) -> anyhow::Result Err(anyhow!("invalid key {k}")), })?; + + // The all-flag sets every method. Apply it first so a specific method can override it. + if let Some(enable) = forward_ftp { + cap.set_forward_ftp_all(enable); + } + if let Some(enable) = forward_ftp_get { + cap.set_forward_ftp_get(enable); + } + if let Some(enable) = forward_ftp_put { + cap.set_forward_ftp_put(enable); + } + if let Some(enable) = forward_ftp_del { + cap.set_forward_ftp_del(enable); + } } _ => return Err(anyhow!("invalid yaml value type for HttpForwardCapability")), } @@ -305,6 +318,12 @@ mod tests { assert!(cap.forward_ftp(&Method::PUT)); assert!(cap.forward_ftp(&Method::DELETE)); + let yaml = yaml_doc!("{ forward-ftp: true }"); + let cap = as_http_forward_capability(&yaml).unwrap(); + assert!(cap.forward_ftp(&Method::GET)); + assert!(cap.forward_ftp(&Method::PUT)); + assert!(cap.forward_ftp(&Method::DELETE)); + let yaml = yaml_doc!( r#" forward_ftp_get: true diff --git a/lib/vey-yaml/src/value/net/tcp.rs b/lib/vey-yaml/src/value/net/tcp.rs index e1d120d00..03e4078fd 100644 --- a/lib/vey-yaml/src/value/net/tcp.rs +++ b/lib/vey-yaml/src/value/net/tcp.rs @@ -30,8 +30,7 @@ fn set_tcp_listen_scale(config: &mut TcpListenConfig, v: &Yaml) -> anyhow::Resul let Ok(d) = usize::from_str(d.trim()) else { return Err(anyhow!("invalid fractional value {s}: invalid denominator"))?; }; - config.set_fraction_scale(n, d); - Ok(()) + config.set_fraction_scale(n, d) } else { let Ok(v) = f64::from_str(s) else { return Err(anyhow!("invalid float value: {s}")); @@ -510,6 +509,10 @@ mod tests { let mut cfg = TcpListenConfig::default(); assert!(set_tcp_listen_scale(&mut cfg, &yaml_map["scale"]).is_err()); + let yaml_map = yaml_doc!("scale: \"1/0\""); + let mut cfg = TcpListenConfig::default(); + assert!(set_tcp_listen_scale(&mut cfg, &yaml_map["scale"]).is_err()); + let yaml_map = yaml_doc!("scale: \"not_a_float\""); let mut cfg = TcpListenConfig::default(); assert!(set_tcp_listen_scale(&mut cfg, &yaml_map["scale"]).is_err()); diff --git a/lib/vey-yaml/src/value/net/udp.rs b/lib/vey-yaml/src/value/net/udp.rs index 6fe68f3f8..593f40102 100644 --- a/lib/vey-yaml/src/value/net/udp.rs +++ b/lib/vey-yaml/src/value/net/udp.rs @@ -97,8 +97,7 @@ fn set_udp_listen_scale(config: &mut UdpListenConfig, v: &Yaml) -> anyhow::Resul let Ok(d) = usize::from_str(d.trim()) else { return Err(anyhow!("invalid fractional value {s}: invalid denominator"))?; }; - config.set_fraction_scale(n, d); - Ok(()) + config.set_fraction_scale(n, d) } else { let Ok(v) = f64::from_str(s) else { return Err(anyhow!("invalid float value: {s}")); @@ -441,7 +440,7 @@ mod tests { let parsed_config = as_udp_listen_config(&yaml).unwrap(); let mut expected_config = UdpListenConfig::default(); expected_config.set_socket_address("127.0.0.1:1001".parse().unwrap()); - expected_config.set_fraction_scale(1, 2); + expected_config.set_fraction_scale(1, 2).unwrap(); assert_eq!(parsed_config, expected_config); // Scale as float @@ -491,6 +490,9 @@ mod tests { let yaml = yaml_doc!("addr: 127.0.0.1:1005\nscale: 1/a"); assert!(as_udp_listen_config(&yaml).is_err()); + let yaml = yaml_doc!("addr: 127.0.0.1:1005\nscale: 1/0"); + assert!(as_udp_listen_config(&yaml).is_err()); + // Invalid key let yaml = yaml_doc!("addr: 127.0.0.1:1006\nfoo: bar"); assert!(as_udp_listen_config(&yaml).is_err()); diff --git a/lib/vey-yaml/src/value/route/alpn.rs b/lib/vey-yaml/src/value/route/alpn.rs index 96da96201..5f54be9fd 100644 --- a/lib/vey-yaml/src/value/route/alpn.rs +++ b/lib/vey-yaml/src/value/route/alpn.rs @@ -1,6 +1,7 @@ /* * SPDX-License-Identifier: Apache-2.0 * SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates. + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ use std::sync::Arc; @@ -139,6 +140,10 @@ fn add_alpn_matched_backend(obj: &mut AlpnMatch, value: &Yaml) -> anyh name = crate::value::as_metric_node_name(value)?; } + if name.is_empty() { + return Err(anyhow!("no backend name is set")); + } + let mut auto_default = true; for protocol in protocol_vs { if obj.add_protocol(protocol.clone(), name.clone()).is_some() { @@ -392,5 +397,12 @@ mod tests { "# ); assert!(as_alpn_matched_backends(&yaml).is_err()); + + let yaml = yaml_doc!( + r#" + protocol: h2 + "# + ); + assert!(as_alpn_matched_backends(&yaml).is_err()); } } diff --git a/vey-bench/CHANGELOG b/vey-bench/CHANGELOG index b8df9c1a7..6bd0f7d65 100644 --- a/vey-bench/CHANGELOG +++ b/vey-bench/CHANGELOG @@ -1,13 +1,16 @@ v0.9.9: - - BUG FIX: DNS-over-HTTPS releases the HTTP/2 receive window, and a DNS-over-HTTPS or DNS-over-HTTP/3 response body stops at 65535 bytes - BUG FIX: a buffered stream copy or vectored write stops when the writer returns no bytes - BUG FIX: a non-ASCII byte size on the command line is rejected instead of panicking - BUG FIX: a rotating histogram still publishes while samples keep arriving, and `0.0` and `-0.0` count as one quantile + - BUG FIX: a statsd export continues after `sendmmsg` writes only part of the batch, and logs the first error in each 64 second slice - BUG FIX: a thrift compact varint that overflows a u32 is rejected instead of being truncated + - BUG FIX: an HTTP chunk size line longer than the limit is rejected even when the newline arrives in the same read - BUG FIX: an HTTP chunked write stops when the writer returns no bytes - BUG FIX: an issued certificate cannot start before its issuer, and its `pathlen` cannot exceed what the issuer allows + - BUG FIX: an OpenSSL async client engine stays registered after the handshake and no longer busy-waits - BUG FIX: certificate times outside 1950-2049 use GeneralizedTime + - BUG FIX: DNS-over-HTTPS releases the HTTP/2 receive window, and a DNS-over-HTTPS or DNS-over-HTTP/3 response body stops at 65535 bytes - BUG FIX: OpenSSL async engine file descriptors are registered with the I/O-safe AsyncFd API - Feature: h1/h2/h3 targets support the HTTP `QUERY` method (RFC 10008), including `--payload` - Compatibility: `-r` / `--rate-limit` paces burst to about 1ms of traffic so load generation does not dump a full period of requests diff --git a/vey-dcgen/CHANGELOG b/vey-dcgen/CHANGELOG index 7acfd60b8..c36f75db6 100644 --- a/vey-dcgen/CHANGELOG +++ b/vey-dcgen/CHANGELOG @@ -1,20 +1,27 @@ v0.10.0: - - Breaking: listen sockets are server entries. `-G port` and `UDP_LISTEN_ADDR` no longer select the listen address. Configure `server` in YAML. `vey-dcgen-ctl` reloads servers and the issuer, and requests offline shutdown - - Breaking: the CA config section is `issuer`. The key `backend` is no longer accepted - - Feature: `vey-dcgen-ctl reload-issuer` re-reads `issuer` and swaps the running CA without restarting listeners - - Breaking: certificate generator metrics use the `issuer` prefix. `backend.refresh_total`, `backend.refresh_ok`, `backend.request_total`, `backend.request_ok`, and `backend.request_duration` are now `issuer.refresh_total`, `issuer.refresh_ok`, `issuer.request_total`, `issuer.request_ok`, and `issuer.request_duration` - - BUG FIX: after graceful shutdown is cancelled, a later SIGTERM still requests shutdown - - Breaking: the default control socket directory is `/run/vey-dcgen` for root on Linux and `/var/run/vey-dcgen` for root on other Unix systems; another Linux user uses an absolute `XDG_RUNTIME_DIR` or `/run/user//vey-dcgen` - BUG FIX: a large Linux journald record can be sealed and sent + - BUG FIX: a log format failure is reported instead of panicking, and syslog is sent before the reconnect wait - BUG FIX: a non-ASCII size string in worker or statsd config is rejected instead of panicking + - BUG FIX: a quit during graceful shutdown closes the unique control listener - BUG FIX: a relative pid file is created in the working directory from before daemonizing, and the lock is held until the process exits - BUG FIX: a rotating histogram still publishes while samples keep arriving, and `0.0` and `-0.0` count as one quantile + - BUG FIX: a statsd export continues after `sendmmsg` writes only part of the batch, and logs the first error in each 64 second slice + - BUG FIX: a UDP listen `scale` whose denominator is 0 is rejected instead of panicking + - BUG FIX: after graceful shutdown is cancelled, a later SIGTERM still requests shutdown - BUG FIX: an empty daemon group uses `_.sock` as the control socket - BUG FIX: an issued certificate cannot start before its issuer, and its `pathlen` cannot exceed what the issuer allows - BUG FIX: certificate times outside 1950-2049 use GeneralizedTime, and a timestamp offset that includes seconds is written in UTC + - BUG FIX: failure to spawn the async log thread is reported + - BUG FIX: IPv4 and IPv6 reuseport listeners use separate eBPF maps, so `0.0.0.0` and `::` no longer share one - BUG FIX: on Windows, another control client can connect while one connection is still open - BUG FIX: the Windows hostname sent to syslog no longer includes a trailing NUL + - Feature: `vey-dcgen-ctl reload-issuer` re-reads `issuer` and swaps the running CA without restarting listeners + - Breaking: certificate generator metrics use the `issuer` prefix. `backend.refresh_total`, `backend.refresh_ok`, `backend.request_total`, `backend.request_ok`, and `backend.request_duration` are now `issuer.refresh_total`, `issuer.refresh_ok`, `issuer.request_total`, `issuer.request_ok`, and `issuer.request_duration` + - Breaking: listen sockets are server entries. `-G port` and `UDP_LISTEN_ADDR` no longer select the listen address. Configure `server` in YAML. `vey-dcgen-ctl` reloads servers and the issuer, and requests offline shutdown + - Breaking: the CA config section is `issuer`. The key `backend` is no longer accepted + - Breaking: the default control socket directory is `/run/vey-dcgen` for root on Linux and `/var/run/vey-dcgen` for root on other Unix systems; another Linux user uses an absolute `XDG_RUNTIME_DIR` or `/run/user//vey-dcgen` + - Compatibility: a reuseport listener does not join eBPF maps pinned by a previous release - Compatibility: bump MSRV to 1.94.0 - Compatibility: DragonFly worker `sched_affinity` applies to the calling thread - Compatibility: published container images set OCI labels so GHCR can describe them diff --git a/vey-dcgen/src/config/issuer.rs b/vey-dcgen/src/config/issuer.rs index 5b4b536b6..1613d91aa 100644 --- a/vey-dcgen/src/config/issuer.rs +++ b/vey-dcgen/src/config/issuer.rs @@ -1,6 +1,7 @@ /* * SPDX-License-Identifier: Apache-2.0 * SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates. + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ use std::sync::Arc; diff --git a/vey-dcgen/src/issuer/stats.rs b/vey-dcgen/src/issuer/stats.rs index ad9c99b9c..f9a796bb2 100644 --- a/vey-dcgen/src/issuer/stats.rs +++ b/vey-dcgen/src/issuer/stats.rs @@ -1,6 +1,7 @@ /* * SPDX-License-Identifier: Apache-2.0 * SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates. + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ use std::sync::atomic::{AtomicU64, Ordering}; diff --git a/vey-dcgen/src/stat/metrics/issuer.rs b/vey-dcgen/src/stat/metrics/issuer.rs index b5796c7f8..d20877659 100644 --- a/vey-dcgen/src/stat/metrics/issuer.rs +++ b/vey-dcgen/src/stat/metrics/issuer.rs @@ -1,6 +1,7 @@ /* * SPDX-License-Identifier: Apache-2.0 * SPDX-FileCopyrightText: 2023-2025 ByteDance and/or its affiliates. + * SPDX-FileCopyrightText: 2026 VEY-OSS Developers. */ use vey_daemon::metrics::TAG_KEY_QUANTILE; diff --git a/vey-gateway/CHANGELOG b/vey-gateway/CHANGELOG index 5daf77360..1025b7d37 100644 --- a/vey-gateway/CHANGELOG +++ b/vey-gateway/CHANGELOG @@ -1,25 +1,34 @@ v0.4.1: - - BUG FIX: a link-local IPv6 peer or local address keeps its scope id - - BUG FIX: after graceful shutdown is cancelled, a later SIGTERM still requests shutdown - - Breaking: the default control socket directory is `/run/vey-gateway` for root on Linux and `/var/run/vey-gateway` for root on other Unix systems; another Linux user uses an absolute `XDG_RUNTIME_DIR` or `/run/user//vey-gateway` - BUG FIX: a buffered stream copy or vectored write stops when the writer returns no bytes - BUG FIX: a Fluentd handshake reports a closed connection instead of parsing a short HELO or PONG - BUG FIX: a large Linux journald record can be sealed and sent + - BUG FIX: a link-local IPv6 peer or local address keeps its scope id + - BUG FIX: a log format failure is reported instead of panicking, and syslog is sent before the reconnect wait - BUG FIX: a non-ASCII size string in config is rejected instead of panicking + - BUG FIX: a quit during graceful shutdown closes the unique control listener - BUG FIX: a relative pid file is created in the working directory from before daemonizing, and the lock is held until the process exits - BUG FIX: a rotating histogram still publishes while samples keep arriving, and `0.0` and `-0.0` count as one quantile + - BUG FIX: a statsd export continues after `sendmmsg` writes only part of the batch, and logs the first error in each 64 second slice + - BUG FIX: a TCP or UDP listen `scale` whose denominator is 0 is rejected instead of panicking - BUG FIX: a timestamp offset that includes seconds is written in UTC - BUG FIX: a TLS Fluentd client finishes the forward handshake after TLS, and an unknown HELO option is rejected + - BUG FIX: a TLS session ticket that is too short to hold an HMAC tag is ignored instead of panicking - BUG FIX: a UDP datagram that fills the byte window exactly is allowed through + - BUG FIX: after graceful shutdown is cancelled, a later SIGTERM still requests shutdown - BUG FIX: an empty daemon group uses `_.sock` as the control socket - BUG FIX: an incomplete legacy rustls certificate pair fails config load instead of being ignored - BUG FIX: an `openssl_proxy` ClientHello whose handshake header arrives in pieces no longer consumes the next handshake message + - BUG FIX: failure to spawn the async log thread is reported + - BUG FIX: IPv4 and IPv6 reuseport listeners use separate eBPF maps, so `0.0.0.0` and `::` no longer share one - BUG FIX: on Windows, another control client can connect while one connection is still open - BUG FIX: OpenSSL async engine file descriptors are registered with the I/O-safe AsyncFd API - BUG FIX: reloading a QUIC listener keeps the new socket buffer size + - BUG FIX: the TLS ticket key still in use can decrypt tickets after a refresh - BUG FIX: the Windows hostname sent to syslog and Fluentd no longer includes a trailing NUL - BUG FIX: UDP receive ancillary data is aligned so control messages can be parsed + - Breaking: the default control socket directory is `/run/vey-gateway` for root on Linux and `/var/run/vey-gateway` for root on other Unix systems; another Linux user uses an absolute `XDG_RUNTIME_DIR` or `/run/user//vey-gateway` + - Compatibility: a reuseport listener does not join eBPF maps pinned by a previous release - Compatibility: bump MSRV to 1.94.0 - Compatibility: DragonFly worker `sched_affinity` applies to the calling thread diff --git a/vey-iploc/CHANGELOG b/vey-iploc/CHANGELOG index a955aed1e..71a9085bb 100644 --- a/vey-iploc/CHANGELOG +++ b/vey-iploc/CHANGELOG @@ -1,23 +1,30 @@ v0.5.0: - - Breaking: listen sockets are `server` entries. UDP uses one reuseport socket per worker when `worker` is set, and one socket on the main runtime otherwise. Unix datagram listen is supported. `UDP_LISTEN_ADDR` and `-G port` no longer select the listen address. `-G` still names the instance and its control socket - - Feature: `vey-iploc-ctl reload` re-reads `server` and `geoip_db`, restarts changed listeners, and swaps the running databases - - Feature: `vey-iploc-ctl query ` looks up one address in the running databases - - Feature: a valid lookup that matches neither database increments `server.lookup_not_found` - - Feature: the packaged build includes the eBPF UDP reuseport selector used for a lossless upgrade. Root attaches it unless `use_ebpf` is false - - BUG FIX: after graceful shutdown is cancelled, a later SIGTERM still requests shutdown - - Breaking: the default control socket directory is `/run/vey-iploc` for root on Linux and `/var/run/vey-iploc` for root on other Unix systems; another Linux user uses an absolute `XDG_RUNTIME_DIR` or `/run/user//vey-iploc` - BUG FIX: a `.GZ` ASN database is decompressed, CSV lines are trimmed, and a prefix with host bits set is still loaded - BUG FIX: a country code that is not two letters is rejected instead of panicking - BUG FIX: a large Linux journald record can be sealed and sent + - BUG FIX: a log format failure is reported instead of panicking, and syslog is sent before the reconnect wait - BUG FIX: a non-ASCII size string in worker or statsd config is rejected instead of panicking + - BUG FIX: a quit during graceful shutdown closes the unique control listener - BUG FIX: a relative pid file is created in the working directory from before daemonizing, and the lock is held until the process exits + - BUG FIX: a statsd export continues after `sendmmsg` writes only part of the batch, and logs the first error in each 64 second slice - BUG FIX: a timestamp offset that includes seconds is written in UTC + - BUG FIX: a UDP listen `scale` whose denominator is 0 is rejected instead of panicking + - BUG FIX: after graceful shutdown is cancelled, a later SIGTERM still requests shutdown - BUG FIX: an empty daemon group uses `_.sock` as the control socket - BUG FIX: an IP location lookup returns an error if the query runtime has quit, instead of panicking - BUG FIX: continent and country counts include the last code + - BUG FIX: failure to spawn the async log thread is reported + - BUG FIX: IPv4 and IPv6 reuseport listeners use separate eBPF maps, so `0.0.0.0` and `::` no longer share one - BUG FIX: on Windows, another control client can connect while one connection is still open - BUG FIX: the Windows hostname sent to syslog no longer includes a trailing NUL + - Feature: a valid lookup that matches neither database increments `server.lookup_not_found` + - Feature: the packaged build includes the eBPF UDP reuseport selector used for a lossless upgrade. Root attaches it unless `use_ebpf` is false + - Feature: `vey-iploc-ctl query ` looks up one address in the running databases + - Feature: `vey-iploc-ctl reload` re-reads `server` and `geoip_db`, restarts changed listeners, and swaps the running databases + - Breaking: listen sockets are `server` entries. UDP uses one reuseport socket per worker when `worker` is set, and one socket on the main runtime otherwise. Unix datagram listen is supported. `UDP_LISTEN_ADDR` and `-G port` no longer select the listen address. `-G` still names the instance and its control socket + - Breaking: the default control socket directory is `/run/vey-iploc` for root on Linux and `/var/run/vey-iploc` for root on other Unix systems; another Linux user uses an absolute `XDG_RUNTIME_DIR` or `/run/user//vey-iploc` + - Compatibility: a reuseport listener does not join eBPF maps pinned by a previous release - Compatibility: bump MSRV to 1.94.0 - Compatibility: DragonFly worker `sched_affinity` applies to the calling thread - Compatibility: published container images set OCI labels so GHCR can describe them diff --git a/vey-keyless/CHANGELOG b/vey-keyless/CHANGELOG index b2b473f6e..3fc15b509 100644 --- a/vey-keyless/CHANGELOG +++ b/vey-keyless/CHANGELOG @@ -1,34 +1,42 @@ v0.6.0: - - BUG FIX: a link-local IPv6 peer or local address keeps its scope id - - BUG FIX: after graceful shutdown is cancelled, a later SIGTERM still requests shutdown - - Breaking: the default control socket directory is `/run/vey-keyless` for root on Linux and `/var/run/vey-keyless` for root on other Unix systems; another Linux user uses an absolute `XDG_RUNTIME_DIR` or `/run/user//vey-keyless` - BUG FIX: a buffered stream copy or vectored write stops when the writer returns no bytes - BUG FIX: a Fluentd handshake reports a closed connection instead of parsing a short HELO or PONG - BUG FIX: a large Linux journald record can be sealed and sent + - BUG FIX: a link-local IPv6 peer or local address keeps its scope id + - BUG FIX: a log format failure is reported instead of panicking, and syslog is sent before the reconnect wait - BUG FIX: a non-ASCII size string in worker or statsd config is rejected instead of panicking + - BUG FIX: a QAT instance drains in-flight operations before it stops, and a failed start is not remembered for the rest of the process + - BUG FIX: a quit during graceful shutdown closes the unique control listener - BUG FIX: a relative pid file is created in the working directory from before daemonizing, and the lock is held until the process exits - BUG FIX: a rotating histogram still publishes while samples keep arriving, and `0.0` and `-0.0` count as one quantile + - BUG FIX: a statsd export continues after `sendmmsg` writes only part of the batch, and logs the first error in each 64 second slice + - BUG FIX: a TCP listen `scale` whose denominator is 0 is rejected instead of panicking - BUG FIX: a TLS Fluentd client finishes the forward handshake after TLS, and an unknown HELO option is rejected + - BUG FIX: a TLS session ticket that is too short to hold an HMAC tag is ignored instead of panicking + - BUG FIX: after graceful shutdown is cancelled, a later SIGTERM still requests shutdown - BUG FIX: an empty daemon group uses `_.sock` as the control socket - BUG FIX: an incomplete legacy rustls certificate pair fails config load instead of being ignored - BUG FIX: an issued certificate cannot start before its issuer, and its `pathlen` cannot exceed what the issuer allows - BUG FIX: certificate times outside 1950-2049 use GeneralizedTime, and a timestamp offset that includes seconds is written in UTC + - BUG FIX: failure to spawn the async log thread is reported - BUG FIX: on Windows, another control client can connect while one connection is still open - BUG FIX: OpenSSL async engine file descriptors are registered with the I/O-safe AsyncFd API - BUG FIX: the crypto_mb backend reduces an ECDSA digest modulo the curve order, and uses OpenSSL when that value is zero - BUG FIX: the crypto_mb backend rejects an RSA-PSS digest that is not the hash length - BUG FIX: the register keepalive request is a complete HTTP header + - BUG FIX: the TLS ticket key still in use can decrypt tickets after a refresh - BUG FIX: the Windows hostname sent to syslog and Fluentd no longer includes a trailing NUL + - Feature: dispatch Ed25519 private-key operations to worker backends + - Feature: `listen` is now optional in a `cloudflare` server, so it can serve only the connections sent by port servers + - Feature: optional `crypto_mb` backend (feature `crypto-mb`, x86_64 only) using Intel crypto_mb for batched RSA-2048/3072/4096, ECDSA P-256/P-384/P-521, and Ed25519; single-request batches fall back to OpenSSL + - Feature: port servers support the PROXY Protocol via `proxy_protocol` and `proxy_protocol_read_timeout` + - Feature: `server` references are verified when the config is loaded, so a missing or self-referencing next server is rejected at startup - Feature: support multiple server types, selected with the `type` key in each server config - `cloudflare`: the Cloudflare Keyless server, which is the default type if `type` is omitted - `plain_tcp_port`: a plain TCP port that forwards accepted connections to the `server` it points to - `plain_tls_port`: a TLS port backed by rustls, which terminates TLS and forwards the stream to the `server` it points to - - Feature: `listen` is now optional in a `cloudflare` server, so it can serve only the connections sent by port servers - - Feature: port servers support the PROXY Protocol via `proxy_protocol` and `proxy_protocol_read_timeout` - - Feature: `server` references are verified when the config is loaded, so a missing or self-referencing next server is rejected at startup - - Feature: optional `crypto_mb` backend (feature `crypto-mb`, x86_64 only) using Intel crypto_mb for batched RSA-2048/3072/4096, ECDSA P-256/P-384/P-521, and Ed25519; single-request batches fall back to OpenSSL - - Feature: dispatch Ed25519 private-key operations to worker backends + - Breaking: the default control socket directory is `/run/vey-keyless` for root on Linux and `/var/run/vey-keyless` for root on other Unix systems; another Linux user uses an absolute `XDG_RUNTIME_DIR` or `/run/user//vey-keyless` - Compatibility: bump MSRV to 1.94.0 - Compatibility: DragonFly worker `sched_affinity` applies to the calling thread diff --git a/vey-proxy/CHANGELOG b/vey-proxy/CHANGELOG index 16ec48b6a..f6660c1eb 100644 --- a/vey-proxy/CHANGELOG +++ b/vey-proxy/CHANGELOG @@ -3,46 +3,65 @@ CHANGELOG Release notes for `vey-proxy`, ordered from newest to oldest. v1.15.2: - - BUG FIX: IMAP interception reads a trailing literal on responses that carry strings, accepts an empty AUTHENTICATE challenge, and treats a tag that starts with ``*`` or ``+`` as that command's result - - BUG FIX: an IMAP BAD reply stays on one line when the tag contains a control character - - BUG FIX: SMTP DATA dot-stuffing leaves a ``.`` that continues the same line unchanged - - BUG FIX: an SMTP blocked-message reason that contains a line break stays on one reply - - BUG FIX: an HTTP log URI redacts the password when the userinfo contains another ``@`` - - BUG FIX: a crypt password hash is rejected when a character is outside the alphabet or the last character has unused high bits set - - BUG FIX: DNS-over-HTTPS releases the HTTP/2 receive window, and a DNS-over-HTTPS or DNS-over-HTTP/3 response body stops at 65535 bytes - - BUG FIX: a link-local IPv6 peer or local address keeps its scope id - - BUG FIX: after graceful shutdown is cancelled, a later SIGTERM still requests shutdown - - Feature: UDP stream and transparent UDP listeners emit ``listen.datagram.recv.*`` and ``listen.datagram.send.*`` - - Breaking: the default control socket directory is ``/run/vey-proxy`` for root on Linux and ``/var/run/vey-proxy`` for root on other Unix systems; another Linux user uses an absolute ``XDG_RUNTIME_DIR`` or ``/run/user//vey-proxy`` - BUG FIX: a buffered stream copy or vectored write stops when the writer returns no bytes - BUG FIX: a cache lookup returns an error if the query runtime has quit, instead of panicking - BUG FIX: a ClientHello whose handshake header arrives in pieces no longer consumes the next handshake message - BUG FIX: a country code that is not two letters is rejected instead of panicking + - BUG FIX: a crypt password hash is rejected when a character is outside the alphabet or the last character has unused high bits set - BUG FIX: a Fluentd handshake reports a closed connection instead of parsing a short HELO or PONG - BUG FIX: a large Linux journald record can be sealed and sent + - BUG FIX: a link-local IPv6 peer or local address keeps its scope id + - BUG FIX: a log format failure is reported instead of panicking, and syslog is sent before the reconnect wait - BUG FIX: a non-ASCII size string in YAML or JSON config is rejected instead of panicking + - BUG FIX: a quit during graceful shutdown closes the unique control listener + - BUG FIX: a Redis handshake times out, and a later resolved address is tried when an earlier TCP connection fails - BUG FIX: a relative pid file is created in the working directory from before daemonizing, and the lock is held until the process exits - BUG FIX: a rotating histogram still publishes while samples keep arriving, and ``0.0`` and ``-0.0`` count as one quantile + - BUG FIX: a statsd export continues after ``sendmmsg`` writes only part of the batch, and logs the first error in each 64 second slice - BUG FIX: a successful HTTP CONNECT or ``101`` Upgrade response is rejected when it carries a message body + - BUG FIX: a TCP or UDP listen ``scale`` whose denominator is 0 is rejected instead of panicking - BUG FIX: a TLS Fluentd client finishes the forward handshake after TLS, and an unknown HELO option is rejected + - BUG FIX: a TLS session ticket that is too short to hold an HMAC tag is ignored instead of panicking + - BUG FIX: a transparent IPv6 listener is IPv6-only - BUG FIX: a UDP datagram that fills the byte window exactly is allowed through + - BUG FIX: a UDP dump larger than one datagram is split, and a failed send does not stop later dumps + - BUG FIX: after graceful shutdown is cancelled, a later SIGTERM still requests shutdown - BUG FIX: an empty daemon group uses ``_.sock`` as the control socket - BUG FIX: an FTP LIST waits until the control connection is readable before reading the closing reply + - BUG FIX: an HTTP chunk size line longer than the limit is rejected even when the newline arrives in the same read + - BUG FIX: an HTTP log URI redacts the password when the userinfo contains another ``@`` - BUG FIX: an HTTP or HTTP/2 chunked transfer stops when a write returns no bytes + - BUG FIX: an IMAP BAD reply stays on one line when the tag contains a control character - BUG FIX: an incomplete legacy rustls certificate pair fails config load instead of being ignored + - BUG FIX: an IP location client keeps querying after a UDP receive error, and a cached prefix is replaced when the answer is a longer match - BUG FIX: an issued certificate cannot start before its issuer, and its ``pathlen`` cannot exceed what the issuer allows - BUG FIX: an MQTT CONNECT whose remaining length is 128 or larger is recognized - BUG FIX: an NNTP greeting that arrives before its newline is still recognized, and an NNSP greeting is reported as NNSP + - BUG FIX: an OpenSSL async client engine stays registered after the handshake and no longer busy-waits + - BUG FIX: an SMTP blocked-message reason that contains a line break stays on one reply + - BUG FIX: an SMTP IPv6 address literal ``[IPv6:...]`` is accepted, and the tag match ignores case + - BUG FIX: an unknown key in a weighted metric node is rejected - BUG FIX: certificate times outside 1950-2049 use GeneralizedTime, and a timestamp offset that includes seconds is written in UTC + - BUG FIX: DNS-over-HTTPS releases the HTTP/2 receive window, and a DNS-over-HTTPS or DNS-over-HTTP/3 response body stops at 65535 bytes + - BUG FIX: failure to spawn the async log thread is reported + - BUG FIX: ``forward-ftp`` enables FTP GET, PUT, and DELETE forwarding, and an ALPN match without a backend name is rejected - BUG FIX: FTP control replies drop only the CR/LF ending, so trailing spaces in the text stay - BUG FIX: FTP stops the next passive mode after the control connection fails, rejects a command parameter that contains a line break, and accepts a blank FEAT end line + - BUG FIX: IMAP interception reads a trailing literal on responses that carry strings, accepts an empty AUTHENTICATE challenge, and treats a tag that starts with ``*`` or ``+`` as that command's result + - BUG FIX: IPv4 and IPv6 reuseport listeners use separate eBPF maps, so ``0.0.0.0`` and ``::`` no longer share one - BUG FIX: on Windows, another control client can connect while one connection is still open - BUG FIX: OpenSSL async engine file descriptors are registered with the I/O-safe AsyncFd API - BUG FIX: protocol inspection recognizes the WebDAV method ``UPDATEREDIRECTREF`` - BUG FIX: reject too large HTTP and ICAP status code - BUG FIX: reloading a UDP or QUIC listener keeps the new socket buffer size + - BUG FIX: SMTP DATA dot-stuffing leaves a ``.`` that continues the same line unchanged + - BUG FIX: the TLS ticket key still in use can decrypt tickets after a refresh - BUG FIX: the Windows hostname sent to syslog and Fluentd no longer includes a trailing NUL - BUG FIX: UDP receive ancillary data is aligned so control messages can be parsed + - Feature: an IP location service can be reached over a Unix datagram socket + - Feature: UDP stream and transparent UDP listeners emit ``listen.datagram.recv.*`` and ``listen.datagram.send.*`` + - Breaking: the default control socket directory is ``/run/vey-proxy`` for root on Linux and ``/var/run/vey-proxy`` for root on other Unix systems; another Linux user uses an absolute ``XDG_RUNTIME_DIR`` or ``/run/user//vey-proxy`` + - Compatibility: a reuseport listener does not join eBPF maps pinned by a previous release - Compatibility: DragonFly worker ``sched_affinity`` applies to the calling thread - Compatibility: published container images set OCI labels so GHCR can describe them diff --git a/vey-statsd/CHANGELOG b/vey-statsd/CHANGELOG index 37b3ed090..29546fd0b 100644 --- a/vey-statsd/CHANGELOG +++ b/vey-statsd/CHANGELOG @@ -1,16 +1,21 @@ v0.3.1: - - BUG FIX: after graceful shutdown is cancelled, a later SIGTERM still requests shutdown - - Breaking: the default control socket directory is `/run/vey-statsd` for root on Linux and `/var/run/vey-statsd` for root on other Unix systems; another Linux user uses an absolute `XDG_RUNTIME_DIR` or `/run/user//vey-statsd` - BUG FIX: a buffered stream copy or vectored write stops when the writer returns no bytes - BUG FIX: a large Linux journald record can be sealed and sent + - BUG FIX: a log format failure is reported instead of panicking, and syslog is sent before the reconnect wait - BUG FIX: a non-ASCII size string in config is rejected instead of panicking + - BUG FIX: a quit during graceful shutdown closes the unique control listener - BUG FIX: a relative pid file is created in the working directory from before daemonizing, and the lock is held until the process exits - BUG FIX: a timestamp offset that includes seconds is written in UTC + - BUG FIX: a UDP listen `scale` whose denominator is 0 is rejected instead of panicking + - BUG FIX: after graceful shutdown is cancelled, a later SIGTERM still requests shutdown - BUG FIX: an empty daemon group uses `_.sock` as the control socket + - BUG FIX: an HTTP chunk size line longer than the limit is rejected even when the newline arrives in the same read - BUG FIX: an HTTP chunked write stops when the writer returns no bytes + - BUG FIX: failure to spawn the async log thread is reported - BUG FIX: on Windows, another control client can connect while one connection is still open - BUG FIX: the Windows hostname sent to syslog no longer includes a trailing NUL + - Breaking: the default control socket directory is `/run/vey-statsd` for root on Linux and `/var/run/vey-statsd` for root on other Unix systems; another Linux user uses an absolute `XDG_RUNTIME_DIR` or `/run/user//vey-statsd` - Compatibility: DragonFly worker `sched_affinity` applies to the calling thread - Compatibility: published container images set OCI labels so GHCR can describe them