Skip to content

Commit 45de0e6

Browse files
fix(dgw): address KDC connector review feedback
1 parent 06706d0 commit 45de0e6

2 files changed

Lines changed: 85 additions & 6 deletions

File tree

devolutions-gateway/src/kdc_connector.rs

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//! pre-decided by the caller.
88
99
use std::io;
10+
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
1011
use std::sync::Arc;
1112

1213
use axum::http::StatusCode;
@@ -128,24 +129,30 @@ impl KdcConnector {
128129
.err(),
129130
)?)
130131
} else {
132+
let udp_payload = message.get(4..).ok_or_else(|| {
133+
HttpError::bad_request().msg("KDC UDP message is too short to contain a length prefix")
134+
})?;
135+
136+
let destination_addr = resolve_udp_destination(kdc_addr).await?;
137+
let bind_addr = udp_bind_addr_for(destination_addr);
138+
131139
// We assume that ticket length is not bigger than 2048 bytes.
132140
let mut buf = [0; 2048];
133141

134-
let udp_socket = UdpSocket::bind("127.0.0.1:0")
142+
let udp_socket = UdpSocket::bind(bind_addr)
135143
.await
136144
.map_err(HttpError::internal().with_msg("unable to bind UDP socket").err())?;
137145

138-
let port = udp_socket
146+
let local_addr = udp_socket
139147
.local_addr()
140-
.map_err(HttpError::internal().with_msg("unable to get UDP socket address").err())?
141-
.port();
148+
.map_err(HttpError::internal().with_msg("unable to get UDP socket address").err())?;
142149

143-
trace!("Binded UDP listener to 127.0.0.1:{port}, forwarding KDC message...");
150+
trace!(%local_addr, %destination_addr, "Bound UDP listener, forwarding KDC message");
144151

145152
// First 4 bytes contains message length. We don't need it for UDP.
146153
#[allow(clippy::redundant_closure)] // We get a better caller location for the error by using a closure.
147154
udp_socket
148-
.send_to(&message[4..], kdc_addr.as_addr())
155+
.send_to(udp_payload, destination_addr)
149156
.await
150157
.map_err(|e| unable_to_reach_kdc_server_err(e))?;
151158

@@ -187,6 +194,24 @@ impl KdcConnector {
187194
}
188195
}
189196

197+
async fn resolve_udp_destination(kdc_addr: &TargetAddr) -> Result<SocketAddr, HttpError> {
198+
let mut addrs = tokio::net::lookup_host(kdc_addr.as_addr())
199+
.await
200+
.map_err(unable_to_reach_kdc_server_err)?;
201+
202+
addrs.next().ok_or_else(|| {
203+
unable_to_reach_kdc_server_err(io::Error::new(io::ErrorKind::NotFound, "KDC address resolved empty"))
204+
})
205+
}
206+
207+
fn udp_bind_addr_for(destination_addr: SocketAddr) -> SocketAddr {
208+
if destination_addr.is_ipv4() {
209+
SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0))
210+
} else {
211+
SocketAddr::from((Ipv6Addr::UNSPECIFIED, 0))
212+
}
213+
}
214+
190215
/// Hard ceiling on the announced length of a TCP-framed KDC reply.
191216
///
192217
/// The KDC TCP transport prefixes its message with a 4-byte big-endian length.
@@ -257,6 +282,10 @@ mod tests {
257282
TargetAddr::parse("tcp://127.0.0.1:1", Some(88)).expect("static target addr is valid")
258283
}
259284

285+
fn udp_kdc_addr() -> TargetAddr {
286+
TargetAddr::parse("udp://127.0.0.1:88", Some(88)).expect("static target addr is valid")
287+
}
288+
260289
/// No tunnel handle + explicit agent pin → must error.
261290
///
262291
/// `jet_agent_id` declares a routing requirement; with no agent tunnel listener
@@ -288,4 +317,26 @@ mod tests {
288317
"should have reached the direct-connect branch, got: {err}",
289318
);
290319
}
320+
321+
#[tokio::test]
322+
async fn udp_message_shorter_than_length_prefix_errors() {
323+
let connector = KdcConnector::new(Uuid::new_v4(), None, None);
324+
let result = connector.send(&udp_kdc_addr(), b"\x00\x01\x02").await;
325+
let err = result.expect_err("UDP message shorter than the TCP length prefix must be rejected");
326+
assert!(
327+
format!("{err}").contains("too short"),
328+
"error message should explain the malformed UDP payload, got: {err}",
329+
);
330+
}
331+
332+
#[test]
333+
fn udp_bind_addr_matches_destination_family() {
334+
let v4_bind = udp_bind_addr_for(SocketAddr::from((Ipv4Addr::LOCALHOST, 88)));
335+
assert!(v4_bind.is_ipv4());
336+
assert_eq!(v4_bind.port(), 0);
337+
338+
let v6_bind = udp_bind_addr_for(SocketAddr::from((Ipv6Addr::LOCALHOST, 88)));
339+
assert!(v6_bind.is_ipv6());
340+
assert_eq!(v6_bind.port(), 0);
341+
}
291342
}

devolutions-gateway/src/token.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,9 +1489,14 @@ mod serde_impl {
14891489
krb_kdc: Option<SmolStr>,
14901490
#[serde(default, skip_serializing_if = "Option::is_none")]
14911491
jet_cred_id: Option<Uuid>,
1492+
#[serde(default = "legacy_kdc_token_jti")]
14921493
jti: Uuid,
14931494
}
14941495

1496+
fn legacy_kdc_token_jti() -> Uuid {
1497+
Uuid::new_v4()
1498+
}
1499+
14951500
impl ser::Serialize for SessionTtl {
14961501
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
14971502
where
@@ -1834,4 +1839,27 @@ mod tests {
18341839
vec!["secondary.example:3389".to_owned()]
18351840
);
18361841
}
1842+
1843+
#[test]
1844+
fn kdc_real_claims_without_jti_deserialize_for_legacy_tokens() {
1845+
let claims: KdcTokenClaims = serde_json::from_value(serde_json::json!({
1846+
"krb_realm": "example.com",
1847+
"krb_kdc": "tcp://dc.example.com:88",
1848+
}))
1849+
.expect("legacy KDC token without jti should still deserialize");
1850+
1851+
assert_ne!(claims.jti, Uuid::nil());
1852+
assert!(matches!(claims.destination, KdcDestination::Real { .. }));
1853+
}
1854+
1855+
#[test]
1856+
fn kdc_injection_claims_without_jti_deserialize_for_legacy_tokens() {
1857+
let claims: KdcTokenClaims = serde_json::from_value(serde_json::json!({
1858+
"jet_cred_id": Uuid::new_v4(),
1859+
}))
1860+
.expect("legacy injection KDC token without jti should still deserialize");
1861+
1862+
assert_ne!(claims.jti, Uuid::nil());
1863+
assert!(matches!(claims.destination, KdcDestination::Inject { .. }));
1864+
}
18371865
}

0 commit comments

Comments
 (0)