From 9522860c0daf135e8dc3ce4380a62a526109beb8 Mon Sep 17 00:00:00 2001 From: Zhang Jingqiang Date: Tue, 20 Jan 2026 23:29:08 +0800 Subject: [PATCH] g3proxy: add ldap user group --- doc/standards.md | 6 + .../ldap_user_auth/dynamic_users.json | 5 + g3proxy/examples/ldap_user_auth/g3proxy.yaml | 58 ++++ g3proxy/src/auth/group/ldap/mod.rs | 136 +++++++++ g3proxy/src/auth/group/ldap/pool/connect.rs | 166 +++++++++++ g3proxy/src/auth/group/ldap/pool/mod.rs | 181 ++++++++++++ g3proxy/src/auth/group/ldap/pool/task.rs | 207 +++++++++++++ .../src/auth/group/ldap/protocol/message.rs | 80 +++++ g3proxy/src/auth/group/ldap/protocol/mod.rs | 10 + .../src/auth/group/ldap/protocol/request.rs | 122 ++++++++ g3proxy/src/auth/group/mod.rs | 35 ++- g3proxy/src/auth/mod.rs | 2 + g3proxy/src/auth/user.rs | 35 ++- g3proxy/src/config/auth/group/basic.rs | 2 +- g3proxy/src/config/auth/group/ldap.rs | 168 +++++++++++ g3proxy/src/config/auth/group/mod.rs | 4 + g3proxy/src/config/auth/mod.rs | 7 +- .../serve/http_proxy/task/pipeline/writer.rs | 18 +- .../serve/http_rproxy/task/pipeline/writer.rs | 20 +- .../socks_proxy/task/negotiation/task.rs | 15 +- lib/g3-dpi/src/parser/quic/frame/ack.rs | 28 +- lib/g3-dpi/src/parser/quic/frame/crypto.rs | 8 +- lib/g3-dpi/src/parser/quic/frame/mod.rs | 2 + lib/g3-dpi/src/parser/quic/mod.rs | 3 + lib/g3-dpi/src/parser/quic/packet/mod.rs | 6 +- lib/g3-dpi/src/parser/quic/packet/v1.rs | 8 +- lib/g3-dpi/src/parser/quic/packet/v2.rs | 8 +- .../net => g3-dpi/src/parser}/quic/var_int.rs | 28 +- lib/g3-dpi/src/protocol/ldap.rs | 13 +- lib/g3-types/src/auth/error.rs | 4 + lib/g3-types/src/codec/ber/integer.rs | 251 ++++++++++++++++ lib/g3-types/src/codec/ber/length.rs | 274 ++++++++++++++++++ lib/g3-types/src/codec/ber/mod.rs | 10 + lib/g3-types/src/codec/ldap/length.rs | 72 +++++ lib/g3-types/src/codec/ldap/message.rs | 110 +++++++ lib/g3-types/src/codec/ldap/message_id.rs | 85 ++++++ lib/g3-types/src/codec/ldap/mod.rs | 19 ++ lib/g3-types/src/codec/ldap/result.rs | 107 +++++++ lib/g3-types/src/codec/ldap/sequence.rs | 117 ++++++++ lib/g3-types/src/codec/mod.rs | 6 + lib/g3-types/src/net/host.rs | 4 +- lib/g3-types/src/net/ldap/message/id.rs | 147 ---------- lib/g3-types/src/net/ldap/message/length.rs | 199 ------------- lib/g3-types/src/net/ldap/message/mod.rs | 10 - lib/g3-types/src/net/ldap/mod.rs | 9 - lib/g3-types/src/net/mod.rs | 4 - lib/g3-types/src/net/proxy/http.rs | 2 +- lib/g3-types/src/net/proxy/socks4.rs | 2 +- lib/g3-types/src/net/proxy/socks5.rs | 2 +- lib/g3-types/src/net/quic/mod.rs | 7 - lib/g3-types/src/net/upstream.rs | 17 +- .../configuration/auth/group/facts.rst | 4 +- .../configuration/auth/group/index.rst | 1 + .../g3proxy/configuration/auth/group/ldap.rst | 106 +++++++ 54 files changed, 2468 insertions(+), 482 deletions(-) create mode 100644 g3proxy/examples/ldap_user_auth/dynamic_users.json create mode 100644 g3proxy/examples/ldap_user_auth/g3proxy.yaml create mode 100644 g3proxy/src/auth/group/ldap/mod.rs create mode 100644 g3proxy/src/auth/group/ldap/pool/connect.rs create mode 100644 g3proxy/src/auth/group/ldap/pool/mod.rs create mode 100644 g3proxy/src/auth/group/ldap/pool/task.rs create mode 100644 g3proxy/src/auth/group/ldap/protocol/message.rs create mode 100644 g3proxy/src/auth/group/ldap/protocol/mod.rs create mode 100644 g3proxy/src/auth/group/ldap/protocol/request.rs create mode 100644 g3proxy/src/config/auth/group/ldap.rs rename lib/{g3-types/src/net => g3-dpi/src/parser}/quic/var_int.rs (73%) create mode 100644 lib/g3-types/src/codec/ber/integer.rs create mode 100644 lib/g3-types/src/codec/ber/length.rs create mode 100644 lib/g3-types/src/codec/ber/mod.rs create mode 100644 lib/g3-types/src/codec/ldap/length.rs create mode 100644 lib/g3-types/src/codec/ldap/message.rs create mode 100644 lib/g3-types/src/codec/ldap/message_id.rs create mode 100644 lib/g3-types/src/codec/ldap/mod.rs create mode 100644 lib/g3-types/src/codec/ldap/result.rs create mode 100644 lib/g3-types/src/codec/ldap/sequence.rs delete mode 100644 lib/g3-types/src/net/ldap/message/id.rs delete mode 100644 lib/g3-types/src/net/ldap/message/length.rs delete mode 100644 lib/g3-types/src/net/ldap/message/mod.rs delete mode 100644 lib/g3-types/src/net/ldap/mod.rs delete mode 100644 lib/g3-types/src/net/quic/mod.rs create mode 100644 sphinx/g3proxy/configuration/auth/group/ldap.rst diff --git a/doc/standards.md b/doc/standards.md index d25a0d94..6c5a9c13 100644 --- a/doc/standards.md +++ b/doc/standards.md @@ -513,6 +513,12 @@ The code should comply to these, but should be more compliant to existing popula - [rfc4511](https://datatracker.ietf.org/doc/html/rfc4511) : Lightweight Directory Access Protocol (LDAP): The Protocol + - [rfc4513](https://datatracker.ietf.org/doc/html/rfc4513) + : Lightweight Directory Access Protocol (LDAP): Authentication Methods and Security Mechanisms + - [rfc4516](https://datatracker.ietf.org/doc/html/rfc4516) + : Lightweight Directory Access Protocol (LDAP): Uniform Resource Locator + - [rfc4519](https://datatracker.ietf.org/doc/html/rfc4519) + : Lightweight Directory Access Protocol (LDAP): Schema for User Applications ## MQTT diff --git a/g3proxy/examples/ldap_user_auth/dynamic_users.json b/g3proxy/examples/ldap_user_auth/dynamic_users.json new file mode 100644 index 00000000..6d5a8139 --- /dev/null +++ b/g3proxy/examples/ldap_user_auth/dynamic_users.json @@ -0,0 +1,5 @@ +[ + { + "name": "euler" + } +] \ No newline at end of file diff --git a/g3proxy/examples/ldap_user_auth/g3proxy.yaml b/g3proxy/examples/ldap_user_auth/g3proxy.yaml new file mode 100644 index 00000000..68d41f8d --- /dev/null +++ b/g3proxy/examples/ldap_user_auth/g3proxy.yaml @@ -0,0 +1,58 @@ +--- + +log: journal + +user_group: + - name: default + type: ldap + ldap_url: ldap://ldap.forumsys.com/dc=example,dc=com + pool: + min_idle_count: 1 + static_users: + - name: gauss + dst_port_filter: + - 80 + - 443 + dst_host_filter_set: + exact: + # for ipinfo.io + - ipinfo.io + - 1.1.1.1 + child: + # for myip.ipip.net + - "ipip.net" + regex: + # for lumtest.com/myip.json + - "lum[a-z]*[.]com$" + source: + type: file + path: dynamic_users.json + unmanaged_user: + name: unmanaged + +server: + - name: socks + escaper: default + user_group: default + type: socks_proxy + enable_udp_associate: true + listen: + address: "[::]:11080" + - name: http + escaper: default + user_group: default + type: http_proxy + listen: + address: "[::]:13128" + +resolver: + - name: default + type: c-ares + +escaper: + - name: default + type: direct_fixed + resolver: default + resolve_strategy: IPv4First + tcp_sock_speed_limit: 80M + udp_sock_speed_limit: 10M diff --git a/g3proxy/src/auth/group/ldap/mod.rs b/g3proxy/src/auth/group/ldap/mod.rs new file mode 100644 index 00000000..89e44299 --- /dev/null +++ b/g3proxy/src/auth/group/ldap/mod.rs @@ -0,0 +1,136 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +use std::collections::hash_map::Entry; +use std::sync::{Arc, Mutex}; + +use ahash::AHashMap; +use arc_swap::ArcSwapOption; +use arcstr::ArcStr; + +use g3_types::auth::{Password, UserAuthError}; +use g3_types::metrics::{MetricTagMap, NodeName}; + +use super::BaseUserGroup; +use crate::auth::{User, UserContext, UserType}; +use crate::config::auth::{LdapUserGroupConfig, UserGroupConfig}; + +mod protocol; +use protocol::{LdapMessageReceiver, SimpleBindRequestEncoder}; + +mod pool; +use pool::{LdapAuthPool, LdapAuthPoolHandle}; + +pub(crate) struct LdapUserGroup { + base: BaseUserGroup, + pool_handle: LdapAuthPoolHandle, + unmanaged_users: Mutex>>, +} + +impl LdapUserGroup { + pub(super) fn base(&self) -> &BaseUserGroup { + &self.base + } + + pub(super) fn clone_config(&self) -> LdapUserGroupConfig { + (*self.base.config).clone() + } + + fn new(base: BaseUserGroup) -> anyhow::Result { + let pool_handle = LdapAuthPool::create(base.config.clone())?; + Ok(LdapUserGroup { + base, + pool_handle, + unmanaged_users: Default::default(), + }) + } + + pub(super) async fn new_with_config(config: LdapUserGroupConfig) -> anyhow::Result> { + let base = BaseUserGroup::new_with_config(config).await?; + Self::new(base).map(Arc::new) + } + + pub(super) fn reload(&self, config: LdapUserGroupConfig) -> anyhow::Result> { + let base = self.base.reload(config)?; + Self::new(base).map(Arc::new) + } + + pub(super) async fn check_user_with_password( + &self, + username: &str, + password: &Password, + server_name: &NodeName, + server_extra_tags: &Arc>, + ) -> Result { + match &self.base.config.unmanaged_user { + Some(unmanaged_user_config) => { + self.pool_handle + .check_username_password(username, password.as_original()) + .await?; + + if let Some((user, user_type)) = self.base.get_user(username) { + return Ok(UserContext::new( + Some(username.into()), + user, + user_type, + server_name, + server_extra_tags, + )); + } + + let mut ht = self.unmanaged_users.lock().unwrap(); + match ht.entry(username.into()) { + Entry::Occupied(o) => { + let user = o.get().clone(); + Ok(UserContext::new( + Some(username.into()), + user.clone(), + UserType::Unmanaged, + server_name, + server_extra_tags, + )) + } + Entry::Vacant(v) => { + let username = ArcStr::from(username); + + let user = User::new_unmanaged( + &username, + self.base.config.basic_config().name(), + unmanaged_user_config, + ) + .map_err(|_| UserAuthError::NoSuchUser)?; + let user = Arc::new(user); + + v.insert(user.clone()); + + Ok(UserContext::new( + Some(username), + user.clone(), + UserType::Unmanaged, + server_name, + server_extra_tags, + )) + } + } + } + None => { + if let Some((user, user_type)) = self.base.get_user(username) { + self.pool_handle + .check_username_password(username, password.as_original()) + .await?; + Ok(UserContext::new( + Some(username.into()), + user, + user_type, + server_name, + server_extra_tags, + )) + } else { + Err(UserAuthError::NoSuchUser) + } + } + } + } +} diff --git a/g3proxy/src/auth/group/ldap/pool/connect.rs b/g3proxy/src/auth/group/ldap/pool/connect.rs new file mode 100644 index 00000000..f8445ab2 --- /dev/null +++ b/g3proxy/src/auth/group/ldap/pool/connect.rs @@ -0,0 +1,166 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +use std::net::SocketAddr; +use std::sync::Arc; + +use anyhow::{Context, anyhow}; +use tokio::net::TcpStream; + +use g3_io_ext::LimitedWriteExt; +use g3_io_ext::openssl::MaybeSslStream; +use g3_openssl::{SslConnector, SslStream}; +use g3_socket::BindAddr; +use g3_types::codec::{LdapResult, LdapSequence}; +use g3_types::net::{Host, OpensslClientConfig}; + +use crate::auth::group::ldap::LdapMessageReceiver; +use crate::config::auth::LdapUserGroupConfig; + +pub(super) struct LdapTcpConnector { + config: Arc, +} + +pub(super) struct LdapTlsConnector { + config: Arc, + tls_client: OpensslClientConfig, +} + +pub(super) enum LdapConnector { + Tcp(LdapTcpConnector), + Tls(LdapTlsConnector), + StartTls(LdapTlsConnector), +} + +impl LdapConnector { + pub(super) fn new(config: Arc) -> anyhow::Result { + let connector = match &config.tls_client { + Some(builder) => { + let tls_client = builder.build().context("failed to build tls client")?; + if config.direct_tls { + LdapConnector::Tls(LdapTlsConnector { config, tls_client }) + } else { + LdapConnector::StartTls(LdapTlsConnector { config, tls_client }) + } + } + None => LdapConnector::Tcp(LdapTcpConnector { config }), + }; + Ok(connector) + } + + pub(super) async fn connect(&self) -> anyhow::Result> { + match self { + LdapConnector::Tcp(c) => { + let stream = c.connect().await?; + Ok(MaybeSslStream::Plain(stream)) + } + LdapConnector::Tls(c) => { + let ssl_stream = c.direct_connect().await?; + Ok(MaybeSslStream::Ssl(ssl_stream)) + } + LdapConnector::StartTls(c) => { + let ssl_stream = c.starttls_connect().await?; + Ok(MaybeSslStream::Ssl(ssl_stream)) + } + } + } +} + +impl LdapTcpConnector { + async fn connect(&self) -> anyhow::Result { + let peer = match self.config.server.host() { + Host::Ip(ip) => SocketAddr::new(*ip, self.config.server.port()), + Host::Domain(domain) => { + let addrs = + tokio::net::lookup_host(format!("{domain}:{}", self.config.server.port())) + .await? + .collect::>(); + fastrand::choice(addrs) + .ok_or_else(|| anyhow!("no address resolved for domain {domain}"))? + } + }; + let socket = g3_socket::tcp::new_socket_to( + peer.ip(), + &BindAddr::None, + &Default::default(), + &Default::default(), + true, + ) + .map_err(|e| anyhow!("setup socket failed: {e}"))?; + tokio::time::timeout(self.config.connect_timeout, socket.connect(peer)) + .await + .map_err(|_| anyhow!("timed out connecting to peer {peer}"))? + .map_err(|e| anyhow!("can't connect to peer {peer}: {e}")) + } +} + +impl LdapTlsConnector { + async fn direct_connect(&self) -> anyhow::Result> { + let stream = self.tcp_connect().await?; + self.tls_handshake(stream).await + } + + async fn starttls_connect(&self) -> anyhow::Result> { + let mut stream = self.tcp_connect().await?; + self.starttls(&mut stream).await?; + self.tls_handshake(stream).await + } + + async fn tcp_connect(&self) -> anyhow::Result { + let tcp_connector = LdapTcpConnector { + config: self.config.clone(), + }; + tcp_connector.connect().await + } + + async fn tls_handshake(&self, stream: TcpStream) -> anyhow::Result> { + let ssl = self + .tls_client + .build_ssl(self.config.server.host(), self.config.server.port()) + .map_err(|e| anyhow!("build ssl context failed: {e}"))?; + let tls_connector = SslConnector::new(ssl, stream) + .map_err(|e| anyhow!("build ssl connector failed: {e}"))?; + tokio::time::timeout(self.tls_client.handshake_timeout, tls_connector.connect()) + .await + .map_err(|_| anyhow!("tls handshake with peer {} timed out", self.config.server))? + .map_err(|e| anyhow!("tls connect failed: {e}")) + } + + async fn starttls(&self, stream: &mut TcpStream) -> anyhow::Result<()> { + let starttls_message: [u8; _] = [ + 0x30, 0x1d, // Begin the LDAPMessage sequence + 0x02, 0x01, 0x7f, // The message ID (integer value 0x7f) + 0x77, 0x18, // Begin the extended request protocol op + 0x80, 0x16, // Begin the extended request OID "1.3.6.1.4.1.1466.20037" + b'1', b'.', b'3', b'.', b'6', b'.', b'1', b'.', b'4', b'.', b'1', b'.', b'1', b'4', + b'6', b'6', b'.', b'2', b'0', b'0', b'3', b'7', + ]; + stream + .write_all_flush(&starttls_message) + .await + .map_err(|e| anyhow!("failed to send StartTls extended request: {e}"))?; + + let mut rsp_receiver = LdapMessageReceiver::new(48); + + let rsp = tokio::time::timeout(self.config.response_timeout, rsp_receiver.recv(stream)) + .await + .map_err(|_| anyhow!("timed out when waiting for STARTTLS response"))? + .map_err(|e| anyhow!("failed to read StartTls response: {e}"))?; + + let rsp_sequence = LdapSequence::parse_extended_response(rsp.payload())?; + let data = rsp_sequence.data(); + let result = LdapResult::parse(data)?; + let left = &data[result.encoded_len()..]; + let oid = LdapSequence::parse_extended_response_oid(left)?; + if oid.data() == b"1.3.6.1.4.1.1466.20037" { + Err(anyhow!( + "unexpected StartTls response payload: {:?}", + rsp.payload() + )) + } else { + Ok(()) + } + } +} diff --git a/g3proxy/src/auth/group/ldap/pool/mod.rs b/g3proxy/src/auth/group/ldap/pool/mod.rs new file mode 100644 index 00000000..08bf03ab --- /dev/null +++ b/g3proxy/src/auth/group/ldap/pool/mod.rs @@ -0,0 +1,181 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use tokio::sync::{mpsc, oneshot}; + +use g3_types::auth::UserAuthError; + +use crate::config::auth::LdapUserGroupConfig; + +mod connect; +use connect::LdapConnector; + +mod task; +use task::LdapAuthTask; + +enum PoolCommand { + Exit, + NeedMoreConnection, + ConnectionClosed, +} + +struct LdapAuthRequest { + uid: String, + password: String, + retry: bool, + result_sender: oneshot::Sender>, +} + +pub(super) struct LdapAuthPoolHandle { + config: Arc, + req_sender: kanal::AsyncSender, + cmd_sender: mpsc::Sender, +} + +impl Drop for LdapAuthPoolHandle { + fn drop(&mut self) { + let _ = self.cmd_sender.try_send(PoolCommand::Exit); + } +} + +impl LdapAuthPoolHandle { + pub(super) async fn check_username_password( + &self, + username: &str, + password: &str, + ) -> Result<(), UserAuthError> { + let (sender, receiver) = oneshot::channel(); + let req = LdapAuthRequest { + uid: username.to_string(), + password: password.to_string(), + retry: true, + result_sender: sender, + }; + + if self.req_sender.is_full() { + let _ = self.cmd_sender.try_send(PoolCommand::NeedMoreConnection); + } + let _ = self.req_sender.send(req).await; + + match tokio::time::timeout(self.config.queue_wait_timeout, receiver).await { + Ok(Ok(Some(_))) => Ok(()), + Ok(Ok(None)) => Err(UserAuthError::TokenNotMatch), + Ok(Err(_)) => Err(UserAuthError::RemoteError), + Err(_) => Err(UserAuthError::RemoteTimeout), + } + } +} + +pub(super) struct LdapAuthPool { + config: Arc, + connector: Arc, + req_receiver: kanal::AsyncReceiver, + cmd_sender: mpsc::Sender, + cmd_receiver: mpsc::Receiver, + idle_conn_count: Arc, + expected_idle_count: usize, +} + +impl LdapAuthPool { + pub(super) fn create(config: Arc) -> anyhow::Result { + let connector = LdapConnector::new(config.clone())?; + let connector = Arc::new(connector); + + let (req_sender, req_receiver) = kanal::bounded_async(config.queue_channel_size); + let (cmd_sender, cmd_receiver) = mpsc::channel(config.connection_pool.max_idle_count()); + + let pool = LdapAuthPool { + config: config.clone(), + connector, + req_receiver, + cmd_sender: cmd_sender.clone(), + cmd_receiver, + idle_conn_count: Arc::new(AtomicUsize::new(0)), + expected_idle_count: config.connection_pool.min_idle_count(), + }; + tokio::spawn(async move { pool.into_running().await }); + + Ok(LdapAuthPoolHandle { + config, + req_sender, + cmd_sender, + }) + } + + async fn into_running(mut self) { + let mut check_interval = + tokio::time::interval(self.config.connection_pool.check_interval()); + + loop { + tokio::select! { + r = self.cmd_receiver.recv() => { + match r { + Some(PoolCommand::Exit) => { + return; + } + Some(PoolCommand::ConnectionClosed) => { + if self.idle_conn_count() < self.expected_idle_count { + self.create_connection(); + } + } + Some(PoolCommand::NeedMoreConnection) => { + if self.expected_idle_count < self.config.connection_pool.max_idle_count() { + self.expected_idle_count += 1; + } + if self.idle_conn_count() < self.config.connection_pool.max_idle_count() { + self.create_connection(); + } + } + None => { + return; + } + } + } + _ = check_interval.tick() => { + self.check(); + } + } + } + } + + fn idle_conn_count(&self) -> usize { + self.idle_conn_count.load(Ordering::Relaxed) + } + + fn check(&mut self) { + if self.expected_idle_count > self.config.connection_pool.min_idle_count() { + let decrease = + (self.config.connection_pool.min_idle_count() - self.expected_idle_count) / 4; + if decrease > 0 { + self.expected_idle_count -= decrease; + } else { + self.expected_idle_count -= 1; + } + } + let current_idle_count = self.idle_conn_count(); + if current_idle_count < self.expected_idle_count { + for _i in current_idle_count..self.expected_idle_count { + self.create_connection(); + } + } + } + + fn create_connection(&self) { + let task = LdapAuthTask::new(self.config.clone(), self.connector.clone()); + let idle_count = self.idle_conn_count.clone(); + let req_receiver = self.req_receiver.clone(); + let cmd_sender = self.cmd_sender.clone(); + + idle_count.fetch_add(1, Ordering::Relaxed); + tokio::spawn(async move { + task.run(req_receiver).await; + idle_count.fetch_sub(1, Ordering::Relaxed); + let _ = cmd_sender.send(PoolCommand::ConnectionClosed).await; + }); + } +} diff --git a/g3proxy/src/auth/group/ldap/pool/task.rs b/g3proxy/src/auth/group/ldap/pool/task.rs new file mode 100644 index 00000000..52b0a49c --- /dev/null +++ b/g3proxy/src/auth/group/ldap/pool/task.rs @@ -0,0 +1,207 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +use std::sync::Arc; + +use anyhow::anyhow; +use kanal::AsyncReceiver; +use log::{debug, warn}; +use tokio::io::{AsyncRead, AsyncWrite}; + +use g3_io_ext::openssl::MaybeSslStream; +use g3_io_ext::{AsyncStream, LimitedWriteExt}; +use g3_types::codec::{LdapResult, LdapSequence}; + +use super::{LdapAuthRequest, LdapConnector}; +use crate::auth::group::ldap::{LdapMessageReceiver, SimpleBindRequestEncoder}; +use crate::config::auth::LdapUserGroupConfig; + +pub(super) struct LdapAuthTask { + config: Arc, + connector: Arc, + quit: bool, + pending_request: Option, + request_encoder: SimpleBindRequestEncoder, +} + +impl LdapAuthTask { + pub(super) fn new(config: Arc, connector: Arc) -> Self { + LdapAuthTask { + config, + connector, + quit: false, + pending_request: None, + request_encoder: SimpleBindRequestEncoder::default(), + } + } + + pub(super) async fn run(mut self, receiver: AsyncReceiver) { + loop { + let r = match self.connector.connect().await { + Ok(MaybeSslStream::Plain(stream)) => self.run_with_stream(stream, &receiver).await, + Ok(MaybeSslStream::Ssl(stream)) => self.run_with_stream(stream, &receiver).await, + Err(e) => Err(anyhow!("failed to connect to ldap server: {e}")), + }; + if let Err(e) = r { + warn!("close connection on error: {e}"); + } + + if let Some(mut request) = self.pending_request + && request.retry + { + request.retry = false; + self.pending_request = Some(request); + continue; + } + + break; + } + } + + async fn run_with_stream( + &mut self, + stream: S, + receiver: &AsyncReceiver, + ) -> anyhow::Result<()> + where + S: AsyncStream, + S::R: AsyncRead + Unpin, + S::W: AsyncWrite + Unpin, + { + self.request_encoder.reset(); + let (mut reader, mut writer) = stream.into_split(); + let mut response_receiver = LdapMessageReceiver::new(self.config.max_message_size); + + loop { + if let Some(r) = self.pending_request.take() { + let message_id = match self.send_simple_bind_request(&mut writer, &r).await { + Ok(id) => id, + Err(e) => { + self.pending_request = Some(r); + return Err(anyhow!("send simple bind request error: {e}")); + } + }; + + match tokio::time::timeout( + self.config.response_timeout, + response_receiver.recv(&mut reader), + ) + .await + { + Ok(Ok(message)) => { + if message.id() == 0 { + self.pending_request = Some(r); + let reconnect = self + .handle_unsolicited_notification(message.payload()) + .map_err(|e| anyhow!("invalid unsolicited notification: {e}"))?; + if reconnect { + return Ok(()); + } else { + continue; + } + } else if message.id() != message_id { + self.pending_request = Some(r); + debug!("unexpected response for message {}", message.id()); + continue; + } else { + self.handle_response(message.payload(), r) + .map_err(|e| anyhow!("invalid response: {e}"))?; + } + } + Ok(Err(e)) => { + self.pending_request = Some(r); + return Err(anyhow!("recv ldap response message error: {e}")); + } + Err(_) => { + let _ = r.result_sender.send(None); + return Err(anyhow!("recv ldap response message timed out")); + } + } + } + + tokio::select! { + biased; + + r = receiver.recv() => { + match r { + Ok(r) => self.pending_request = Some(r), + Err(_) => { + self.quit = true; + return Ok(()); + } + } + } + r = response_receiver.recv(&mut reader) => { + // detect the close of ldap server + match r { + Ok(message) => { + if message.id() != 0 { + debug!("unexpected response received for message {}", message.id()); + } else { + let reconnect = self + .handle_unsolicited_notification(message.payload()) + .map_err(|e| anyhow!("invalid unsolicited notification: {e}"))?; + if reconnect { + return Ok(()); + } else { + continue; + } + } + } + Err(e) => { + return Err(anyhow!("ldap connection closed with error {e}")); + } + } + } + } + } + } + + async fn send_simple_bind_request( + &mut self, + writer: &mut W, + r: &LdapAuthRequest, + ) -> anyhow::Result + where + W: AsyncWrite + Unpin, + { + let request_msg = self + .request_encoder + .encode(&r.uid, &r.password, &self.config.base_dn); + writer + .write_all_flush(request_msg) + .await + .map_err(|e| anyhow!("failed to write bind request: {e}"))?; + Ok(self.request_encoder.message_id()) + } + + fn handle_unsolicited_notification(&self, op_data: &[u8]) -> anyhow::Result { + let rsp_sequence = LdapSequence::parse_extended_response(op_data)?; + let data = rsp_sequence.data(); + let result = LdapResult::parse(data)?; + let left = &data[result.encoded_len()..]; + let oid = LdapSequence::parse_extended_response_oid(left)?; + if oid.data() == b"1.3.6.1.4.1.1466.20036" { + // The notice of disconnection unsolicited notification OID + Ok(true) + } else { + // TODO log other OID + Ok(false) + } + } + + fn handle_response(&self, op_data: &[u8], r: LdapAuthRequest) -> anyhow::Result<()> { + let rsp_sequence = LdapSequence::parse_bind_response(op_data)?; + let data = rsp_sequence.data(); + let result = LdapResult::parse(data)?; + if result.result_code() == 0 { + let _ = r.result_sender.send(Some((r.uid, r.password))); + } else { + // TODO log error + let _ = r.result_sender.send(None); + } + Ok(()) + } +} diff --git a/g3proxy/src/auth/group/ldap/protocol/message.rs b/g3proxy/src/auth/group/ldap/protocol/message.rs new file mode 100644 index 00000000..09e8937d --- /dev/null +++ b/g3proxy/src/auth/group/ldap/protocol/message.rs @@ -0,0 +1,80 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +use anyhow::anyhow; +use tokio::io::AsyncRead; + +use g3_io_ext::LimitedReadExt; +use g3_types::codec::{LdapMessage, LdapMessageParseError}; + +pub(crate) struct LdapMessageReceiver { + max_message_size: usize, + buffer: Box<[u8]>, + received_len: usize, + cur_message_len: usize, +} + +impl LdapMessageReceiver { + pub(crate) fn new(max_message_size: usize) -> Self { + let buffer_size = max_message_size + 10; + LdapMessageReceiver { + max_message_size, + buffer: vec![0; buffer_size].into_boxed_slice(), + received_len: 0, + cur_message_len: 0, + } + } + + fn consume_cur_message(&mut self) { + if self.cur_message_len == 0 { + return; + } + + let left_size = self.received_len - self.cur_message_len; + if left_size > 0 { + self.buffer + .copy_within(self.cur_message_len..self.received_len, 0); + } + self.received_len = left_size; + self.cur_message_len = 0; + } + + pub(crate) async fn recv(&mut self, reader: &mut R) -> anyhow::Result> + where + R: AsyncRead + Unpin, + { + self.consume_cur_message(); + + // to workaround limitations of rust borrow checker + let buffer_ptr = self.buffer.as_mut_ptr(); + let shadow_buffer = unsafe { + std::ptr::slice_from_raw_parts_mut(buffer_ptr, self.buffer.len()) + .as_mut() + .unwrap() + }; + + loop { + if self.received_len > 0 { + match LdapMessage::parse(&self.buffer[..self.received_len], self.max_message_size) { + Ok(message) => { + self.cur_message_len = message.encoded_size(); + return Ok(message); + } + Err(LdapMessageParseError::NeedMoreData(_)) => {} + Err(e) => return Err(anyhow!("invalid ldap response message received: {e}")), + } + } + + let nr = reader + .read_all_once(&mut shadow_buffer[self.received_len..]) + .await + .map_err(|e| anyhow!("read io error: {e}"))?; + if nr == 0 { + return Err(anyhow!("ldap connection closed unexpected")); + } + self.received_len += nr; + } + } +} diff --git a/g3proxy/src/auth/group/ldap/protocol/mod.rs b/g3proxy/src/auth/group/ldap/protocol/mod.rs new file mode 100644 index 00000000..d30312e9 --- /dev/null +++ b/g3proxy/src/auth/group/ldap/protocol/mod.rs @@ -0,0 +1,10 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +mod request; +pub(super) use request::SimpleBindRequestEncoder; + +mod message; +pub(super) use message::LdapMessageReceiver; diff --git a/g3proxy/src/auth/group/ldap/protocol/request.rs b/g3proxy/src/auth/group/ldap/protocol/request.rs new file mode 100644 index 00000000..71ce03b2 --- /dev/null +++ b/g3proxy/src/auth/group/ldap/protocol/request.rs @@ -0,0 +1,122 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +use g3_types::codec::BerLengthEncoder; + +const MAX_MESSAGE_ID: u8 = 0x7F; +const MIN_MESSAGE_ID: u8 = 1; + +pub(crate) struct SimpleBindRequestEncoder { + message_id: u8, + bind_dn_length_encoder: BerLengthEncoder, + password_length_encoder: BerLengthEncoder, + request_length_encoder: BerLengthEncoder, + message_length_encoder: BerLengthEncoder, + request_buf: Vec, +} + +impl Default for SimpleBindRequestEncoder { + fn default() -> Self { + SimpleBindRequestEncoder { + message_id: MAX_MESSAGE_ID, + bind_dn_length_encoder: Default::default(), + password_length_encoder: Default::default(), + request_length_encoder: Default::default(), + message_length_encoder: Default::default(), + request_buf: Vec::with_capacity(256), + } + } +} + +impl SimpleBindRequestEncoder { + pub(crate) fn reset(&mut self) { + self.message_id = 1; + } + + pub(crate) fn message_id(&self) -> u32 { + self.message_id as u32 + } + + pub(crate) fn encode(&mut self, username: &str, password: &str, base_dn: &str) -> &[u8] { + self.message_id += 1; + if self.message_id > MAX_MESSAGE_ID { + self.message_id = MIN_MESSAGE_ID; + } + + let bind_dn = format!("uid={username},{base_dn}"); + let bind_dn_len = bind_dn.len(); + let bind_dn_length_bytes = self.bind_dn_length_encoder.encode(bind_dn_len); + let bind_dn_encoded_len = 1 + bind_dn_length_bytes.len() + bind_dn_len; + + let password_len = password.len(); + let password_length_bytes = self.password_length_encoder.encode(password_len); + let password_encoded_len = 1 + password_length_bytes.len() + password_len; + + let request_len = 3 + bind_dn_encoded_len + password_encoded_len; + let request_length_bytes = self.request_length_encoder.encode(request_len); + let request_encoded_len = 1 + request_length_bytes.len() + request_len; + + let message_len = 3 + request_encoded_len; + let message_length_bytes = self.message_length_encoder.encode(message_len); + let message_encoded_len = 1 + message_length_bytes.len() + message_len; + + self.request_buf.clear(); + self.request_buf.reserve(message_encoded_len); + + // Begin the LDAPMessage sequence + self.request_buf.push(0x30); + self.request_buf.extend_from_slice(message_length_bytes); + + // The message ID + self.request_buf.push(0x02); + self.request_buf.push(0x01); + self.request_buf.push(self.message_id); // the message is always <= 0x7F + + // Begin the bind request protocol op + self.request_buf.push(0x60); + self.request_buf.extend_from_slice(request_length_bytes); + + // The LDAP protocol version (integer value 3) + self.request_buf.extend_from_slice(&[0x02, 0x01, 0x03]); + + // The bind DN + self.request_buf.push(0x04); + self.request_buf.extend_from_slice(bind_dn_length_bytes); + self.request_buf.extend_from_slice(bind_dn.as_bytes()); + + // The password + self.request_buf.push(0x80); + self.request_buf.extend_from_slice(password_length_bytes); + self.request_buf.extend_from_slice(password.as_bytes()); + + &self.request_buf + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encode() { + let mut encoder = SimpleBindRequestEncoder::default(); + let base_dn = "ou=People,dc=example,dc=com"; + + let request = encoder.encode("jdoe", "secret123", base_dn); + assert_eq!( + request, + [ + 0x30, 0x39, // Begin the LDAPMessage sequence + 0x02, 0x01, 0x01, // The message ID (integer value 1) + 0x60, 0x34, // Begin the bind request protocol op + 0x02, 0x01, 0x03, // The LDAP protocol version (integer value 3) + 0x04, 0x24, b'u', b'i', b'd', b'=', b'j', b'd', b'o', b'e', b',', b'o', b'u', b'=', + b'P', b'e', b'o', b'p', b'l', b'e', b',', b'd', b'c', b'=', b'e', b'x', b'a', b'm', + b'p', b'l', b'e', b',', b'd', b'c', b'=', b'c', b'o', b'm', // base dn + 0x80, 0x09, b's', b'e', b'c', b'r', b'e', b't', b'1', b'2', b'3', // password + ] + ); + } +} diff --git a/g3proxy/src/auth/group/mod.rs b/g3proxy/src/auth/group/mod.rs index 3beaff0a..cc6c630e 100644 --- a/g3proxy/src/auth/group/mod.rs +++ b/g3proxy/src/auth/group/mod.rs @@ -28,10 +28,14 @@ pub(crate) use basic::BasicUserGroup; mod facts; pub(crate) use facts::FactsUserGroup; +mod ldap; +pub(crate) use ldap::LdapUserGroup; + #[derive(Clone)] pub(crate) enum UserGroup { Basic(Arc), Facts(Arc), + Ldap(Arc), } impl UserGroup { @@ -39,6 +43,7 @@ impl UserGroup { match self { UserGroup::Basic(v) => v.base().r#type(), UserGroup::Facts(v) => v.base().r#type(), + UserGroup::Ldap(v) => v.base().r#type(), } } @@ -52,6 +57,10 @@ impl UserGroup { let c = v.clone_config(); AnyUserGroupConfig::Facts(c) } + UserGroup::Ldap(v) => { + let c = v.clone_config(); + AnyUserGroupConfig::Ldap(c) + } } } @@ -70,6 +79,10 @@ impl UserGroup { let group = FactsUserGroup::new_with_config(c).await?; Ok(UserGroup::Facts(group)) } + AnyUserGroupConfig::Ldap(c) => { + let group = LdapUserGroup::new_with_config(c).await?; + Ok(UserGroup::Ldap(group)) + } } } @@ -83,6 +96,10 @@ impl UserGroup { let group = g.reload(c)?; Ok(UserGroup::Facts(group)) } + (UserGroup::Ldap(g), AnyUserGroupConfig::Ldap(c)) => { + let group = g.reload(c)?; + Ok(UserGroup::Ldap(group)) + } (_, config) => Err(anyhow!( "reload user group {} type {} to {} is invalid", config.name(), @@ -96,6 +113,7 @@ impl UserGroup { match self { UserGroup::Basic(v) => v.base().stop_fetch_job(), UserGroup::Facts(v) => v.base().stop_fetch_job(), + UserGroup::Ldap(v) => v.base().stop_fetch_job(), } } @@ -103,6 +121,7 @@ impl UserGroup { match self { UserGroup::Basic(v) => v.base().allow_anonymous(client_addr), UserGroup::Facts(v) => v.base().allow_anonymous(client_addr), + UserGroup::Ldap(v) => v.base().allow_anonymous(client_addr), } } @@ -110,6 +129,7 @@ impl UserGroup { match self { UserGroup::Basic(v) => v.base().get_anonymous_user(), UserGroup::Facts(v) => v.base().get_anonymous_user(), + UserGroup::Ldap(v) => v.base().get_anonymous_user(), } } @@ -120,6 +140,7 @@ impl UserGroup { match self { UserGroup::Basic(v) => v.base().foreach_user(f), UserGroup::Facts(v) => v.base().foreach_user(f), + UserGroup::Ldap(v) => v.base().foreach_user(f), } } @@ -127,6 +148,7 @@ impl UserGroup { match self { UserGroup::Basic(v) => v.base().all_static_users(), UserGroup::Facts(v) => v.base().all_static_users(), + UserGroup::Ldap(v) => v.base().all_static_users(), } } @@ -134,6 +156,7 @@ impl UserGroup { match self { UserGroup::Basic(v) => v.base().all_dynamic_users(), UserGroup::Facts(v) => v.base().all_dynamic_users(), + UserGroup::Ldap(v) => v.base().all_dynamic_users(), } } @@ -141,6 +164,7 @@ impl UserGroup { match self { UserGroup::Basic(v) => v.base().publish_dynamic_users(contents).await, UserGroup::Facts(v) => v.base().publish_dynamic_users(contents).await, + UserGroup::Ldap(v) => v.base().publish_dynamic_users(contents).await, } } @@ -163,10 +187,15 @@ impl UserGroup { v.rebuild_match_table(); Ok(()) } + UserGroup::Ldap(v) => { + v.base() + .save_dynamic_users(contents, dynamic_config, Some(dynamic_key)) + .await + } } } - pub(crate) fn check_user_with_password( + pub(crate) async fn check_user_with_password( &self, username: &str, password: &Password, @@ -181,6 +210,10 @@ impl UserGroup { server_extra_tags, ), UserGroup::Facts(_) => Err(UserAuthError::NoSuchUser), + UserGroup::Ldap(v) => { + v.check_user_with_password(username, password, server_name, server_extra_tags) + .await + } } } } diff --git a/g3proxy/src/auth/mod.rs b/g3proxy/src/auth/mod.rs index 507b4d60..4449002d 100644 --- a/g3proxy/src/auth/mod.rs +++ b/g3proxy/src/auth/mod.rs @@ -33,6 +33,7 @@ mod source; pub(crate) enum UserType { Static, Dynamic, + Unmanaged, Anonymous, } @@ -41,6 +42,7 @@ impl UserType { match self { UserType::Static => "Static", UserType::Dynamic => "Dynamic", + UserType::Unmanaged => "Unmanaged", UserType::Anonymous => "Anonymous", } } diff --git a/g3proxy/src/auth/user.rs b/g3proxy/src/auth/user.rs index c783a09c..2702a4c5 100644 --- a/g3proxy/src/auth/user.rs +++ b/g3proxy/src/auth/user.rs @@ -32,6 +32,7 @@ use crate::config::auth::{UserAuditConfig, UserConfig}; pub(crate) struct User { config: Arc, + name: ArcStr, group: NodeName, started: Instant, is_expired: AtomicBool, @@ -88,6 +89,24 @@ impl User { group: &NodeName, config: &Arc, datetime_now: &DateTime, + ) -> anyhow::Result { + Self::new_with_name(config.name().clone(), group, config, datetime_now) + } + + pub(super) fn new_unmanaged( + name: &ArcStr, + group: &NodeName, + config: &Arc, + ) -> anyhow::Result { + let now = Utc::now(); + Self::new_with_name(name.clone(), group, config, &now) + } + + fn new_with_name( + name: ArcStr, + group: &NodeName, + config: &Arc, + datetime_now: &DateTime, ) -> anyhow::Result { let request_rate_limit = config .request_rate_limit @@ -133,10 +152,11 @@ impl User { let is_expired = AtomicBool::new(config.is_expired(datetime_now)); let is_blocked = Arc::new(AtomicBool::new(config.block_and_delay.is_some())); - let explicit_sites = UserSites::new(config.explicit_sites.values(), config.name(), group) + let explicit_sites = UserSites::new(config.explicit_sites.values(), &name, group) .context("failed to build sites config")?; let mut user = User { + name, config: Arc::clone(config), group: group.clone(), started: Instant::now(), @@ -291,10 +311,11 @@ impl User { let explicit_sites = self .explicit_sites - .new_for_reload(config.explicit_sites.values(), config.name(), &self.group) + .new_for_reload(config.explicit_sites.values(), &self.name, &self.group) .context("failed to build sites config")?; let mut user = User { + name: self.name.clone(), config: Arc::clone(config), group: self.group.clone(), started: self.started, @@ -423,7 +444,7 @@ impl User { let stats = map.entry(server.clone()).or_insert_with(|| { Arc::new(UserForbiddenStats::new( &self.group, - self.config.name().clone(), + self.name.clone(), user_type, server, server_extra_tags, @@ -451,7 +472,7 @@ impl User { let stats = map.entry(server.clone()).or_insert_with(|| { Arc::new(UserRequestStats::new( &self.group, - self.config.name().clone(), + self.name.clone(), user_type, server, server_extra_tags, @@ -479,7 +500,7 @@ impl User { let stats = map.entry(server.clone()).or_insert_with(|| { Arc::new(UserTrafficStats::new( &self.group, - self.config.name().clone(), + self.name.clone(), user_type, server, server_extra_tags, @@ -507,7 +528,7 @@ impl User { let stats = map.entry(escaper.clone()).or_insert_with(|| { Arc::new(UserUpstreamTrafficStats::new( &self.group, - self.config.name().clone(), + self.name.clone(), user_type, escaper, escaper_extra_tags, @@ -754,7 +775,7 @@ impl UserContext { #[inline] pub(crate) fn user_name(&self) -> &ArcStr { - self.user.config.name() + &self.user.name } #[inline] diff --git a/g3proxy/src/config/auth/group/basic.rs b/g3proxy/src/config/auth/group/basic.rs index 5e8819c7..a4f55373 100644 --- a/g3proxy/src/config/auth/group/basic.rs +++ b/g3proxy/src/config/auth/group/basic.rs @@ -27,7 +27,7 @@ const USER_GROUP_TYPE: &str = "basic"; #[derive(Clone)] pub(crate) struct BasicUserGroupConfig { name: NodeName, - position: Option, + pub(super) position: Option, pub(crate) static_users: HashMap>, pub(crate) dynamic_source: Option, pub(crate) dynamic_cache: PathBuf, diff --git a/g3proxy/src/config/auth/group/ldap.rs b/g3proxy/src/config/auth/group/ldap.rs new file mode 100644 index 00000000..99a5065c --- /dev/null +++ b/g3proxy/src/config/auth/group/ldap.rs @@ -0,0 +1,168 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{Context, anyhow}; +use arcstr::ArcStr; +use yaml_rust::{Yaml, yaml}; + +use g3_types::net::{ConnectionPoolConfig, OpensslClientConfigBuilder, UpstreamAddr}; +use g3_yaml::YamlDocPosition; + +use super::{BasicUserGroupConfig, UserGroupConfig}; +use crate::config::auth::UserConfig; + +const USER_GROUP_TYPE: &str = "ldap"; + +#[derive(Clone)] +pub(crate) struct LdapUserGroupConfig { + basic: BasicUserGroupConfig, + pub(crate) server: UpstreamAddr, + pub(crate) tls_client: Option, + pub(crate) direct_tls: bool, + pub(crate) base_dn: ArcStr, + pub(crate) unmanaged_user: Option>, + pub(crate) max_message_size: usize, + pub(crate) connect_timeout: Duration, + pub(crate) response_timeout: Duration, + pub(crate) connection_pool: ConnectionPoolConfig, + pub(crate) queue_channel_size: usize, + pub(crate) queue_wait_timeout: Duration, +} + +impl LdapUserGroupConfig { + fn new(position: Option) -> Self { + LdapUserGroupConfig { + basic: BasicUserGroupConfig::new(position), + server: UpstreamAddr::empty(), + tls_client: None, + direct_tls: false, + base_dn: ArcStr::new(), + unmanaged_user: None, + max_message_size: 256, + connect_timeout: Duration::from_secs(4), + response_timeout: Duration::from_secs(2), + connection_pool: ConnectionPoolConfig::new(1024, 8), + queue_channel_size: 64, + queue_wait_timeout: Duration::from_secs(4), + } + } + + pub(crate) fn parse( + map: &yaml::Hash, + position: Option, + ) -> anyhow::Result { + let mut config = Self::new(position); + g3_yaml::foreach_kv(map, |k, v| config.set(k, v))?; + config.check()?; + Ok(config) + } + + fn check(&mut self) -> anyhow::Result<()> { + if self.server.is_empty() { + return Err(anyhow!("no ldap url set")); + } + + if self.direct_tls && self.tls_client.is_none() { + self.tls_client = Some(OpensslClientConfigBuilder::with_cache_for_one_site()); + } + + self.basic.check() + } + + fn set(&mut self, k: &str, v: &Yaml) -> anyhow::Result<()> { + match g3_yaml::key::normalize(k).as_str() { + "ldap_url" => { + let url = g3_yaml::value::as_url(v) + .context(format!("invalid ldap url value for key {k}"))?; + let default_port; + match url.scheme() { + "ldap" => default_port = 389, + "ldaps" => { + self.direct_tls = true; + default_port = 636; + } + scheme => return Err(anyhow!("unsupported ldap url scheme {scheme}")), + } + let Some(host) = url.host() else { + return Err(anyhow!("no host found in ldap url {url}")); + }; + let port = url.port().unwrap_or(default_port); + self.server = UpstreamAddr::new(host, port); + let path = url.path(); + let encoded_dn = path.strip_prefix("/").unwrap_or(path); + let base_dn = percent_encoding::percent_decode_str(encoded_dn) + .decode_utf8() + .map_err(|e| anyhow!("the base dn is not valid utf-8 string: {e}"))?; + self.base_dn = ArcStr::from(base_dn.as_ref()); + Ok(()) + } + "tls_client" => { + let lookup_dir = g3_daemon::config::get_lookup_dir(self.basic.position.as_ref())?; + let config = g3_yaml::value::as_to_one_openssl_tls_client_config_builder( + v, + Some(lookup_dir), + ) + .context(format!( + "invalid openssl tls client config value for key {k}" + ))?; + self.tls_client = Some(config); + Ok(()) + } + "unmanaged_user" => { + if let Yaml::Hash(map) = v { + let mut user = UserConfig::parse_yaml(map, self.basic.position.as_ref())?; + user.set_no_password(); + self.unmanaged_user = Some(Arc::new(user)); + Ok(()) + } else { + Err(anyhow!("invalid hash value for key {k}")) + } + } + "max_message_size" => { + self.max_message_size = g3_yaml::value::as_usize(v)?; + Ok(()) + } + "connect_timeout" => { + self.connect_timeout = g3_yaml::humanize::as_duration(v) + .context(format!("invalid humanize duration value for key {k}"))?; + Ok(()) + } + "response_timeout" => { + self.response_timeout = g3_yaml::humanize::as_duration(v) + .context(format!("invalid humanize duration value for key {k}"))?; + Ok(()) + } + "connection_pool" | "pool" => { + self.connection_pool = g3_yaml::value::as_connection_pool_config(v) + .context(format!("invalid connection pool config for key {k}"))?; + Ok(()) + } + "queue_channel_size" => { + let channel_size = g3_yaml::value::as_nonzero_usize(v)?; + self.queue_channel_size = channel_size.get(); + Ok(()) + } + "queue_wait_timeout" => { + self.queue_wait_timeout = g3_yaml::humanize::as_duration(v) + .context(format!("invalid humanize duration value for key {k}"))?; + Ok(()) + } + _ => self.basic.set(k, v), + } + } +} + +impl UserGroupConfig for LdapUserGroupConfig { + fn basic_config(&self) -> &BasicUserGroupConfig { + &self.basic + } + + fn r#type(&self) -> &'static str { + USER_GROUP_TYPE + } +} diff --git a/g3proxy/src/config/auth/group/mod.rs b/g3proxy/src/config/auth/group/mod.rs index f83d6216..fbfd2995 100644 --- a/g3proxy/src/config/auth/group/mod.rs +++ b/g3proxy/src/config/auth/group/mod.rs @@ -13,6 +13,9 @@ pub(crate) use basic::BasicUserGroupConfig; mod facts; pub(crate) use facts::FactsUserGroupConfig; +mod ldap; +pub(crate) use ldap::LdapUserGroupConfig; + pub(crate) trait UserGroupConfig { fn basic_config(&self) -> &BasicUserGroupConfig; @@ -25,6 +28,7 @@ pub(crate) trait UserGroupConfig { pub(crate) enum AnyUserGroupConfig { Basic(BasicUserGroupConfig), Facts(FactsUserGroupConfig), + Ldap(LdapUserGroupConfig), } impl AnyUserGroupConfig { diff --git a/g3proxy/src/config/auth/mod.rs b/g3proxy/src/config/auth/mod.rs index 5fee0452..9fdd6aa8 100644 --- a/g3proxy/src/config/auth/mod.rs +++ b/g3proxy/src/config/auth/mod.rs @@ -21,7 +21,8 @@ pub(crate) use source::*; pub(crate) mod group; pub(crate) use group::{ - AnyUserGroupConfig, BasicUserGroupConfig, FactsUserGroupConfig, UserGroupConfig, + AnyUserGroupConfig, BasicUserGroupConfig, FactsUserGroupConfig, LdapUserGroupConfig, + UserGroupConfig, }; mod registry; @@ -62,6 +63,10 @@ fn load_user_group( let group = FactsUserGroupConfig::parse(map, position)?; Ok(AnyUserGroupConfig::Facts(group)) } + "ldap" => { + let group = LdapUserGroupConfig::parse(map, position)?; + Ok(AnyUserGroupConfig::Ldap(group)) + } _ => Err(anyhow!("unsupported user group type {group_type}")), } } diff --git a/g3proxy/src/serve/http_proxy/task/pipeline/writer.rs b/g3proxy/src/serve/http_proxy/task/pipeline/writer.rs index a348c7d2..ee4cfbd7 100644 --- a/g3proxy/src/serve/http_proxy/task/pipeline/writer.rs +++ b/g3proxy/src/serve/http_proxy/task/pipeline/writer.rs @@ -118,7 +118,7 @@ where } } - fn do_auth( + async fn do_auth( &mut self, req: &HttpProxyRequest, ) -> Result, UserAuthError> { @@ -145,12 +145,14 @@ where .as_ref() .map(|c| c.real_username(username)) .unwrap_or(username); - user_group.check_user_with_password( - username, - &v.password, - self.ctx.server_config.name(), - self.ctx.server_stats.share_extra_tags(), - )? + user_group + .check_user_with_password( + username, + &v.password, + self.ctx.server_config.name(), + self.ctx.server_stats.share_extra_tags(), + ) + .await? } }; user_ctx.check_client_addr(self.ctx.client_addr())?; @@ -195,7 +197,7 @@ where loop { let res = match self.task_queue.recv().await { Some(Ok(req)) => { - let res = match self.do_auth(&req) { + let res = match self.do_auth(&req).await { Ok(user_ctx) => { self.req_count.consequent_auth_failed = 0; self.run(req, user_ctx).await diff --git a/g3proxy/src/serve/http_rproxy/task/pipeline/writer.rs b/g3proxy/src/serve/http_rproxy/task/pipeline/writer.rs index 0d83360c..3d3e4418 100644 --- a/g3proxy/src/serve/http_rproxy/task/pipeline/writer.rs +++ b/g3proxy/src/serve/http_rproxy/task/pipeline/writer.rs @@ -114,7 +114,7 @@ where } } - fn do_auth( + async fn do_auth( &mut self, req: &HttpRProxyRequest, ) -> Result, UserAuthError> { @@ -132,12 +132,16 @@ where ) }) .ok_or(UserAuthError::NoUserSupplied)?, - HttpAuth::Basic(v) => user_group.check_user_with_password( - v.username.as_original(), - &v.password, - self.ctx.server_config.name(), - self.ctx.server_stats.share_extra_tags(), - )?, + HttpAuth::Basic(v) => { + user_group + .check_user_with_password( + v.username.as_original(), + &v.password, + self.ctx.server_config.name(), + self.ctx.server_stats.share_extra_tags(), + ) + .await? + } }; user_ctx.check_client_addr(self.ctx.client_addr())?; @@ -182,7 +186,7 @@ where loop { let res = match self.task_queue.recv().await { Some(Ok(req)) => { - let res = match self.do_auth(&req) { + let res = match self.do_auth(&req).await { Ok(user_ctx) => { self.req_count.consequent_auth_failed = 0; diff --git a/g3proxy/src/serve/socks_proxy/task/negotiation/task.rs b/g3proxy/src/serve/socks_proxy/task/negotiation/task.rs index 08f01a4f..1718666f 100644 --- a/g3proxy/src/serve/socks_proxy/task/negotiation/task.rs +++ b/g3proxy/src/serve/socks_proxy/task/negotiation/task.rs @@ -254,12 +254,15 @@ impl SocksProxyNegotiationTask { base_username = username.as_original(); } - match user_group.check_user_with_password( - base_username, - &password, - self.ctx.server_config.name(), - self.ctx.server_stats.share_extra_tags(), - ) { + match user_group + .check_user_with_password( + base_username, + &password, + self.ctx.server_config.name(), + self.ctx.server_stats.share_extra_tags(), + ) + .await + { Ok(user_ctx) => { if user_ctx.check_client_addr(self.ctx.client_addr()).is_err() { self.ctx.server_stats.forbidden.add_auth_failed(); diff --git a/lib/g3-dpi/src/parser/quic/frame/ack.rs b/lib/g3-dpi/src/parser/quic/frame/ack.rs index d36bf6f9..538aaea9 100644 --- a/lib/g3-dpi/src/parser/quic/frame/ack.rs +++ b/lib/g3-dpi/src/parser/quic/frame/ack.rs @@ -3,14 +3,12 @@ * Copyright 2024-2025 ByteDance and/or its affiliates. */ -use g3_types::net::QuicVarInt; - -use super::FrameParseError; +use super::{FrameParseError, VarInt}; pub struct AckFrame { - pub largest_ack: QuicVarInt, - pub ack_delay: QuicVarInt, - pub first_ack_range: QuicVarInt, + pub largest_ack: VarInt, + pub ack_delay: VarInt, + pub first_ack_range: VarInt, pub ack_ranges: Vec, pub ecn_counts: Option, pub(crate) encoded_len: usize, @@ -23,7 +21,7 @@ impl AckFrame { macro_rules! read_var_int { ($var:ident) => { - let Some($var) = QuicVarInt::parse(&data[offset..]) else { + let Some($var) = VarInt::parse(&data[offset..]) else { return Err(FrameParseError::NotEnoughData); }; offset += $var.encoded_len(); @@ -69,19 +67,19 @@ impl AckFrame { } pub struct AckRange { - pub gap: QuicVarInt, - pub length: QuicVarInt, + pub gap: VarInt, + pub length: VarInt, encoded_len: usize, } impl AckRange { fn parse(data: &[u8]) -> Result { - let Some(gap) = QuicVarInt::parse(data) else { + let Some(gap) = VarInt::parse(data) else { return Err(FrameParseError::NotEnoughData); }; let offset = gap.encoded_len(); - let Some(length) = QuicVarInt::parse(&data[offset..]) else { + let Some(length) = VarInt::parse(&data[offset..]) else { return Err(FrameParseError::NotEnoughData); }; @@ -95,9 +93,9 @@ impl AckRange { } pub struct EcnCounts { - pub ect0: QuicVarInt, - pub ect1: QuicVarInt, - pub ecn_ce: QuicVarInt, + pub ect0: VarInt, + pub ect1: VarInt, + pub ecn_ce: VarInt, encoded_len: usize, } @@ -107,7 +105,7 @@ impl EcnCounts { macro_rules! read_var_int { ($var:ident) => { - let Some($var) = QuicVarInt::parse(&data[offset..]) else { + let Some($var) = VarInt::parse(&data[offset..]) else { return Err(FrameParseError::NotEnoughData); }; offset += $var.encoded_len(); diff --git a/lib/g3-dpi/src/parser/quic/frame/crypto.rs b/lib/g3-dpi/src/parser/quic/frame/crypto.rs index 44095b22..8a3215cb 100644 --- a/lib/g3-dpi/src/parser/quic/frame/crypto.rs +++ b/lib/g3-dpi/src/parser/quic/frame/crypto.rs @@ -6,9 +6,7 @@ use std::cmp::Ordering; use std::collections::BTreeSet; -use g3_types::net::QuicVarInt; - -use super::{FrameConsume, FrameParseError}; +use super::{FrameConsume, FrameParseError, VarInt}; use crate::parser::tls::{ClientHello, ClientHelloParseError, HandshakeHeader, HandshakeType}; pub struct CryptoFrame<'a> { @@ -20,7 +18,7 @@ pub struct CryptoFrame<'a> { impl<'a> CryptoFrame<'a> { /// Parse a Crypto Frame from a packet buffer pub fn parse(data: &'a [u8]) -> Result { - let Some(stream_offset) = QuicVarInt::parse(data) else { + let Some(stream_offset) = VarInt::parse(data) else { return Err(FrameParseError::NotEnoughData); }; let mut offset = stream_offset.encoded_len(); @@ -28,7 +26,7 @@ impl<'a> CryptoFrame<'a> { .map_err(|_| FrameParseError::TooBigOffsetValue(stream_offset.value()))?; let left = &data[offset..]; - let Some(length) = QuicVarInt::parse(left) else { + let Some(length) = VarInt::parse(left) else { return Err(FrameParseError::NotEnoughData); }; offset += length.encoded_len(); diff --git a/lib/g3-dpi/src/parser/quic/frame/mod.rs b/lib/g3-dpi/src/parser/quic/frame/mod.rs index 19e74bbf..7610a636 100644 --- a/lib/g3-dpi/src/parser/quic/frame/mod.rs +++ b/lib/g3-dpi/src/parser/quic/frame/mod.rs @@ -5,6 +5,8 @@ use thiserror::Error; +use super::VarInt; + mod crypto; pub use crypto::{CryptoFrame, HandshakeCoalescer}; diff --git a/lib/g3-dpi/src/parser/quic/mod.rs b/lib/g3-dpi/src/parser/quic/mod.rs index 98e72307..a2691c24 100644 --- a/lib/g3-dpi/src/parser/quic/mod.rs +++ b/lib/g3-dpi/src/parser/quic/mod.rs @@ -3,6 +3,9 @@ * Copyright 2024-2025 ByteDance and/or its affiliates. */ +mod var_int; +use var_int::VarInt; + mod packet; pub use packet::{InitialPacket, PacketParseError}; diff --git a/lib/g3-dpi/src/parser/quic/packet/mod.rs b/lib/g3-dpi/src/parser/quic/packet/mod.rs index d5221baf..debe9ee0 100644 --- a/lib/g3-dpi/src/parser/quic/packet/mod.rs +++ b/lib/g3-dpi/src/parser/quic/packet/mod.rs @@ -6,9 +6,7 @@ use openssl::error::ErrorStack; use thiserror::Error; -use g3_types::net::QuicVarInt; - -use super::{AckFrame, CryptoFrame, FrameConsume, FrameParseError}; +use super::{AckFrame, CryptoFrame, FrameConsume, FrameParseError, VarInt}; mod hkdf; use hkdf::{quic_hkdf_expand, quic_hkdf_extract_expand}; @@ -96,7 +94,7 @@ impl InitialPacket { while offset < payload.len() { let left = &payload[offset..]; - let Some(frame_type) = QuicVarInt::parse(left) else { + let Some(frame_type) = VarInt::parse(left) else { return Err(FrameParseError::NotEnoughData); }; diff --git a/lib/g3-dpi/src/parser/quic/packet/v1.rs b/lib/g3-dpi/src/parser/quic/packet/v1.rs index f3caeb5c..da259245 100644 --- a/lib/g3-dpi/src/parser/quic/packet/v1.rs +++ b/lib/g3-dpi/src/parser/quic/packet/v1.rs @@ -5,9 +5,7 @@ use openssl::error::ErrorStack; -use g3_types::net::QuicVarInt; - -use super::{Header, PacketParseError}; +use super::{Header, PacketParseError, VarInt}; const INITIAL_SALT: &[u8] = &[ 0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, @@ -64,7 +62,7 @@ impl InitialPacketV1 { // Token let left = &data[offset..]; - let Some(token_len) = QuicVarInt::parse(left) else { + let Some(token_len) = VarInt::parse(left) else { return Err(PacketParseError::TooSmall); }; let start = offset + token_len.encoded_len(); @@ -75,7 +73,7 @@ impl InitialPacketV1 { // Length let left = &data[offset..]; - let Some(length) = QuicVarInt::parse(left) else { + let Some(length) = VarInt::parse(left) else { return Err(PacketParseError::TooSmall); }; offset += length.encoded_len(); diff --git a/lib/g3-dpi/src/parser/quic/packet/v2.rs b/lib/g3-dpi/src/parser/quic/packet/v2.rs index 471b3001..32b50543 100644 --- a/lib/g3-dpi/src/parser/quic/packet/v2.rs +++ b/lib/g3-dpi/src/parser/quic/packet/v2.rs @@ -5,9 +5,7 @@ use openssl::error::ErrorStack; -use g3_types::net::QuicVarInt; - -use super::{Header, PacketParseError}; +use super::{Header, PacketParseError, VarInt}; const INITIAL_SALT: &[u8] = &[ 0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, @@ -66,7 +64,7 @@ impl InitialPacketV2 { // Token let left = &data[offset..]; - let Some(token_len) = QuicVarInt::parse(left) else { + let Some(token_len) = VarInt::parse(left) else { return Err(PacketParseError::TooSmall); }; let start = offset + token_len.encoded_len(); @@ -77,7 +75,7 @@ impl InitialPacketV2 { // Length let left = &data[offset..]; - let Some(length) = QuicVarInt::parse(left) else { + let Some(length) = VarInt::parse(left) else { return Err(PacketParseError::TooSmall); }; offset += length.encoded_len(); diff --git a/lib/g3-types/src/net/quic/var_int.rs b/lib/g3-dpi/src/parser/quic/var_int.rs similarity index 73% rename from lib/g3-types/src/net/quic/var_int.rs rename to lib/g3-dpi/src/parser/quic/var_int.rs index b44d420f..ed091f62 100644 --- a/lib/g3-types/src/net/quic/var_int.rs +++ b/lib/g3-dpi/src/parser/quic/var_int.rs @@ -4,12 +4,12 @@ */ #[derive(Debug)] -pub struct QuicVarInt { +pub struct VarInt { value: u64, encoded_len: usize, } -impl QuicVarInt { +impl VarInt { /// Try to parse a QUIC variant-length int value from the buffer pub fn parse(data: &[u8]) -> Option { if data.is_empty() { @@ -18,7 +18,7 @@ impl QuicVarInt { let value0 = data[0] & 0b0011_1111; match data[0] >> 6 { - 0 => Some(QuicVarInt { + 0 => Some(VarInt { value: value0 as u64, encoded_len: 1, }), @@ -26,7 +26,7 @@ impl QuicVarInt { if data.len() < 2 { return None; } - Some(QuicVarInt { + Some(VarInt { value: u16::from_be_bytes([value0, data[1]]) as u64, encoded_len: 2, }) @@ -35,7 +35,7 @@ impl QuicVarInt { if data.len() < 4 { return None; } - Some(QuicVarInt { + Some(VarInt { value: u32::from_be_bytes([value0, data[1], data[2], data[3]]) as u64, encoded_len: 4, }) @@ -44,7 +44,7 @@ impl QuicVarInt { if data.len() < 8 { return None; } - Some(QuicVarInt { + Some(VarInt { value: u64::from_be_bytes([ value0, data[1], data[2], data[3], data[4], data[5], data[6], data[7], ]), @@ -72,24 +72,24 @@ mod tests { #[test] fn parse() { - assert!(QuicVarInt::parse(b"").is_none()); + assert!(VarInt::parse(b"").is_none()); - let v = QuicVarInt::parse(&[0x02]).unwrap(); + let v = VarInt::parse(&[0x02]).unwrap(); assert_eq!(v.value, 2); assert_eq!(v.encoded_len(), 1); - assert!(QuicVarInt::parse(&[0b0100_1111]).is_none()); - let v = QuicVarInt::parse(&[0b0100_1111, 0]).unwrap(); + assert!(VarInt::parse(&[0b0100_1111]).is_none()); + let v = VarInt::parse(&[0b0100_1111, 0]).unwrap(); assert_eq!(v.value, 0x0F00); assert_eq!(v.encoded_len(), 2); - assert!(QuicVarInt::parse(&[0b1000_1111, 0x00]).is_none()); - let v = QuicVarInt::parse(&[0b1000_1111, 0, 0, 0x01]).unwrap(); + assert!(VarInt::parse(&[0b1000_1111, 0x00]).is_none()); + let v = VarInt::parse(&[0b1000_1111, 0, 0, 0x01]).unwrap(); assert_eq!(v.value, 0x0F000001); assert_eq!(v.encoded_len(), 4); - assert!(QuicVarInt::parse(&[0b1100_1111, 0]).is_none()); - let v = QuicVarInt::parse(&[0b1100_1111, 0, 0, 0, 0, 0, 0, 0x01]).unwrap(); + assert!(VarInt::parse(&[0b1100_1111, 0]).is_none()); + let v = VarInt::parse(&[0b1100_1111, 0, 0, 0, 0, 0, 0, 0x01]).unwrap(); assert_eq!(v.value, 0x0F00000000000001); assert_eq!(v.encoded_len(), 8); } diff --git a/lib/g3-dpi/src/protocol/ldap.rs b/lib/g3-dpi/src/protocol/ldap.rs index ba705be8..8c993104 100644 --- a/lib/g3-dpi/src/protocol/ldap.rs +++ b/lib/g3-dpi/src/protocol/ldap.rs @@ -3,9 +3,7 @@ * Copyright 2026 G3-OSS developers. */ -use g3_types::net::{ - LdapMessageId, LdapMessageIdParseError, LdapMessageLength, LdapMessageLengthParseError, -}; +use g3_types::codec::{LdapLength, LdapLengthParseError, LdapMessageId}; use super::{MaybeProtocol, Protocol, ProtocolInspectError, ProtocolInspectState}; use crate::ProtocolInspectionSizeLimit; @@ -43,7 +41,7 @@ impl ProtocolInspectState { let header_len; let content_len; - match LdapMessageLength::parse(&data[1..]) { + match LdapLength::parse(&data[1..]) { Ok(v) => { if v.value() > size_limit.ldap_request_msg as u64 { self.exclude_current(); @@ -57,7 +55,7 @@ impl ProtocolInspectState { )); } } - Err(LdapMessageLengthParseError::NeedMoreData(n)) => { + Err(LdapLengthParseError::NeedMoreData(n)) => { return Err(ProtocolInspectError::NeedMoreData(n)); } Err(_) => { @@ -66,7 +64,7 @@ impl ProtocolInspectState { } } - let content = &data[header_len..]; + let content = &data[header_len..header_len + content_len]; match LdapMessageId::parse(content) { Ok(id) => { if id.value() == 0 { @@ -74,9 +72,6 @@ impl ProtocolInspectState { return Ok(None); } } - Err(LdapMessageIdParseError::NeedMoreData(n)) => { - return Err(ProtocolInspectError::NeedMoreData(n)); - } Err(_) => { self.exclude_current(); return Ok(None); diff --git a/lib/g3-types/src/auth/error.rs b/lib/g3-types/src/auth/error.rs index 8be32011..340e3f8b 100644 --- a/lib/g3-types/src/auth/error.rs +++ b/lib/g3-types/src/auth/error.rs @@ -22,6 +22,10 @@ pub enum UserAuthError { BlockedUser(Duration), #[error("src addr {0} is blocked")] BlockedSrcIp(SocketAddr), + #[error("remote timeout")] + RemoteTimeout, + #[error("remote error")] + RemoteError, } impl UserAuthError { diff --git a/lib/g3-types/src/codec/ber/integer.rs b/lib/g3-types/src/codec/ber/integer.rs new file mode 100644 index 00000000..0fdf5733 --- /dev/null +++ b/lib/g3-types/src/codec/ber/integer.rs @@ -0,0 +1,251 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +use thiserror::Error; + +use super::{BerLength, BerLengthParseError}; + +#[derive(Debug, PartialEq, Eq, Error)] +pub enum BerIntegerParseError { + #[error("need {0} bytes more data")] + NeedMoreData(usize), + #[error("invalid ber type")] + InvalidType, + #[error("invalid ber length")] + TooLargeLength, + #[error("indefinite length")] + IndefiniteLength, + #[error("invalid value bytes")] + InvalidValueBytes, +} + +impl From for BerIntegerParseError { + fn from(value: BerLengthParseError) -> Self { + match value { + BerLengthParseError::NeedMoreData(n) => BerIntegerParseError::NeedMoreData(n), + BerLengthParseError::TooLargeValue => BerIntegerParseError::TooLargeLength, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct BerInteger { + value: i64, + encoded_len: usize, +} + +impl BerInteger { + pub fn parse(data: &[u8]) -> Result { + Self::parse_with_identifier(data, 0x02) + } + + pub fn parse_enumerated_value(data: &[u8]) -> Result { + Self::parse_with_identifier(data, 0x0a) + } + + fn parse_with_identifier(data: &[u8], identifier: u8) -> Result { + if data.is_empty() { + return Err(BerIntegerParseError::NeedMoreData(1)); + } + if data[0] != identifier { + return Err(BerIntegerParseError::InvalidType); + } + + let length = BerLength::parse(&data[1..])?; + if length.indefinite() { + return Err(BerIntegerParseError::IndefiniteLength); + } + + let offset = 1 + length.encoded_len(); + let left = &data[offset..]; + let value0 = left[0] & 0x7F; + let mut integer = match length.value() { + 1 => { + if left.is_empty() { + return Err(BerIntegerParseError::NeedMoreData(1)); + } + BerInteger { + value: i64::from(value0), + encoded_len: offset + 1, + } + } + 2 => { + if left.len() < 2 { + return Err(BerIntegerParseError::NeedMoreData(2 - left.len())); + } + BerInteger { + value: i16::from_be_bytes([value0, left[1]]) as i64, + encoded_len: offset + 2, + } + } + 3 => { + if left.len() < 3 { + return Err(BerIntegerParseError::NeedMoreData(3 - left.len())); + } + BerInteger { + value: i32::from_be_bytes([0, value0, left[1], left[2]]) as i64, + encoded_len: offset + 3, + } + } + 4 => { + if left.len() < 4 { + return Err(BerIntegerParseError::NeedMoreData(4 - left.len())); + } + BerInteger { + value: i32::from_be_bytes([value0, left[1], left[2], left[3]]) as i64, + encoded_len: offset + 4, + } + } + 5 => { + if left.len() < 5 { + return Err(BerIntegerParseError::NeedMoreData(5 - left.len())); + } + BerInteger { + value: i64::from_be_bytes([ + 0, 0, 0, value0, left[1], left[2], left[3], left[4], + ]), + encoded_len: offset + 5, + } + } + 6 => { + if left.len() < 6 { + return Err(BerIntegerParseError::NeedMoreData(6 - left.len())); + } + BerInteger { + value: i64::from_be_bytes([ + 0, 0, value0, left[1], left[2], left[3], left[4], left[5], + ]), + encoded_len: offset + 6, + } + } + 7 => { + if left.len() < 7 { + return Err(BerIntegerParseError::NeedMoreData(7 - left.len())); + } + BerInteger { + value: i64::from_be_bytes([ + 0, value0, left[1], left[2], left[3], left[4], left[5], left[6], + ]), + encoded_len: offset + 7, + } + } + 8 => { + if left.len() < 8 { + return Err(BerIntegerParseError::NeedMoreData(8 - left.len())); + } + BerInteger { + value: i64::from_be_bytes([ + value0, left[1], left[2], left[3], left[4], left[5], left[6], left[7], + ]), + encoded_len: offset + 8, + } + } + _ => return Err(BerIntegerParseError::InvalidValueBytes), + }; + if left[0] >> 7 != 0 { + integer.value = 0 - integer.value; + } + Ok(integer) + } + + #[inline] + pub fn encoded_len(&self) -> usize { + self.encoded_len + } + + #[inline] + pub fn value(&self) -> i64 { + self.value + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse() { + let e = BerInteger::parse(&[0x02]).unwrap_err(); + assert_eq!(e, BerIntegerParseError::NeedMoreData(1)); + + let e = BerInteger::parse(&[0x03, 0x01, 0x02]).unwrap_err(); + assert_eq!(e, BerIntegerParseError::InvalidType); + let e = BerInteger::parse(&[0x02, 0x00, 0x02]).unwrap_err(); + assert_eq!(e, BerIntegerParseError::InvalidValueBytes); + + let v = BerInteger::parse(&[0x02, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, 2); + assert_eq!(v.encoded_len(), 3); + let v = BerInteger::parse(&[0x02, 0x81, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, 2); + assert_eq!(v.encoded_len(), 4); + let v = BerInteger::parse(&[0x02, 0x01, 0x82]).unwrap(); + assert_eq!(v.value, -2); + assert_eq!(v.encoded_len(), 3); + + let v = BerInteger::parse(&[0x02, 0x02, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, 0x0102); + assert_eq!(v.encoded_len(), 4); + let e = BerInteger::parse(&[0x02, 0x02, 0x01]).unwrap_err(); + assert_eq!(e, BerIntegerParseError::NeedMoreData(1)); + let v = BerInteger::parse(&[0x02, 0x02, 0x81, 0x02]).unwrap(); + assert_eq!(v.value, -0x0102); + assert_eq!(v.encoded_len(), 4); + + let v = BerInteger::parse(&[0x02, 0x03, 0x01, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, 0x010102); + assert_eq!(v.encoded_len(), 5); + let e = BerInteger::parse(&[0x02, 0x03, 0x01]).unwrap_err(); + assert_eq!(e, BerIntegerParseError::NeedMoreData(2)); + let v = BerInteger::parse(&[0x02, 0x03, 0x81, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, -0x010102); + assert_eq!(v.encoded_len(), 5); + + let v = BerInteger::parse(&[0x02, 0x04, 0x01, 0x01, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, 0x01010102); + assert_eq!(v.encoded_len(), 6); + let e = BerInteger::parse(&[0x02, 0x04, 0x01, 0x01]).unwrap_err(); + assert_eq!(e, BerIntegerParseError::NeedMoreData(2)); + let v = BerInteger::parse(&[0x02, 0x04, 0x81, 0x01, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, -0x01010102); + assert_eq!(v.encoded_len(), 6); + + let v = BerInteger::parse(&[0x02, 0x05, 0x01, 0x01, 0x01, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, 0x0101010102); + assert_eq!(v.encoded_len(), 7); + let e = BerInteger::parse(&[0x02, 0x05, 0x01, 0x01]).unwrap_err(); + assert_eq!(e, BerIntegerParseError::NeedMoreData(3)); + let v = BerInteger::parse(&[0x02, 0x05, 0x81, 0x01, 0x01, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, -0x0101010102); + assert_eq!(v.encoded_len(), 7); + + let v = BerInteger::parse(&[0x02, 0x06, 0, 0x01, 0x01, 0x01, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, 0x0101010102); + assert_eq!(v.encoded_len(), 8); + let e = BerInteger::parse(&[0x02, 0x06, 0x01, 0x01]).unwrap_err(); + assert_eq!(e, BerIntegerParseError::NeedMoreData(4)); + let v = BerInteger::parse(&[0x02, 0x06, 0x80, 0x01, 0x01, 0x01, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, -0x0101010102); + assert_eq!(v.encoded_len(), 8); + + let v = BerInteger::parse(&[0x02, 0x07, 0, 0, 0x01, 0x01, 0x01, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, 0x0101010102); + assert_eq!(v.encoded_len(), 9); + let e = BerInteger::parse(&[0x02, 0x07, 0x01, 0x01]).unwrap_err(); + assert_eq!(e, BerIntegerParseError::NeedMoreData(5)); + let v = BerInteger::parse(&[0x02, 0x07, 0x80, 0, 0x01, 0x01, 0x01, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, -0x0101010102); + assert_eq!(v.encoded_len(), 9); + + let v = BerInteger::parse(&[0x02, 0x08, 0, 0, 0, 0x01, 0x01, 0x01, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, 0x0101010102); + assert_eq!(v.encoded_len(), 10); + let e = BerInteger::parse(&[0x02, 0x08, 0x01, 0x01]).unwrap_err(); + assert_eq!(e, BerIntegerParseError::NeedMoreData(6)); + let v = BerInteger::parse(&[0x02, 0x08, 0x80, 0, 0, 0x01, 0x01, 0x01, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, -0x0101010102); + assert_eq!(v.encoded_len(), 10); + } +} diff --git a/lib/g3-types/src/codec/ber/length.rs b/lib/g3-types/src/codec/ber/length.rs new file mode 100644 index 00000000..31b87464 --- /dev/null +++ b/lib/g3-types/src/codec/ber/length.rs @@ -0,0 +1,274 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +#[derive(Debug, PartialEq, Eq)] +pub enum BerLengthParseError { + NeedMoreData(usize), + TooLargeValue, +} + +#[derive(Debug)] +pub struct BerLength { + value: u64, + indefinite: bool, + encoded_len: usize, +} + +impl BerLength { + /// Try to parse a BER length value with LDAP constraints from the buffer + pub fn parse(data: &[u8]) -> Result { + if data.is_empty() { + return Err(BerLengthParseError::NeedMoreData(1)); + } + + if data[0] & 0x80 == 0 { + return Ok(BerLength { + value: data[0] as u64, + indefinite: false, + encoded_len: 1, + }); + } + + match data[0] & 0x7F { + 0 => Ok(BerLength { + value: 0, + indefinite: true, + encoded_len: 1, + }), + 1 => { + if data.len() < 2 { + return Err(BerLengthParseError::NeedMoreData(2 - data.len())); + } + Ok(BerLength { + value: data[1] as u64, + indefinite: false, + encoded_len: 2, + }) + } + 2 => { + if data.len() < 3 { + return Err(BerLengthParseError::NeedMoreData(3 - data.len())); + } + Ok(BerLength { + value: u16::from_be_bytes([data[1], data[2]]) as u64, + indefinite: false, + encoded_len: 3, + }) + } + 3 => { + if data.len() < 4 { + return Err(BerLengthParseError::NeedMoreData(4 - data.len())); + } + Ok(BerLength { + value: u32::from_be_bytes([0, data[1], data[2], data[3]]) as u64, + indefinite: false, + encoded_len: 4, + }) + } + 4 => { + if data.len() < 5 { + return Err(BerLengthParseError::NeedMoreData(5 - data.len())); + } + Ok(BerLength { + value: u32::from_be_bytes([data[1], data[2], data[3], data[4]]) as u64, + indefinite: false, + encoded_len: 5, + }) + } + 5 => { + if data.len() < 6 { + return Err(BerLengthParseError::NeedMoreData(6 - data.len())); + } + Ok(BerLength { + value: u64::from_be_bytes([ + 0, 0, 0, data[1], data[2], data[3], data[4], data[5], + ]), + indefinite: false, + encoded_len: 6, + }) + } + 6 => { + if data.len() < 7 { + return Err(BerLengthParseError::NeedMoreData(7 - data.len())); + } + Ok(BerLength { + value: u64::from_be_bytes([ + 0, 0, data[1], data[2], data[3], data[4], data[5], data[6], + ]), + indefinite: false, + encoded_len: 7, + }) + } + 7 => { + if data.len() < 8 { + return Err(BerLengthParseError::NeedMoreData(8 - data.len())); + } + Ok(BerLength { + value: u64::from_be_bytes([ + 0, data[1], data[2], data[3], data[4], data[5], data[6], data[7], + ]), + indefinite: false, + encoded_len: 8, + }) + } + 8 => { + if data.len() < 9 { + return Err(BerLengthParseError::NeedMoreData(9 - data.len())); + } + Ok(BerLength { + value: u64::from_be_bytes([ + data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8], + ]), + indefinite: false, + encoded_len: 9, + }) + } + _ => Err(BerLengthParseError::TooLargeValue), + } + } + + #[inline] + pub fn indefinite(&self) -> bool { + self.indefinite + } + + #[inline] + pub fn encoded_len(&self) -> usize { + self.encoded_len + } + + #[inline] + pub fn value(&self) -> u64 { + self.value + } +} + +#[derive(Default)] +pub struct BerLengthEncoder { + buf: [u8; 9], + offset: usize, +} + +impl BerLengthEncoder { + pub fn encode(&mut self, value: usize) -> &[u8] { + if value <= 0x7F { + self.offset = 8; + self.buf[8] = (value & 0x7F) as u8; + return &self.buf[8..]; + } + + let bytes = (value as u64).to_be_bytes(); + unsafe { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), self.buf[1..].as_mut_ptr(), 8); + } + let unused_bits = value.leading_zeros() as usize; + self.offset = unused_bits / 8; + self.buf[self.offset] = 8 - self.offset as u8; + &self.buf[self.offset..] + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse() { + assert!(BerLength::parse(b"").is_err()); + + let v = BerLength::parse(&[0x80]).unwrap(); + assert_eq!(v.encoded_len(), 1); + assert!(v.indefinite()); + + let v = BerLength::parse(&[0x02]).unwrap(); + assert_eq!(v.value, 2); + assert_eq!(v.encoded_len(), 1); + + let v = BerLength::parse(&[0x81]).unwrap_err(); + assert_eq!(v, BerLengthParseError::NeedMoreData(1)); + let v = BerLength::parse(&[0x81, 0x01]).unwrap(); + assert_eq!(v.value, 0x01); + assert_eq!(v.encoded_len(), 2); + + let v = BerLength::parse(&[0x82, 0]).unwrap_err(); + assert_eq!(v, BerLengthParseError::NeedMoreData(1)); + let v = BerLength::parse(&[0x82, 0, 0x01]).unwrap(); + assert_eq!(v.value, 0x01); + assert_eq!(v.encoded_len(), 3); + + let v = BerLength::parse(&[0x83, 0]).unwrap_err(); + assert_eq!(v, BerLengthParseError::NeedMoreData(2)); + let v = BerLength::parse(&[0x83, 0, 0, 0x01]).unwrap(); + assert_eq!(v.value, 0x01); + assert_eq!(v.encoded_len(), 4); + + let v = BerLength::parse(&[0x84, 0, 0]).unwrap_err(); + assert_eq!(v, BerLengthParseError::NeedMoreData(2)); + let v = BerLength::parse(&[0x84, 0, 0, 0, 0x01]).unwrap(); + assert_eq!(v.value, 0x01); + assert_eq!(v.encoded_len(), 5); + + let v = BerLength::parse(&[0x85, 0, 0]).unwrap_err(); + assert_eq!(v, BerLengthParseError::NeedMoreData(3)); + let v = BerLength::parse(&[0x85, 0, 0, 0, 0, 0x01]).unwrap(); + assert_eq!(v.value, 0x01); + assert_eq!(v.encoded_len(), 6); + + let v = BerLength::parse(&[0x86, 0, 0, 0]).unwrap_err(); + assert_eq!(v, BerLengthParseError::NeedMoreData(3)); + let v = BerLength::parse(&[0x86, 0, 0, 0, 0, 0, 0x01]).unwrap(); + assert_eq!(v.value, 0x01); + assert_eq!(v.encoded_len(), 7); + + let v = BerLength::parse(&[0x87, 0, 0, 0]).unwrap_err(); + assert_eq!(v, BerLengthParseError::NeedMoreData(4)); + let v = BerLength::parse(&[0x87, 0, 0, 0, 0, 0, 0, 0x01]).unwrap(); + assert_eq!(v.value, 0x01); + assert_eq!(v.encoded_len(), 8); + + let v = BerLength::parse(&[0x88, 0, 0, 0]).unwrap_err(); + assert_eq!(v, BerLengthParseError::NeedMoreData(5)); + let v = BerLength::parse(&[0x88, 0, 0, 0, 0, 0, 0, 0, 0x01]).unwrap(); + assert_eq!(v.value, 0x01); + assert_eq!(v.encoded_len(), 9); + + let v = BerLength::parse(&[0x89, 0, 0, 0]).unwrap_err(); + assert_eq!(v, BerLengthParseError::TooLargeValue); + } + + #[test] + fn encode_32() { + let mut encoder = BerLengthEncoder::default(); + assert_eq!(encoder.encode(0), &[0]); + assert_eq!(encoder.encode(1), &[1]); + assert_eq!(encoder.encode(0x7F), &[0x7F]); + assert_eq!(encoder.encode(0x80), &[0x01, 0x80]); + assert_eq!(encoder.encode(0x0100), &[0x02, 0x01, 0x00]); + assert_eq!(encoder.encode(0x010000), &[0x03, 0x01, 0x00, 0x00]); + assert_eq!(encoder.encode(0x01000000), &[0x04, 0x01, 0x00, 0x00, 0x00]); + } + + #[cfg(target_pointer_width = "64")] + #[test] + fn encode_64() { + let mut encoder = BerLengthEncoder::default(); + assert_eq!( + encoder.encode(0x0100000000), + &[0x05, 0x01, 0x00, 0x00, 0x00, 0x00] + ); + assert_eq!( + encoder.encode(0x010000000000), + &[0x06, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00] + ); + assert_eq!( + encoder.encode(0x01000000000000), + &[0x07, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ); + assert_eq!( + encoder.encode(0x0100000000000000), + &[0x08, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ); + } +} diff --git a/lib/g3-types/src/codec/ber/mod.rs b/lib/g3-types/src/codec/ber/mod.rs new file mode 100644 index 00000000..bf46edbb --- /dev/null +++ b/lib/g3-types/src/codec/ber/mod.rs @@ -0,0 +1,10 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +mod length; +pub use length::{BerLength, BerLengthEncoder, BerLengthParseError}; + +mod integer; +pub use integer::{BerInteger, BerIntegerParseError}; diff --git a/lib/g3-types/src/codec/ldap/length.rs b/lib/g3-types/src/codec/ldap/length.rs new file mode 100644 index 00000000..6c19061d --- /dev/null +++ b/lib/g3-types/src/codec/ldap/length.rs @@ -0,0 +1,72 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +use thiserror::Error; + +use crate::codec::ber::{BerLength, BerLengthParseError}; + +#[derive(Debug, PartialEq, Eq, Error)] +pub enum LdapLengthParseError { + #[error("need {0} bytes more data")] + NeedMoreData(usize), + #[error("too large value")] + TooLargeValue, + #[error("indefinite value")] + IndefiniteValue, +} + +impl From for LdapLengthParseError { + fn from(value: BerLengthParseError) -> Self { + match value { + BerLengthParseError::NeedMoreData(needed) => Self::NeedMoreData(needed), + BerLengthParseError::TooLargeValue => Self::TooLargeValue, + } + } +} + +#[derive(Debug)] +pub struct LdapLength { + value: u64, + encoded_len: usize, +} + +impl LdapLength { + /// Try to parse a BER length value with LDAP constraints from the buffer + pub fn parse(data: &[u8]) -> Result { + let ber_len = BerLength::parse(data)?; + if ber_len.indefinite() { + return Err(LdapLengthParseError::IndefiniteValue); + } + Ok(LdapLength { + value: ber_len.value(), + encoded_len: ber_len.encoded_len(), + }) + } + + #[inline] + pub fn encoded_len(&self) -> usize { + self.encoded_len + } + + #[inline] + pub fn value(&self) -> u64 { + self.value + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse() { + assert!(LdapLength::parse(b"").is_err()); + assert!(LdapLength::parse(&[0x80]).is_err()); + + let v = LdapLength::parse(&[0x02]).unwrap(); + assert_eq!(v.value, 2); + assert_eq!(v.encoded_len(), 1); + } +} diff --git a/lib/g3-types/src/codec/ldap/message.rs b/lib/g3-types/src/codec/ldap/message.rs new file mode 100644 index 00000000..76774312 --- /dev/null +++ b/lib/g3-types/src/codec/ldap/message.rs @@ -0,0 +1,110 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +use thiserror::Error; + +use super::{LdapLength, LdapLengthParseError, LdapMessageId, LdapMessageIdParseError}; + +#[derive(Debug, Error)] +pub enum LdapMessageParseError { + #[error("need {0} bytes more data")] + NeedMoreData(usize), + #[error("invalid message ber type")] + InvalidBerType, + #[error("invalid message length value")] + InvalidMessageLength, + #[error("invalid message id: {0}")] + InvalidMessageId(#[from] LdapMessageIdParseError), +} + +impl From for LdapMessageParseError { + fn from(value: LdapLengthParseError) -> Self { + match value { + LdapLengthParseError::NeedMoreData(n) => LdapMessageParseError::NeedMoreData(n), + LdapLengthParseError::TooLargeValue | LdapLengthParseError::IndefiniteValue => { + LdapMessageParseError::InvalidMessageLength + } + } + } +} + +pub struct LdapMessage<'a> { + id: u32, + payload: &'a [u8], + encoded_size: usize, +} + +impl<'a> LdapMessage<'a> { + pub fn parse(data: &'a [u8], max_message_length: usize) -> Result { + if data.is_empty() { + return Err(LdapMessageParseError::NeedMoreData(1)); + } + + if data[0] != 0x30 { + return Err(LdapMessageParseError::InvalidBerType); + } + let mut offset = 1usize; + + let length = LdapLength::parse(&data[offset..])?; + offset += length.encoded_len(); + let message_length = length.value(); + if message_length > max_message_length as u64 { + return Err(LdapMessageParseError::InvalidMessageLength); + } + let message_length = message_length as usize; + + let left = &data[offset..]; + if left.len() < message_length { + return Err(LdapMessageParseError::NeedMoreData( + message_length - left.len(), + )); + } + + let id = LdapMessageId::parse(&left[..message_length])?; + let message_id = id.value(); + + Ok(LdapMessage { + id: message_id, + payload: &left[id.encoded_len()..message_length], + encoded_size: offset + message_length, + }) + } + + #[inline] + pub fn id(&self) -> u32 { + self.id + } + + #[inline] + pub fn payload(&self) -> &'a [u8] { + self.payload + } + + #[inline] + pub fn encoded_size(&self) -> usize { + self.encoded_size + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn simple_bind_response() { + let data = [ + 0x30, 0x0c, // Begin the LDAPMessage sequence + 0x02, 0x01, 0x01, // The message ID (integer value 1) + 0x61, 0x07, // Begin the bind response protocol op + 0x0a, 0x01, 0x00, // success result code (enumerated value 0) + 0x04, 0x00, // No matched DN (0-byte octet string) + 0x04, 0x00, // No diagnostic message (0-byte octet string) + ]; + let message = LdapMessage::parse(&data, 128).unwrap(); + assert_eq!(message.id(), 1); + assert_eq!(message.payload(), &data[5..]); + assert_eq!(message.encoded_size(), data.len()); + } +} diff --git a/lib/g3-types/src/codec/ldap/message_id.rs b/lib/g3-types/src/codec/ldap/message_id.rs new file mode 100644 index 00000000..1e0140f6 --- /dev/null +++ b/lib/g3-types/src/codec/ldap/message_id.rs @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +use thiserror::Error; + +use crate::codec::{BerInteger, BerIntegerParseError}; + +#[derive(Debug, PartialEq, Eq, Error)] +pub enum LdapMessageIdParseError { + #[error("invalid integer value: {0}")] + InvalidIntegerValue(#[from] BerIntegerParseError), + #[error("negative value")] + NegativeValue, + #[error("too large value")] + TooLargeValue, +} + +#[derive(Debug)] +pub struct LdapMessageId { + value: u32, + encoded_len: usize, +} + +impl LdapMessageId { + /// Try to parse a BER integer value with LDAP constraints from the buffer + pub fn parse(data: &[u8]) -> Result { + let ber_integer = BerInteger::parse(data)?; + if ber_integer.value() < 0 { + return Err(LdapMessageIdParseError::NegativeValue); + } + let value = u32::try_from(ber_integer.value()) + .map_err(|_| LdapMessageIdParseError::TooLargeValue)?; + Ok(LdapMessageId { + value, + encoded_len: ber_integer.encoded_len(), + }) + } + + #[inline] + pub fn encoded_len(&self) -> usize { + self.encoded_len + } + + #[inline] + pub fn value(&self) -> u32 { + self.value + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse() { + let v = LdapMessageId::parse(&[0x02, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, 2); + assert_eq!(v.encoded_len(), 3); + let v = LdapMessageId::parse(&[0x02, 0x81, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, 2); + assert_eq!(v.encoded_len(), 4); + let e = LdapMessageId::parse(&[0x02, 0x01, 0x82]).unwrap_err(); + assert_eq!(e, LdapMessageIdParseError::NegativeValue); + + let v = LdapMessageId::parse(&[0x02, 0x02, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, 0x0102); + assert_eq!(v.encoded_len(), 4); + let e = LdapMessageId::parse(&[0x02, 0x02, 0x81, 0x02]).unwrap_err(); + assert_eq!(e, LdapMessageIdParseError::NegativeValue); + + let v = LdapMessageId::parse(&[0x02, 0x03, 0x01, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, 0x010102); + assert_eq!(v.encoded_len(), 5); + let e = LdapMessageId::parse(&[0x02, 0x03, 0x81, 0x01, 0x02]).unwrap_err(); + assert_eq!(e, LdapMessageIdParseError::NegativeValue); + + let v = LdapMessageId::parse(&[0x02, 0x04, 0x01, 0x01, 0x01, 0x02]).unwrap(); + assert_eq!(v.value, 0x01010102); + assert_eq!(v.encoded_len(), 6); + let e = LdapMessageId::parse(&[0x02, 0x04, 0x81, 0x01, 0x01, 0x02]).unwrap_err(); + assert_eq!(e, LdapMessageIdParseError::NegativeValue); + } +} diff --git a/lib/g3-types/src/codec/ldap/mod.rs b/lib/g3-types/src/codec/ldap/mod.rs new file mode 100644 index 00000000..aca77007 --- /dev/null +++ b/lib/g3-types/src/codec/ldap/mod.rs @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +mod length; +pub use length::{LdapLength, LdapLengthParseError}; + +mod sequence; +pub use sequence::{LdapSequence, LdapSequenceParseError}; + +mod message_id; +pub use message_id::{LdapMessageId, LdapMessageIdParseError}; + +mod message; +pub use message::{LdapMessage, LdapMessageParseError}; + +mod result; +pub use result::{LdapResult, LdapResultParseError}; diff --git a/lib/g3-types/src/codec/ldap/result.rs b/lib/g3-types/src/codec/ldap/result.rs new file mode 100644 index 00000000..16ca93ca --- /dev/null +++ b/lib/g3-types/src/codec/ldap/result.rs @@ -0,0 +1,107 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +use thiserror::Error; + +use crate::codec::{BerInteger, BerIntegerParseError, LdapSequence, LdapSequenceParseError}; + +#[derive(Debug, Error)] +pub enum LdapResultParseError { + #[error("invalid result code value: {0}")] + InvalidResultCode(#[from] BerIntegerParseError), + #[error("out of range result code")] + OutOfRangeResultCode, + #[error("invalid matched dn string: {0}")] + InvalidMatchedDn(LdapSequenceParseError), + #[error("invalid diagnostic message string: {0}")] + InvalidDiagnosticMessage(LdapSequenceParseError), + #[error("invalid referrals sequence: {0}")] + InvalidReferralsSequence(LdapSequenceParseError), +} + +pub struct LdapResult<'a> { + result_code: u16, + matched_dn: &'a [u8], + diagnostic_message: &'a [u8], + #[allow(unused)] + referral_sequence: &'a [u8], // only if code is REFERRAL 0x0a + encoded_len: usize, +} + +impl<'a> LdapResult<'a> { + pub fn parse(data: &'a [u8]) -> Result { + let code = BerInteger::parse_enumerated_value(data)?; + let result_code = + u16::try_from(code.value()).map_err(|_| LdapResultParseError::OutOfRangeResultCode)?; + let mut offset = code.encoded_len(); + + let matched_dn = LdapSequence::parse_octet_string(&data[offset..]) + .map_err(LdapResultParseError::InvalidMatchedDn)?; + offset += matched_dn.encoded_len(); + + let diagnostic_message = LdapSequence::parse_octet_string(&data[offset..]) + .map_err(LdapResultParseError::InvalidDiagnosticMessage)?; + offset += diagnostic_message.encoded_len(); + + if result_code == 10 { + let referral_sequence = LdapSequence::parse_referrals_sequence(&data[offset..]) + .map_err(LdapResultParseError::InvalidReferralsSequence)?; + Ok(LdapResult { + result_code, + matched_dn: matched_dn.data(), + diagnostic_message: diagnostic_message.data(), + referral_sequence: referral_sequence.data(), + encoded_len: offset + referral_sequence.encoded_len(), + }) + } else { + Ok(LdapResult { + result_code, + matched_dn: matched_dn.data(), + diagnostic_message: diagnostic_message.data(), + referral_sequence: b"", + encoded_len: offset, + }) + } + } + + #[inline] + pub fn result_code(&self) -> u16 { + self.result_code + } + + #[inline] + pub fn matched_dn(&self) -> &[u8] { + self.matched_dn + } + + #[inline] + pub fn diagnostic_message(&self) -> &[u8] { + self.diagnostic_message + } + + #[inline] + pub fn encoded_len(&self) -> usize { + self.encoded_len + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_simple() { + let data = &[ + 0x0a, 0x01, 0x00, // result code 0 + 0x04, 0x00, // no matched dn + 0x04, 0x00, // no diagnostic message + ]; + let v = LdapResult::parse(data).unwrap(); + assert_eq!(v.result_code(), 0); + assert!(v.matched_dn().is_empty()); + assert!(v.diagnostic_message().is_empty()); + assert_eq!(v.encoded_len(), 7); + } +} diff --git a/lib/g3-types/src/codec/ldap/sequence.rs b/lib/g3-types/src/codec/ldap/sequence.rs new file mode 100644 index 00000000..8ff10d28 --- /dev/null +++ b/lib/g3-types/src/codec/ldap/sequence.rs @@ -0,0 +1,117 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2026 G3-OSS developers. + */ + +use thiserror::Error; + +use crate::codec::{BerLength, BerLengthParseError}; + +#[derive(Debug, Error)] +pub enum LdapSequenceParseError { + #[error("need {0} bytes more data")] + NeedMoreData(usize), + #[error("invalid ber type")] + InvalidType, + #[error("invalid ber length")] + TooLargeLength, + #[error("indefinite length")] + IndefiniteLength, +} + +impl From for LdapSequenceParseError { + fn from(value: BerLengthParseError) -> Self { + match value { + BerLengthParseError::NeedMoreData(n) => LdapSequenceParseError::NeedMoreData(n), + BerLengthParseError::TooLargeValue => LdapSequenceParseError::TooLargeLength, + } + } +} + +pub struct LdapSequence<'a> { + data: &'a [u8], + encoded_len: usize, +} + +impl<'a> LdapSequence<'a> { + pub fn parse_octet_string(data: &'a [u8]) -> Result { + Self::parse_with_identifier(data, 0x04) + } + + pub fn parse_referrals_sequence(data: &'a [u8]) -> Result { + Self::parse_with_identifier(data, 0xa3) + } + + pub fn parse_bind_response(data: &'a [u8]) -> Result { + Self::parse_with_identifier(data, 0x61) + } + + pub fn parse_extended_response(data: &'a [u8]) -> Result { + Self::parse_with_identifier(data, 0x78) + } + + pub fn parse_extended_response_oid(data: &'a [u8]) -> Result { + Self::parse_with_identifier(data, 0x8a) + } + + fn parse_with_identifier( + data: &'a [u8], + identifier: u8, + ) -> Result { + if data.is_empty() { + return Err(LdapSequenceParseError::NeedMoreData(1)); + } + if data[0] != identifier { + return Err(LdapSequenceParseError::InvalidType); + } + + let ber_length = BerLength::parse(&data[1..])?; + if ber_length.indefinite() { + return Err(LdapSequenceParseError::IndefiniteLength); + } + + let offset = 1 + ber_length.encoded_len(); + if ber_length.value() == 0 { + Ok(LdapSequence { + data: b"", + encoded_len: offset, + }) + } else if ber_length.value() + offset as u64 > data.len() as u64 { + Err(LdapSequenceParseError::TooLargeLength) + } else { + let encoded_len = ber_length.value() as usize + offset; + Ok(LdapSequence { + data: &data[offset..encoded_len], + encoded_len, + }) + } + } + + #[inline] + pub fn data(&self) -> &'a [u8] { + self.data + } + + #[inline] + pub fn encoded_len(&self) -> usize { + self.encoded_len + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse() { + assert!(LdapSequence::parse_octet_string(b"").is_err()); + + let v = LdapSequence::parse_octet_string(&[0x04, 0x00]).unwrap(); + assert_eq!(v.data, b""); + assert_eq!(v.encoded_len(), 2); + + let v = LdapSequence::parse_octet_string(&[0x04, 0x02, 0x01, 0x02]).unwrap(); + assert_eq!(v.data, &[0x01, 0x02]); + assert_eq!(v.encoded_len(), 4); + } +} diff --git a/lib/g3-types/src/codec/mod.rs b/lib/g3-types/src/codec/mod.rs index 19a7e1f8..801c57ae 100644 --- a/lib/g3-types/src/codec/mod.rs +++ b/lib/g3-types/src/codec/mod.rs @@ -5,3 +5,9 @@ mod tlv; pub use tlv::{T1L2BVParse, TlvParse}; + +mod ber; +pub use ber::*; + +mod ldap; +pub use ldap::*; diff --git a/lib/g3-types/src/net/host.rs b/lib/g3-types/src/net/host.rs index 5f9b12bd..c7e0ac60 100644 --- a/lib/g3-types/src/net/host.rs +++ b/lib/g3-types/src/net/host.rs @@ -99,8 +99,8 @@ impl fmt::Display for Host { } } -impl From for Host { - fn from(v: url::Host) -> Self { +impl> From> for Host { + fn from(v: url::Host) -> Self { match v { url::Host::Ipv4(ip4) => Host::Ip(IpAddr::V4(ip4)), url::Host::Ipv6(ip6) => Host::Ip(IpAddr::V6(ip6)), diff --git a/lib/g3-types/src/net/ldap/message/id.rs b/lib/g3-types/src/net/ldap/message/id.rs deleted file mode 100644 index 672ea5e0..00000000 --- a/lib/g3-types/src/net/ldap/message/id.rs +++ /dev/null @@ -1,147 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2026 G3-OSS developers. - */ - -#[derive(Debug, PartialEq, Eq)] -pub enum LdapMessageIdParseError { - NeedMoreData(usize), - InvalidBerType, - InvalidBytes, - TooLargeValue, - NegativeValue, -} - -#[derive(Debug)] -pub struct LdapMessageId { - value: u32, - encoded_len: usize, -} - -impl LdapMessageId { - /// Try to parse a BER integer value with LDAP constraints from the buffer - pub fn parse(data: &[u8]) -> Result { - if data.len() < 3 { - return Err(LdapMessageIdParseError::NeedMoreData(3 - data.len())); - } - - if data[0] != 0x02 { - return Err(LdapMessageIdParseError::InvalidBerType); - } - - match data[1] { - 1 => { - if data[2] & 0x80 != 0 { - return Err(LdapMessageIdParseError::NegativeValue); - } - - Ok(LdapMessageId { - value: data[2] as u32, - encoded_len: 3, - }) - } - 2 => { - if data[2] & 0x80 != 0 { - return Err(LdapMessageIdParseError::NegativeValue); - } - - if data.len() < 4 { - return Err(LdapMessageIdParseError::NeedMoreData(4 - data.len())); - } - - let value = u16::from_be_bytes([data[2], data[3]]) as u32; - Ok(LdapMessageId { - value, - encoded_len: 4, - }) - } - 3 => { - if data[2] & 0x80 != 0 { - return Err(LdapMessageIdParseError::NegativeValue); - } - - if data.len() < 5 { - return Err(LdapMessageIdParseError::NeedMoreData(5 - data.len())); - } - - let value = u32::from_be_bytes([0, data[2], data[3], data[4]]); - Ok(LdapMessageId { - value, - encoded_len: 5, - }) - } - 4 => { - if data[2] & 0x80 != 0 { - return Err(LdapMessageIdParseError::NegativeValue); - } - - if data.len() < 6 { - return Err(LdapMessageIdParseError::NeedMoreData(6 - data.len())); - } - - let value = u32::from_be_bytes([data[2], data[3], data[4], data[5]]); - Ok(LdapMessageId { - value, - encoded_len: 6, - }) - } - _ => Err(LdapMessageIdParseError::InvalidBytes), - } - } - - #[inline] - pub fn encoded_len(&self) -> usize { - self.encoded_len - } - - #[inline] - pub fn value(&self) -> u32 { - self.value - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse() { - let e = LdapMessageId::parse(&[0x02]).unwrap_err(); - assert_eq!(e, LdapMessageIdParseError::NeedMoreData(2)); - - let e = LdapMessageId::parse(&[0x03, 0x01, 0x02]).unwrap_err(); - assert_eq!(e, LdapMessageIdParseError::InvalidBerType); - let e = LdapMessageId::parse(&[0x02, 0x00, 0x02]).unwrap_err(); - assert_eq!(e, LdapMessageIdParseError::InvalidBytes); - - let v = LdapMessageId::parse(&[0x02, 0x01, 0x02]).unwrap(); - assert_eq!(v.value, 2); - assert_eq!(v.encoded_len(), 3); - let e = LdapMessageId::parse(&[0x02, 0x01, 0x82]).unwrap_err(); - assert_eq!(e, LdapMessageIdParseError::NegativeValue); - - let v = LdapMessageId::parse(&[0x02, 0x02, 0x01, 0x02]).unwrap(); - assert_eq!(v.value, 0x0102); - assert_eq!(v.encoded_len(), 4); - let e = LdapMessageId::parse(&[0x02, 0x02, 0x01]).unwrap_err(); - assert_eq!(e, LdapMessageIdParseError::NeedMoreData(1)); - let e = LdapMessageId::parse(&[0x02, 0x02, 0x81, 0x02]).unwrap_err(); - assert_eq!(e, LdapMessageIdParseError::NegativeValue); - - let v = LdapMessageId::parse(&[0x02, 0x03, 0x01, 0x01, 0x02]).unwrap(); - assert_eq!(v.value, 0x010102); - assert_eq!(v.encoded_len(), 5); - let e = LdapMessageId::parse(&[0x02, 0x03, 0x01]).unwrap_err(); - assert_eq!(e, LdapMessageIdParseError::NeedMoreData(2)); - let e = LdapMessageId::parse(&[0x02, 0x03, 0x81, 0x01, 0x02]).unwrap_err(); - assert_eq!(e, LdapMessageIdParseError::NegativeValue); - - let v = LdapMessageId::parse(&[0x02, 0x04, 0x01, 0x01, 0x01, 0x02]).unwrap(); - assert_eq!(v.value, 0x01010102); - assert_eq!(v.encoded_len(), 6); - let e = LdapMessageId::parse(&[0x02, 0x04, 0x01, 0x01]).unwrap_err(); - assert_eq!(e, LdapMessageIdParseError::NeedMoreData(2)); - let e = LdapMessageId::parse(&[0x02, 0x04, 0x81, 0x01, 0x01, 0x02]).unwrap_err(); - assert_eq!(e, LdapMessageIdParseError::NegativeValue); - } -} diff --git a/lib/g3-types/src/net/ldap/message/length.rs b/lib/g3-types/src/net/ldap/message/length.rs deleted file mode 100644 index ae0b1003..00000000 --- a/lib/g3-types/src/net/ldap/message/length.rs +++ /dev/null @@ -1,199 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2026 G3-OSS developers. - */ - -#[derive(Debug, PartialEq, Eq)] -pub enum LdapMessageLengthParseError { - NeedMoreData(usize), - TooLargeValue, -} - -#[derive(Debug)] -pub struct LdapMessageLength { - value: u64, - encoded_len: usize, -} - -impl LdapMessageLength { - /// Try to parse a BER length value with LDAP constraints from the buffer - pub fn parse(data: &[u8]) -> Result { - if data.is_empty() { - return Err(LdapMessageLengthParseError::NeedMoreData(1)); - } - - if data[0] & 0x80 == 0 { - return Ok(LdapMessageLength { - value: data[0] as u64, - encoded_len: 1, - }); - } - - match data[0] & 0x7F { - 0 => Ok(LdapMessageLength { - value: 0, - encoded_len: 1, - }), - 1 => { - if data.len() < 2 { - return Err(LdapMessageLengthParseError::NeedMoreData(2 - data.len())); - } - Ok(LdapMessageLength { - value: data[1] as u64, - encoded_len: 2, - }) - } - 2 => { - if data.len() < 3 { - return Err(LdapMessageLengthParseError::NeedMoreData(3 - data.len())); - } - Ok(LdapMessageLength { - value: u16::from_be_bytes([data[1], data[2]]) as u64, - encoded_len: 3, - }) - } - 3 => { - if data.len() < 4 { - return Err(LdapMessageLengthParseError::NeedMoreData(4 - data.len())); - } - Ok(LdapMessageLength { - value: u32::from_be_bytes([0, data[1], data[2], data[3]]) as u64, - encoded_len: 4, - }) - } - 4 => { - if data.len() < 5 { - return Err(LdapMessageLengthParseError::NeedMoreData(5 - data.len())); - } - Ok(LdapMessageLength { - value: u32::from_be_bytes([data[1], data[2], data[3], data[4]]) as u64, - encoded_len: 5, - }) - } - 5 => { - if data.len() < 6 { - return Err(LdapMessageLengthParseError::NeedMoreData(6 - data.len())); - } - Ok(LdapMessageLength { - value: u64::from_be_bytes([ - 0, 0, 0, data[1], data[2], data[3], data[4], data[5], - ]), - encoded_len: 6, - }) - } - 6 => { - if data.len() < 7 { - return Err(LdapMessageLengthParseError::NeedMoreData(7 - data.len())); - } - Ok(LdapMessageLength { - value: u64::from_be_bytes([ - 0, 0, data[1], data[2], data[3], data[4], data[5], data[6], - ]), - encoded_len: 7, - }) - } - 7 => { - if data.len() < 8 { - return Err(LdapMessageLengthParseError::NeedMoreData(8 - data.len())); - } - Ok(LdapMessageLength { - value: u64::from_be_bytes([ - 0, data[1], data[2], data[3], data[4], data[5], data[6], data[7], - ]), - encoded_len: 8, - }) - } - 8 => { - if data.len() < 9 { - return Err(LdapMessageLengthParseError::NeedMoreData(9 - data.len())); - } - Ok(LdapMessageLength { - value: u64::from_be_bytes([ - data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8], - ]), - encoded_len: 9, - }) - } - _ => Err(LdapMessageLengthParseError::TooLargeValue), - } - } - - #[inline] - pub fn encoded_len(&self) -> usize { - self.encoded_len - } - - #[inline] - pub fn value(&self) -> u64 { - self.value - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse() { - assert!(LdapMessageLength::parse(b"").is_err()); - - let v = LdapMessageLength::parse(&[0x02]).unwrap(); - assert_eq!(v.value, 2); - assert_eq!(v.encoded_len(), 1); - - let v = LdapMessageLength::parse(&[0x80]).unwrap(); - assert_eq!(v.value, 0); - assert_eq!(v.encoded_len(), 1); - - let v = LdapMessageLength::parse(&[0x81]).unwrap_err(); - assert_eq!(v, LdapMessageLengthParseError::NeedMoreData(1)); - let v = LdapMessageLength::parse(&[0x81, 0x01]).unwrap(); - assert_eq!(v.value, 0x01); - assert_eq!(v.encoded_len(), 2); - - let v = LdapMessageLength::parse(&[0x82, 0]).unwrap_err(); - assert_eq!(v, LdapMessageLengthParseError::NeedMoreData(1)); - let v = LdapMessageLength::parse(&[0x82, 0, 0x01]).unwrap(); - assert_eq!(v.value, 0x01); - assert_eq!(v.encoded_len(), 3); - - let v = LdapMessageLength::parse(&[0x83, 0]).unwrap_err(); - assert_eq!(v, LdapMessageLengthParseError::NeedMoreData(2)); - let v = LdapMessageLength::parse(&[0x83, 0, 0, 0x01]).unwrap(); - assert_eq!(v.value, 0x01); - assert_eq!(v.encoded_len(), 4); - - let v = LdapMessageLength::parse(&[0x84, 0, 0]).unwrap_err(); - assert_eq!(v, LdapMessageLengthParseError::NeedMoreData(2)); - let v = LdapMessageLength::parse(&[0x84, 0, 0, 0, 0x01]).unwrap(); - assert_eq!(v.value, 0x01); - assert_eq!(v.encoded_len(), 5); - - let v = LdapMessageLength::parse(&[0x85, 0, 0]).unwrap_err(); - assert_eq!(v, LdapMessageLengthParseError::NeedMoreData(3)); - let v = LdapMessageLength::parse(&[0x85, 0, 0, 0, 0, 0x01]).unwrap(); - assert_eq!(v.value, 0x01); - assert_eq!(v.encoded_len(), 6); - - let v = LdapMessageLength::parse(&[0x86, 0, 0, 0]).unwrap_err(); - assert_eq!(v, LdapMessageLengthParseError::NeedMoreData(3)); - let v = LdapMessageLength::parse(&[0x86, 0, 0, 0, 0, 0, 0x01]).unwrap(); - assert_eq!(v.value, 0x01); - assert_eq!(v.encoded_len(), 7); - - let v = LdapMessageLength::parse(&[0x87, 0, 0, 0]).unwrap_err(); - assert_eq!(v, LdapMessageLengthParseError::NeedMoreData(4)); - let v = LdapMessageLength::parse(&[0x87, 0, 0, 0, 0, 0, 0, 0x01]).unwrap(); - assert_eq!(v.value, 0x01); - assert_eq!(v.encoded_len(), 8); - - let v = LdapMessageLength::parse(&[0x88, 0, 0, 0]).unwrap_err(); - assert_eq!(v, LdapMessageLengthParseError::NeedMoreData(5)); - let v = LdapMessageLength::parse(&[0x88, 0, 0, 0, 0, 0, 0, 0, 0x01]).unwrap(); - assert_eq!(v.value, 0x01); - assert_eq!(v.encoded_len(), 9); - - let v = LdapMessageLength::parse(&[0x89, 0, 0, 0]).unwrap_err(); - assert_eq!(v, LdapMessageLengthParseError::TooLargeValue); - } -} diff --git a/lib/g3-types/src/net/ldap/message/mod.rs b/lib/g3-types/src/net/ldap/message/mod.rs deleted file mode 100644 index 6b921550..00000000 --- a/lib/g3-types/src/net/ldap/message/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2026 G3-OSS developers. - */ - -mod id; -pub use id::{LdapMessageId, LdapMessageIdParseError}; - -mod length; -pub use length::{LdapMessageLength, LdapMessageLengthParseError}; diff --git a/lib/g3-types/src/net/ldap/mod.rs b/lib/g3-types/src/net/ldap/mod.rs deleted file mode 100644 index 57db6bcc..00000000 --- a/lib/g3-types/src/net/ldap/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2026 G3-OSS developers. - */ - -mod message; -pub use message::{ - LdapMessageId, LdapMessageIdParseError, LdapMessageLength, LdapMessageLengthParseError, -}; diff --git a/lib/g3-types/src/net/mod.rs b/lib/g3-types/src/net/mod.rs index 34f087c7..33faa2c9 100644 --- a/lib/g3-types/src/net/mod.rs +++ b/lib/g3-types/src/net/mod.rs @@ -9,11 +9,9 @@ mod egress; mod error; mod haproxy; mod host; -mod ldap; mod pool; mod port; mod proxy; -mod quic; mod rate_limit; mod socks; mod tcp; @@ -46,11 +44,9 @@ pub use haproxy::{ ProxyProtocolEncodeError, ProxyProtocolEncoder, ProxyProtocolV2Encoder, ProxyProtocolVersion, }; pub use host::Host; -pub use ldap::*; pub use pool::ConnectionPoolConfig; pub use port::{PortRange, Ports}; pub use proxy::{Proxy, ProxyParseError, ProxyRequestType, Socks4Proxy, Socks5Proxy}; -pub use quic::*; pub use rate_limit::{ RATE_LIMIT_SHIFT_MILLIS_DEFAULT, RATE_LIMIT_SHIFT_MILLIS_MAX, TcpSockSpeedLimitConfig, UdpSockSpeedLimitConfig, diff --git a/lib/g3-types/src/net/proxy/http.rs b/lib/g3-types/src/net/proxy/http.rs index 250bd397..91f246ba 100644 --- a/lib/g3-types/src/net/proxy/http.rs +++ b/lib/g3-types/src/net/proxy/http.rs @@ -27,7 +27,7 @@ impl HttpProxy { let host = url.host().ok_or(ProxyParseError::NoHostFound)?; let port = url.port().unwrap_or(8080); - let peer = UpstreamAddr::from_url_host_and_port(host.to_owned(), port); + let peer = UpstreamAddr::new(host, port); let auth = HttpAuth::try_from(url)?; diff --git a/lib/g3-types/src/net/proxy/socks4.rs b/lib/g3-types/src/net/proxy/socks4.rs index ad652760..7b4812ea 100644 --- a/lib/g3-types/src/net/proxy/socks4.rs +++ b/lib/g3-types/src/net/proxy/socks4.rs @@ -21,7 +21,7 @@ impl Socks4Proxy { let host = url.host().ok_or(ProxyParseError::NoHostFound)?; let port = url.port().unwrap_or(1080); - let peer = UpstreamAddr::from_url_host_and_port(host.to_owned(), port); + let peer = UpstreamAddr::new(host, port); Ok(Socks4Proxy { peer }) } diff --git a/lib/g3-types/src/net/proxy/socks5.rs b/lib/g3-types/src/net/proxy/socks5.rs index ab60457b..1cf513ba 100644 --- a/lib/g3-types/src/net/proxy/socks5.rs +++ b/lib/g3-types/src/net/proxy/socks5.rs @@ -23,7 +23,7 @@ impl Socks5Proxy { let host = url.host().ok_or(ProxyParseError::NoHostFound)?; let port = url.port().unwrap_or(1080); - let peer = UpstreamAddr::from_url_host_and_port(host.to_owned(), port); + let peer = UpstreamAddr::new(host, port); let auth = SocksAuth::try_from(url)?; diff --git a/lib/g3-types/src/net/quic/mod.rs b/lib/g3-types/src/net/quic/mod.rs deleted file mode 100644 index 6d51908e..00000000 --- a/lib/g3-types/src/net/quic/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2026 G3-OSS developers. - */ - -mod var_int; -pub use var_int::QuicVarInt; diff --git a/lib/g3-types/src/net/upstream.rs b/lib/g3-types/src/net/upstream.rs index 57240203..7232e79c 100644 --- a/lib/g3-types/src/net/upstream.rs +++ b/lib/g3-types/src/net/upstream.rs @@ -28,8 +28,11 @@ pub struct UpstreamAddr { } impl UpstreamAddr { - pub fn new(host: Host, port: u16) -> Self { - UpstreamAddr { host, port } + pub fn new>(host: T, port: u16) -> Self { + UpstreamAddr { + host: host.into(), + port, + } } pub fn empty() -> Self { @@ -109,14 +112,6 @@ impl UpstreamAddr { let host = Host::from_maybe_mapped_ip6(ip6); UpstreamAddr { host, port } } - - #[inline] - pub(crate) fn from_url_host_and_port(host: url::Host, port: u16) -> Self { - UpstreamAddr { - host: host.into(), - port, - } - } } impl TryFrom<&Url> for UpstreamAddr { @@ -127,7 +122,7 @@ impl TryFrom<&Url> for UpstreamAddr { let port = u .port_or_known_default() .ok_or_else(|| anyhow!("unable to detect port in this url"))?; - Ok(UpstreamAddr::from_url_host_and_port(host.to_owned(), port)) + Ok(UpstreamAddr::new(host, port)) } else { Err(anyhow!("no host found in this url")) } diff --git a/sphinx/g3proxy/configuration/auth/group/facts.rst b/sphinx/g3proxy/configuration/auth/group/facts.rst index 343110b9..a24182f0 100644 --- a/sphinx/g3proxy/configuration/auth/group/facts.rst +++ b/sphinx/g3proxy/configuration/auth/group/facts.rst @@ -3,7 +3,9 @@ Facts ===== -The following keys are supported: +The user group that auth the user based on connection facts. + +The following common keys are supported: * :ref:`name ` * :ref:`type ` diff --git a/sphinx/g3proxy/configuration/auth/group/index.rst b/sphinx/g3proxy/configuration/auth/group/index.rst index ae99138b..2b9e4ef6 100644 --- a/sphinx/g3proxy/configuration/auth/group/index.rst +++ b/sphinx/g3proxy/configuration/auth/group/index.rst @@ -25,6 +25,7 @@ Groups basic facts + ldap Common Keys =========== diff --git a/sphinx/g3proxy/configuration/auth/group/ldap.rst b/sphinx/g3proxy/configuration/auth/group/ldap.rst new file mode 100644 index 00000000..429546b9 --- /dev/null +++ b/sphinx/g3proxy/configuration/auth/group/ldap.rst @@ -0,0 +1,106 @@ +.. _configuration_auth_user_group_ldap: + +LDAP +==== + +The user group that auth user with remote a LDAP server (simple bind). + +The following common keys are supported: + +* :ref:`name ` +* :ref:`type ` +* :ref:`static users ` +* :ref:`source ` +* :ref:`cache ` +* :ref:`refresh_interval ` +* :ref:`anonymous_user ` + +ldap_url +-------- + +**required**, **type**: LDAP URL + +Set the LDAP url in format `://:[]/`. +The schema should be one of `ldap` or `ldaps`, the default for `ldap` is 389 while 636 will be used for `ldaps`. + +tls_client +---------- + +**optional**, **type**: :ref:`openssl tls client config ` + +Set TLS parameters for this local TLS client. +If set to empty map, a default config is used. + +If the schema of LDAP url is "ldap" and this has been set, then "STARTTLS" will be used. + +If the schema is "ldaps", a default value will be used if not set. + +**default**: not set + +unmanaged_user +-------------- + +**optional**, **type**: :ref:`user ` + +Set and enable unmanaged users. + +This is a template user config for all users that auth OK with the LDAP server but not has been set +in both static and dynamic users config. + +If not set, only static or dynamic users will be allowed. + +**default**: not set + +max_message_size +---------------- + +**optional**, **type**: :ref:`humanize usize ` + +Set the max header size when parsing response from the LDAP server. + +**default**: 256 + +connect_timeout +--------------- + +**optional**, **type**: :ref:`humanize duration ` + +Set the timeout value when TCP connect to the LDAP server. + +**default**: 4s + +response_timeout +---------------- + +**optional**, **type**: :ref:`humanize duration ` + +Set the timeout value for the read of response from LDAP server. + +**default**: 2s + +connection_pool +--------------- + +**optional**, **type**: :ref:`connection pool ` + +Set the connection pool config. + +**default**: set with default value + +queue_channel_size +------------------ + +**optional**, **type**: usize + +Set the queue channel size value when auth with the LDAP server for a client request. + +**default**: 64 + +queue_wait_timeout +------------------ + +**optional**, **type**: :ref:`humanize duration ` + +Set the timeout value when auth with the LDAP server for a client request. + +**default**: 4s