g3proxy: add ldap user group

This commit is contained in:
Zhang Jingqiang 2026-01-20 23:29:08 +08:00
parent ab2dd54a32
commit 9522860c0d
54 changed files with 2468 additions and 482 deletions

View file

@ -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

View file

@ -0,0 +1,5 @@
[
{
"name": "euler"
}
]

View file

@ -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

View file

@ -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<LdapUserGroupConfig>,
pool_handle: LdapAuthPoolHandle,
unmanaged_users: Mutex<AHashMap<ArcStr, Arc<User>>>,
}
impl LdapUserGroup {
pub(super) fn base(&self) -> &BaseUserGroup<LdapUserGroupConfig> {
&self.base
}
pub(super) fn clone_config(&self) -> LdapUserGroupConfig {
(*self.base.config).clone()
}
fn new(base: BaseUserGroup<LdapUserGroupConfig>) -> anyhow::Result<Self> {
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<Arc<Self>> {
let base = BaseUserGroup::new_with_config(config).await?;
Self::new(base).map(Arc::new)
}
pub(super) fn reload(&self, config: LdapUserGroupConfig) -> anyhow::Result<Arc<Self>> {
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<ArcSwapOption<MetricTagMap>>,
) -> Result<UserContext, UserAuthError> {
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)
}
}
}
}
}

View file

@ -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<LdapUserGroupConfig>,
}
pub(super) struct LdapTlsConnector {
config: Arc<LdapUserGroupConfig>,
tls_client: OpensslClientConfig,
}
pub(super) enum LdapConnector {
Tcp(LdapTcpConnector),
Tls(LdapTlsConnector),
StartTls(LdapTlsConnector),
}
impl LdapConnector {
pub(super) fn new(config: Arc<LdapUserGroupConfig>) -> anyhow::Result<Self> {
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<MaybeSslStream<TcpStream>> {
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<TcpStream> {
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::<Vec<_>>();
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<SslStream<TcpStream>> {
let stream = self.tcp_connect().await?;
self.tls_handshake(stream).await
}
async fn starttls_connect(&self) -> anyhow::Result<SslStream<TcpStream>> {
let mut stream = self.tcp_connect().await?;
self.starttls(&mut stream).await?;
self.tls_handshake(stream).await
}
async fn tcp_connect(&self) -> anyhow::Result<TcpStream> {
let tcp_connector = LdapTcpConnector {
config: self.config.clone(),
};
tcp_connector.connect().await
}
async fn tls_handshake(&self, stream: TcpStream) -> anyhow::Result<SslStream<TcpStream>> {
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(())
}
}
}

View file

@ -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<Option<(String, String)>>,
}
pub(super) struct LdapAuthPoolHandle {
config: Arc<LdapUserGroupConfig>,
req_sender: kanal::AsyncSender<LdapAuthRequest>,
cmd_sender: mpsc::Sender<PoolCommand>,
}
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<LdapUserGroupConfig>,
connector: Arc<LdapConnector>,
req_receiver: kanal::AsyncReceiver<LdapAuthRequest>,
cmd_sender: mpsc::Sender<PoolCommand>,
cmd_receiver: mpsc::Receiver<PoolCommand>,
idle_conn_count: Arc<AtomicUsize>,
expected_idle_count: usize,
}
impl LdapAuthPool {
pub(super) fn create(config: Arc<LdapUserGroupConfig>) -> anyhow::Result<LdapAuthPoolHandle> {
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;
});
}
}

View file

@ -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<LdapUserGroupConfig>,
connector: Arc<LdapConnector>,
quit: bool,
pending_request: Option<LdapAuthRequest>,
request_encoder: SimpleBindRequestEncoder,
}
impl LdapAuthTask {
pub(super) fn new(config: Arc<LdapUserGroupConfig>, connector: Arc<LdapConnector>) -> Self {
LdapAuthTask {
config,
connector,
quit: false,
pending_request: None,
request_encoder: SimpleBindRequestEncoder::default(),
}
}
pub(super) async fn run(mut self, receiver: AsyncReceiver<LdapAuthRequest>) {
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<S>(
&mut self,
stream: S,
receiver: &AsyncReceiver<LdapAuthRequest>,
) -> 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<W>(
&mut self,
writer: &mut W,
r: &LdapAuthRequest,
) -> anyhow::Result<u32>
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<bool> {
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(())
}
}

View file

@ -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<R>(&mut self, reader: &mut R) -> anyhow::Result<LdapMessage<'_>>
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;
}
}
}

