use ssh_encoding::{Decode, Encode, Reader};
use crate::msg;
use crate::map_err;
pub(crate) fn ensure_end(reader: &impl Reader) -> Result<(), crate::Error> {
if reader.is_finished() {
Ok(())
} else {
Err(ssh_encoding::Error::TrailingData {
remaining: reader.remaining_len(),
}
.into())
}
}
#[derive(Debug)]
pub struct OpenChannelMessage {
pub typ: ChannelType,
pub recipient_channel: u32,
pub recipient_window_size: u32,
pub recipient_maximum_packet_size: u32,
}
impl OpenChannelMessage {
pub fn parse<R: Reader>(r: &mut R) -> Result<Self, crate::Error> {
let typ = map_err!(String::decode(r))?;
let sender = map_err!(u32::decode(r))?;
let window = map_err!(u32::decode(r))?;
let maxpacket = map_err!(u32::decode(r))?;
let typ = match typ.as_str() {
"session" => {
ensure_end(r)?;
ChannelType::Session
}
"x11" => {
let originator_address = map_err!(String::decode(r))?;
let originator_port = map_err!(u32::decode(r))?;
ensure_end(r)?;
ChannelType::X11 {
originator_address,
originator_port,
}
}
"direct-tcpip" => {
let info = TcpChannelInfo::decode(r)?;
ensure_end(r)?;
ChannelType::DirectTcpip(info)
}
"direct-streamlocal@openssh.com" => {
let info = StreamLocalChannelInfo::decode(r)?;
String::decode(r)?; u32::decode(r)?; ensure_end(r)?;
ChannelType::DirectStreamLocal(info)
}
"forwarded-tcpip" => {
let info = TcpChannelInfo::decode(r)?;
ensure_end(r)?;
ChannelType::ForwardedTcpIp(info)
}
"forwarded-streamlocal@openssh.com" => {
let info = StreamLocalChannelInfo::decode(r)?;
String::decode(r)?; ensure_end(r)?;
ChannelType::ForwardedStreamLocal(info)
}
"auth-agent@openssh.com" => {
ensure_end(r)?;
ChannelType::AgentForward
}
_ => ChannelType::Unknown { typ },
};
Ok(Self {
typ,
recipient_channel: sender,
recipient_window_size: window,
recipient_maximum_packet_size: maxpacket,
})
}
pub fn confirm(
&self,
buffer: &mut Vec<u8>,
sender_channel: u32,
window_size: u32,
packet_size: u32,
) -> Result<(), crate::Error> {
push_packet!(buffer, {
msg::CHANNEL_OPEN_CONFIRMATION.encode(buffer)?;
self.recipient_channel.encode(buffer)?; sender_channel.encode(buffer)?; window_size.encode(buffer)?;
packet_size.encode(buffer)?;
});
Ok(())
}
pub fn fail(
&self,
buffer: &mut Vec<u8>,
reason: u8,
message: &[u8],
) -> Result<(), crate::Error> {
push_packet!(buffer, {
msg::CHANNEL_OPEN_FAILURE.encode(buffer)?;
self.recipient_channel.encode(buffer)?;
(reason as u32).encode(buffer)?;
message.encode(buffer)?;
"en".encode(buffer)?;
});
Ok(())
}
pub fn unknown_type(&self, buffer: &mut Vec<u8>) -> Result<(), crate::Error> {
self.fail(
buffer,
msg::SSH_OPEN_UNKNOWN_CHANNEL_TYPE,
b"Unknown channel type",
)
}
}
#[derive(Debug)]
pub enum ChannelType {
Session,
X11 {
originator_address: String,
originator_port: u32,
},
DirectTcpip(TcpChannelInfo),
DirectStreamLocal(StreamLocalChannelInfo),
ForwardedTcpIp(TcpChannelInfo),
ForwardedStreamLocal(StreamLocalChannelInfo),
AgentForward,
Unknown {
typ: String,
},
}
#[derive(Debug)]
pub struct TcpChannelInfo {
pub host_to_connect: String,
pub port_to_connect: u32,
pub originator_address: String,
pub originator_port: u32,
}
#[derive(Debug)]
pub struct StreamLocalChannelInfo {
pub socket_path: String,
}
impl Decode for StreamLocalChannelInfo {
type Error = ssh_encoding::Error;
fn decode(r: &mut impl Reader) -> Result<Self, Self::Error> {
let socket_path = String::decode(r)?.to_owned();
Ok(Self { socket_path })
}
}
impl Decode for TcpChannelInfo {
type Error = ssh_encoding::Error;
fn decode(r: &mut impl Reader) -> Result<Self, Self::Error> {
let host_to_connect = String::decode(r)?;
let port_to_connect = u32::decode(r)?;
let originator_address = String::decode(r)?;
let originator_port = u32::decode(r)?;
Ok(Self {
host_to_connect,
port_to_connect,
originator_address,
originator_port,
})
}
}
#[derive(Debug)]
pub(crate) struct ChannelOpenConfirmation {
pub recipient_channel: u32,
pub sender_channel: u32,
pub initial_window_size: u32,
pub maximum_packet_size: u32,
}
impl Decode for ChannelOpenConfirmation {
type Error = ssh_encoding::Error;
fn decode(r: &mut impl Reader) -> Result<Self, Self::Error> {
let recipient_channel = u32::decode(r)?;
let sender_channel = u32::decode(r)?;
let initial_window_size = u32::decode(r)?;
let maximum_packet_size = u32::decode(r)?;
Ok(Self {
recipient_channel,
sender_channel,
initial_window_size,
maximum_packet_size,
})
}
}
#[cfg(test)]
mod tests {
use super::{ChannelType, OpenChannelMessage};
use crate::tests::raw_no_crypto::{channel_open_payload, encode_string, push_u32};
#[test]
fn known_channel_open_with_trailing_bytes_is_rejected() {
let mut payload = channel_open_payload(b"session");
payload.push(0);
assert!(
OpenChannelMessage::parse(&mut payload.as_slice()).is_err(),
"known channel-open type accepted trailing bytes"
);
}
#[test]
fn unknown_channel_open_with_extra_payload_stays_permissive() {
let mut payload = channel_open_payload(b"unknown@example.com");
payload.extend_from_slice(b"opaque");
let parsed = OpenChannelMessage::parse(&mut payload.as_slice())
.expect("unknown channel-open payload should remain opaque");
assert!(matches!(parsed.typ, ChannelType::Unknown { .. }));
}
#[test]
fn openssh_streamlocal_channel_open_reserved_fields_are_consumed() {
let mut direct = channel_open_payload(b"direct-streamlocal@openssh.com");
encode_string(&mut direct, b"/tmp/socket");
encode_string(&mut direct, b"");
push_u32(&mut direct, 0);
let parsed = OpenChannelMessage::parse(&mut direct.as_slice())
.expect("direct streamlocal reserved fields should be consumed");
assert!(matches!(parsed.typ, ChannelType::DirectStreamLocal(_)));
let mut forwarded = channel_open_payload(b"forwarded-streamlocal@openssh.com");
encode_string(&mut forwarded, b"/tmp/socket");
encode_string(&mut forwarded, b"");
let parsed = OpenChannelMessage::parse(&mut forwarded.as_slice())
.expect("forwarded streamlocal reserved field should be consumed");
assert!(matches!(parsed.typ, ChannelType::ForwardedStreamLocal(_)));
}
}