View file

@ -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;

View file

@ -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<u8>,
}
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
]
);
}
}

View file

@ -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<BasicUserGroup>),
Facts(Arc<FactsUserGroup>),
Ldap(Arc<LdapUserGroup>),
}
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
}
}
}
}

View file

@ -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",
}
}

View file

@ -32,6 +32,7 @@ use crate::config::auth::{UserAuditConfig, UserConfig};
pub(crate) struct User {
config: Arc<UserConfig>,
name: ArcStr,
group: NodeName,
started: Instant,
is_expired: AtomicBool,
@ -88,6 +89,24 @@ impl User {
group: &NodeName,
config: &Arc<UserConfig>,
datetime_now: &DateTime<Utc>,
) -> anyhow::Result<Self> {
Self::new_with_name(config.name().clone(), group, config, datetime_now)
}
pub(super) fn new_unmanaged(
name: &ArcStr,
group: &NodeName,
config: &Arc<UserConfig>,
) -> anyhow::Result<Self> {
let now = Utc::now();
Self::new_with_name(name.clone(), group, config, &now)
}
fn new_with_name(
name: ArcStr,
group: &NodeName,
config: &Arc<UserConfig>,
datetime_now: &DateTime<Utc>,
) -> anyhow::Result<Self> {
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]

View file

@ -27,7 +27,7 @@ const USER_GROUP_TYPE: &str = "basic";
#[derive(Clone)]
pub(crate) struct BasicUserGroupConfig {
name: NodeName,
position: Option<YamlDocPosition>,
pub(super) position: Option<YamlDocPosition>,
pub(crate) static_users: HashMap<ArcStr, Arc<UserConfig>>,
pub(crate) dynamic_source: Option<UserDynamicSource>,
pub(crate) dynamic_cache: PathBuf,

View file

@ -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<OpensslClientConfigBuilder>,
pub(crate) direct_tls: bool,
pub(crate) base_dn: ArcStr,
pub(crate) unmanaged_user: Option<Arc<UserConfig>>,
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<YamlDocPosition>) -> 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<YamlDocPosition>,
) -> anyhow::Result<Self> {
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
}
}

View file

@ -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 {

View file

@ -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}")),
}
}

View file

@ -118,7 +118,7 @@ where
}
}
fn do_auth(
async fn do_auth(
&mut self,
req: &HttpProxyRequest<CDR>,
) -> Result<Option<UserContext>, 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

View file

@ -114,7 +114,7 @@ where
}
}
fn do_auth(
async fn do_auth(
&mut self,
req: &HttpRProxyRequest<CDR>,
) -> Result<Option<UserContext>, 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;

View file

@ -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();

View file

@ -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<AckRange>,
pub ecn_counts: Option<EcnCounts>,
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<Self, FrameParseError> {
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();

View file

@ -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<Self, FrameParseError> {
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();

View file

@ -5,6 +5,8 @@
use thiserror::Error;
use super::VarInt;
mod crypto;
pub use crypto::{CryptoFrame, HandshakeCoalescer};

View file

@ -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};

View file

@ -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);
};

View file

@ -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();

View file

@ -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();

View file

@ -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<Self> {
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);
}

View file

@ -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);

View file

@ -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 {

View file

@ -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<BerLengthParseError> 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<BerInteger, BerIntegerParseError> {
Self::parse_with_identifier(data, 0x02)
}
pub fn parse_enumerated_value(data: &[u8]) -> Result<BerInteger, BerIntegerParseError> {
Self::parse_with_identifier(data, 0x0a)
}
fn parse_with_identifier(data: &[u8], identifier: u8) -> Result<Self, BerIntegerParseError> {
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);
}
}

View file

@ -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<Self, BerLengthParseError> {
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]
);
}
}

View file

@ -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};

View file

@ -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<BerLengthParseError> 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<Self, LdapLengthParseError> {
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);
}
}

View file

@ -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<LdapLengthParseError> 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<Self, LdapMessageParseError> {
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());
}
}

View file

@ -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<Self, LdapMessageIdParseError> {
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);
}
}

View file

@ -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};

View file

@ -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<Self, LdapResultParseError> {
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);
}
}

View file

@ -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<BerLengthParseError> 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, LdapSequenceParseError> {
Self::parse_with_identifier(data, 0x04)
}
pub fn parse_referrals_sequence(data: &'a [u8]) -> Result<Self, LdapSequenceParseError> {
Self::parse_with_identifier(data, 0xa3)
}
pub fn parse_bind_response(data: &'a [u8]) -> Result<Self, LdapSequenceParseError> {
Self::parse_with_identifier(data, 0x61)
}
pub fn parse_extended_response(data: &'a [u8]) -> Result<Self, LdapSequenceParseError> {
Self::parse_with_identifier(data, 0x78)
}
pub fn parse_extended_response_oid(data: &'a [u8]) -> Result<Self, LdapSequenceParseError> {
Self::parse_with_identifier(data, 0x8a)
}
fn parse_with_identifier(
data: &'a [u8],
identifier: u8,
) -> Result<Self, LdapSequenceParseError> {
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);
}
}

View file

@ -5,3 +5,9 @@
mod tlv;
pub use tlv::{T1L2BVParse, TlvParse};
mod ber;
pub use ber::*;
mod ldap;
pub use ldap::*;

View file

@ -99,8 +99,8 @@ impl fmt::Display for Host {
}
}
impl From<url::Host> for Host {
fn from(v: url::Host) -> Self {
impl<T: Into<ArcStr>> From<url::Host<T>> for Host {
fn from(v: url::Host<T>) -> Self {
match v {
url::Host::Ipv4(ip4) => Host::Ip(IpAddr::V4(ip4)),
url::Host::Ipv6(ip6) => Host::Ip(IpAddr::V6(ip6)),

View file

@ -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<Self, LdapMessageIdParseError> {
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);
}
}

View file

@ -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<Self, LdapMessageLengthParseError> {
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);
}
}

View file

@ -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};

View file

@ -1,9 +0,0 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2026 G3-OSS developers.
*/
mod message;
pub use message::{
LdapMessageId, LdapMessageIdParseError, LdapMessageLength, LdapMessageLengthParseError,
};

View file

@ -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,

View file

@ -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)?;

View file

@ -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 })
}

View file

@ -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)?;

View file

@ -1,7 +0,0 @@
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2026 G3-OSS developers.
*/
mod var_int;
pub use var_int::QuicVarInt;

View file

@ -28,8 +28,11 @@ pub struct UpstreamAddr {
}
impl UpstreamAddr {
pub fn new(host: Host, port: u16) -> Self {
UpstreamAddr { host, port }
pub fn new<T: Into<Host>>(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"))
}

View file

@ -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 <conf_auth_user_group_name>`
* :ref:`type <conf_auth_user_group_type>`

View file

@ -25,6 +25,7 @@ Groups
basic
facts
ldap
Common Keys
===========

View file

@ -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 <conf_auth_user_group_name>`
* :ref:`type <conf_auth_user_group_type>`
* :ref:`static users <conf_auth_user_group_static_users>`
* :ref:`source <conf_auth_user_group_source>`
* :ref:`cache <conf_auth_user_group_cache>`
* :ref:`refresh_interval <conf_auth_user_group_refresh_interval>`
* :ref:`anonymous_user <conf_auth_user_group_anonymous_user>`
ldap_url
--------
**required**, **type**: LDAP URL
Set the LDAP url in format `<schema>://<server_name>:[<port>]/<base_dn>`.
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 <conf_value_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 <configuration_auth_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 <conf_value_humanize_usize>`
Set the max header size when parsing response from the LDAP server.
**default**: 256
connect_timeout
---------------
**optional**, **type**: :ref:`humanize duration <conf_value_humanize_duration>`
Set the timeout value when TCP connect to the LDAP server.
**default**: 4s
response_timeout
----------------
**optional**, **type**: :ref:`humanize duration <conf_value_humanize_duration>`
Set the timeout value for the read of response from LDAP server.
**default**: 2s
connection_pool
---------------
**optional**, **type**: :ref:`connection pool <conf_value_connection_pool_config>`
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 <conf_value_humanize_duration>`
Set the timeout value when auth with the LDAP server for a client request.
**default**: 4s