diff --git a/sshproto/src/async_behaviour.rs b/sshproto/src/async_behaviour.rs index 2dba4729481949e516c93811e7b0b3171889594a..5fac8f1c19c52771c7703ccbadbb9c2fd03ff1ec 100644 --- a/sshproto/src/async_behaviour.rs +++ b/sshproto/src/async_behaviour.rs @@ -93,6 +93,11 @@ pub trait AsyncCliBehaviour: Sync+Send { info!("Got banner:\n{:?}", banner.escape_default()); } // TODO: postauth channel callbacks + + // TODO: do we want this to be async? probably not. + fn open_tcp_forwarded(&self, chan: u32) -> channel::ChanOpened; + + fn open_tcp_direct(&self, chan: u32) -> channel::ChanOpened; } // #[async_trait(?Send)] @@ -101,8 +106,8 @@ pub trait AsyncServBehaviour: Sync+Send { async fn hostkeys(&self) -> BhResult<&[&sign::SignKey]>; // TODO: or return a slice of enums - async fn have_auth_password(&self, username: &str) -> bool; - async fn have_auth_pubkey(&self, username: &str) -> bool; + fn have_auth_password(&self, username: &str) -> bool; + fn have_auth_pubkey(&self, username: &str) -> bool; #[allow(unused)] @@ -119,5 +124,9 @@ pub trait AsyncServBehaviour: Sync+Send { } /// Returns whether a session can be opened - async fn open_session(&self) -> bool; + fn open_session(&self, chan: u32) -> channel::ChanOpened; + + fn open_tcp_forwarded(&self, chan: u32) -> channel::ChanOpened; + + fn open_tcp_direct(&self, chan: u32) -> channel::ChanOpened; } diff --git a/sshproto/src/behaviour.rs b/sshproto/src/behaviour.rs index 3f4336f61ebbb669abdd3c5c5981a3e95b821aa7..8d8a3a5724bb65643e626ad488cf8e05bc14da10 100644 --- a/sshproto/src/behaviour.rs +++ b/sshproto/src/behaviour.rs @@ -44,9 +44,9 @@ pub enum BhError { pub struct Behaviour<'a> { #[cfg(feature = "std")] - inner: crate::async_behaviour::AsyncCliServ<'a>, + inner: async_behaviour::AsyncCliServ<'a>, #[cfg(not(feature = "std"))] - inner: crate::block_behaviour::BlockCliServ<'a>, + inner: block_behaviour::BlockCliServ<'a>, } #[cfg(feature = "std")] @@ -72,6 +72,32 @@ impl<'a> Behaviour<'a> { pub(crate) fn server(&mut self) -> Result<ServBehaviour> { self.inner.server() } + + pub(crate) fn is_client(&self) -> bool { + matches!(self.inner, async_behaviour::AsyncCliServ::Client(_)) + } + + pub(crate) fn is_server(&self) -> bool { + !self.is_client() + } + + /// Calls either client or server + pub(crate) fn open_tcp_forwarded(&mut self, chan: u32) -> channel::ChanOpened { + if self.is_client() { + self.client().unwrap().open_tcp_forwarded(chan) + } else { + self.server().unwrap().open_tcp_forwarded(chan) + } + } + + /// Calls either client or server + pub(crate) fn open_tcp_direct(&mut self, chan: u32) -> channel::ChanOpened { + if self.is_client() { + self.client().unwrap().open_tcp_direct(chan) + } else { + self.server().unwrap().open_tcp_direct(chan) + } + } } #[cfg(not(feature = "std"))] @@ -94,9 +120,36 @@ impl<'a> Behaviour<'a> pub(crate) fn client(&mut self) -> Result<CliBehaviour> { self.inner.client() } + pub(crate) fn server(&mut self) -> Result<ServBehaviour> { self.inner.server() } + + pub(crate) fn is_client(&mut self) -> bool { + matches!(self.inner, block_behaviour::BlockCliServ::Client(_)) + } + + pub(crate) fn is_server(&mut self) -> bool { + !self.is_client() + } + + /// Calls either client or server + pub(crate) fn open_tcp_forwarded(&mut self, chan: u32) -> channel::ChanOpened { + if self.is_client() { + self.client().unwrap().open_tcp_forwarded(chan) + } else { + self.server().unwrap().open_tcp_forwarded(chan) + } + } + + /// Calls either client or server + pub(crate) fn open_tcp_direct(&mut self, chan: u32) -> channel::ChanOpened { + if self.is_client() { + self.client().unwrap().open_tcp_direct(chan) + } else { + self.server().unwrap().open_tcp_direct(chan) + } + } } pub struct CliBehaviour<'a> { @@ -136,9 +189,17 @@ impl<'a> CliBehaviour<'a> { self.inner.show_banner(banner, language).await; Ok(()) } + + pub(crate) fn open_tcp_forwarded(&self, chan: u32) -> channel::ChanOpened { + self.inner.open_tcp_forwarded(chan) + } + + pub(crate) fn open_tcp_direct(&self, chan: u32) -> channel::ChanOpened { + self.inner.open_tcp_direct(chan) + } } -// no-std blocking variant +// no_std blocking variant #[cfg(not(feature = "std"))] impl<'a> CliBehaviour<'a> { pub(crate) async fn username(&mut self) -> BhResult<ResponseString>{ @@ -169,6 +230,14 @@ impl<'a> CliBehaviour<'a> { self.inner.show_banner(banner, language); Ok(()) } + + pub(crate) fn open_tcp_forwarded(&self, chan: u32) -> channel::ChanOpened { + self.inner.open_tcp_forwarded(chan) + } + + pub(crate) fn open_tcp_direct(&self, chan: u32) -> channel::ChanOpened { + self.inner.open_tcp_direct(chan) + } } pub struct ServBehaviour<'a> { @@ -183,6 +252,32 @@ impl<'a> ServBehaviour<'a> { pub(crate) async fn hostkeys(&self) -> BhResult<&[&sign::SignKey]> { self.inner.hostkeys().await } + + pub(crate) fn have_auth_password(&self, username: &str) -> bool { + self.inner.have_auth_password(username) + } + pub(crate) fn have_auth_pubkey(&self, username: &str) -> bool { + self.inner.have_auth_pubkey(username) + } + + // fn authmethods(&self) -> [AuthMethod]; + + pub(crate) async fn auth_password(&self, user: &str, password: &str) -> bool { + self.inner.auth_password(user, password).await + } + + /// Returns whether a session channel can be opened + pub(crate) fn open_session(&self, chan: u32) -> channel::ChanOpened { + self.inner.open_session(chan) + } + + pub(crate) fn open_tcp_forwarded(&self, chan: u32) -> channel::ChanOpened { + self.inner.open_tcp_forwarded(chan) + } + + pub(crate) fn open_tcp_direct(&self, chan: u32) -> channel::ChanOpened { + self.inner.open_tcp_direct(chan) + } } #[cfg(not(feature = "std"))] @@ -190,6 +285,19 @@ impl<'a> ServBehaviour<'a> { pub(crate) async fn hostkeys(&self) -> BhResult<&[&sign::SignKey]> { self.inner.hostkeys() } + + /// Returns whether a session channel can be opened + pub(crate) fn open_session(&self, chan: u32) -> channel::ChanOpened { + self.inner.open_session(chan) + } + + pub(crate) fn open_tcp_forwarded(&self, chan: u32) -> channel::ChanOpened { + self.inner.open_tcp_forwarded(chan) + } + + pub(crate) fn open_tcp_direct(&self, chan: u32) -> channel::ChanOpened { + self.inner.open_tcp_direct(chan) + } } /// A stack-allocated string to store responses for usernames or passwords. diff --git a/sshproto/src/block_behaviour.rs b/sshproto/src/block_behaviour.rs index 99d0271ef2a476c2f21304d2f28310a4e9310560..fb72694d7ee3dfb51e7de39dc5ba8275b8824c35 100644 --- a/sshproto/src/block_behaviour.rs +++ b/sshproto/src/block_behaviour.rs @@ -82,6 +82,10 @@ pub trait BlockCliBehaviour { info!("Got banner:\n{:?}", banner.escape_default()); } // TODO: postauth channel callbacks + + fn open_tcp_forwarded(&self, chan: u32) -> channel::ChanOpened; + + fn open_tcp_direct(&self, chan: u32) -> channel::ChanOpened; } pub trait BlockServBehaviour { @@ -94,10 +98,10 @@ pub trait BlockServBehaviour { fn auth_password(&self, user: &str, password: &str) -> bool; - /// Returns whether a session channel can be opened - fn open_session(&self) -> BhResult<bool>; + /// Returns whether a session can be opened + fn open_session(&self, chan: u32) -> channel::ChanOpened; - fn open_tcp_forwarded(&self, ) -> BhResult<bool>; + fn open_tcp_forwarded(&self, chan: u32) -> channel::ChanOpened; - fn open_tcp_direct(&self) -> BhResult<bool>; + fn open_tcp_direct(&self, chan: u32) -> channel::ChanOpened; } diff --git a/sshproto/src/channel.rs b/sshproto/src/channel.rs index d86fdeb894e0dbca38a324aa857d7bb6d5645d55..b3568a1927b5da72238898d4d005187c06dc22f4 100644 --- a/sshproto/src/channel.rs +++ b/sshproto/src/channel.rs @@ -10,8 +10,18 @@ use heapless::{Deque, String, Vec}; use crate::{conn::RespPackets, *}; use config::*; -use packets::{ChannelReqType, ChannelRequest, Packet, ChannelOpenType, ChannelData, ChannelDataExt}; +use packets::{ChannelReqType, ChannelRequest, Packet, ChannelOpen, ChannelOpenType, ChannelData, ChannelDataExt}; use sshwire::{BinString, TextString}; +use sshnames::*; + +pub enum ChanOpened { + Success, + + /// A channel open response will be sent later + Defer, + + Failure(ChanFail), +} pub(crate) struct Channels { ch: [Option<Channel>; config::MAX_CHANNELS], @@ -35,22 +45,14 @@ impl Channels { ty: packets::ChannelOpenType<'b>, init_req: InitReqs, ) -> Result<(&Channel, Packet<'b>)> { - // first available channel - let num = self - .ch - .iter() - .enumerate() - .find_map( - |(i, ch)| if ch.as_ref().is_none() { Some(i as u32) } else { None }, - ) - .ok_or(Error::NoChannels)?; + let num = self.unused_chan()?; let chan = Channel::new(num, (&ty).into(), init_req); let p = packets::ChannelOpen { num, initial_window: chan.recv.window as u32, max_packet: chan.recv.max_packet as u32, - ch: ty, + ty, } .into(); let ch = &mut self.ch[num as usize]; @@ -58,7 +60,8 @@ impl Channels { Ok((ch.as_ref().unwrap(), p)) } - pub(crate) fn get(&self, num: u32) -> Result<&Channel> { + /// Returns a `Channel` for a local number, any state. + pub fn get_any(&self, num: u32) -> Result<&Channel> { self.ch .get(num as usize) // out of range @@ -68,14 +71,31 @@ impl Channels { .ok_or(Error::BadChannel) } - pub(crate) fn get_mut(&mut self, num: u32) -> Result<&mut Channel> { - self.ch + /// Returns a `Channel` for a local number. Excludes `InOpen` state. + pub fn get(&self, num: u32) -> Result<&Channel> { + let ch = self.get_any(num)?; + + if matches!(ch.state, ChanState::InOpen) { + Err(Error::BadChannel) + } else { + Ok(ch) + } + } + + pub fn get_mut(&mut self, num: u32) -> Result<&mut Channel> { + let ch = self.ch .get_mut(num as usize) // out of range .ok_or(Error::BadChannel)? .as_mut() // unused channel - .ok_or(Error::BadChannel) + .ok_or(Error::BadChannel)?; + + if matches!(ch.state, ChanState::InOpen) { + Err(Error::BadChannel) + } else { + Ok(ch) + } } fn remove(&mut self, num: u32) -> Result<()> { @@ -85,6 +105,31 @@ impl Channels { // Ok(()) } + /// Returns the first available channel + fn unused_chan(&self) -> Result<u32> { + self.ch.iter().enumerate() + .find_map( + |(i, ch)| if ch.as_ref().is_none() { Some(i as u32) } else { None }, + ) + .ok_or(Error::NoChannels) + } + + /// Creates a new channel in InOpen state. + fn reserve_chan(&mut self, co: &ChannelOpen<'_>) -> Result<&mut Channel> { + let num = self.unused_chan()?; + let mut chan = Channel::new(num, (&co.ty).into(), Vec::new()); + chan.send = Some(ChanDir { + num: co.num, + max_packet: co.max_packet as usize, + window: co.initial_window as usize, + }); + chan.state = ChanState::InOpen; + + let ch = &mut self.ch[num as usize]; + *ch = Some(chan); + Ok(ch.as_mut().unwrap()) + } + /// Returns the channel data packet to send, and the length of data consumed. /// Caller has already checked valid length with send_allowed() pub(crate) fn send_data<'b>(&mut self, num: u32, ext: Option<u32>, data: &'b [u8]) @@ -129,16 +174,103 @@ impl Channels { self.get(num).map_or(Some(0), |c| c.send_allowed()) } - // incoming packet handling + pub fn channel_open(&mut self, p: &ChannelOpen<'_>, + resp: &mut RespPackets<'_>, + b: &mut Behaviour<'_>, + ) -> Result<Option<ChanEventMaker>> { + let mut failure = None; + let open_res = match &p.ty { + ChannelOpenType::Session => { + // only server should receive session opens + let bserv = b.server().map_err(|_| Error::SSHProtoError)?; + + match self.reserve_chan(p) { + Ok(ch) => { + let r = bserv.open_session(ch.recv.num); + Some((ch, r)) + } + Err(_) => { + failure = Some(ChanFail::SSH_OPEN_RESOURCE_SHORTAGE); + None + }, + } + } + ChannelOpenType::ForwardedTcpip(t) => { + match self.reserve_chan(p) { + Ok(ch) => { + let r = b.open_tcp_forwarded(ch.recv.num); + Some((ch, r)) + } + Err(_) => { + failure = Some(ChanFail::SSH_OPEN_RESOURCE_SHORTAGE); + None + }, + } + + } + ChannelOpenType::DirectTcpip(t) => { + match self.reserve_chan(p) { + Ok(ch) => { + let r = b.open_tcp_direct(ch.recv.num); + Some((ch, r)) + } + Err(_) => { + failure = Some(ChanFail::SSH_OPEN_RESOURCE_SHORTAGE); + None + }, + } + } + ChannelOpenType::Unknown(u) => { + debug!("Rejecting unknown channel type '{u}'"); + failure = Some(ChanFail::SSH_OPEN_UNKNOWN_CHANNEL_TYPE); + None + } + }; + + match open_res { + Some((ch, r)) => { + match r { + ChanOpened::Success => { + ch.open_done(); + }, + ChanOpened::Failure(f) => { + failure = Some(f); + } + ChanOpened::Defer => { + // application will reply later + } + } + } + _ => () + } + + if let Some(reason) = failure { + let r = packets::ChannelOpenFailure { + num: p.num, + reason: reason as u32, + desc: "".into(), + lang: "", + }; + let r: Packet = r.into(); + resp.push(r.into()).trap()?; + } + + Ok(None) + } + + /// Incoming packet handling + // TODO: protocol errors etc should perhaps be less fatal, + // ssh implementations are usually imperfect. pub async fn dispatch( &mut self, packet: Packet<'_>, resp: &mut RespPackets<'_>, + b: &mut Behaviour<'_>, ) -> Result<Option<ChanEventMaker>> { trace!("chan dispatch"); let r = match packet { - Packet::ChannelOpen(_p) => { - todo!(); + Packet::ChannelOpen(p) => { + self.channel_open(&p, resp, b) } Packet::ChannelOpenConfirmation(p) => { let ch = self.get_mut(p.num)?; @@ -166,6 +298,7 @@ impl Channels { Packet::ChannelOpenFailure(p) => { let ch = self.get(p.num)?; if ch.send.is_some() { + // TODO: or just warn? Err(Error::SSHProtoError) } else { self.remove(p.num); @@ -356,8 +489,11 @@ pub struct ChanDir { window: usize, } -pub enum ChanState { - /// init_req are the request messages to be sent once the ChannelOpenConfirmation +enum ChanState { + /// An incoming channel open request that has not yet been responded to, + /// should not be used + InOpen, + /// `init_req` are the request messages to be sent once the ChannelOpenConfirmation /// is received // TODO: this is wasting half a kB. where else could we store it? could // the Behaviour own it? Or we don't store them here, just callback to the Behaviour. @@ -365,10 +501,12 @@ pub enum ChanState { Normal, RecvEof, + + // TODO: recvclose state probably shouldn't be possible, we remove it straight away? RecvClose, } -pub struct Channel { +pub(crate) struct Channel { ty: ChanType, state: ChanState, sent_eof: bool, @@ -377,7 +515,7 @@ pub struct Channel { last_req: heapless::Deque<ReqKind, MAX_OUTSTANDING_REQS>, recv: ChanDir, - // filled after confirmation + // filled after confirmation when we initiate the channel send: Option<ChanDir>, /// Accumulated bytes for the next window adjustment (inbound data direction) @@ -404,6 +542,7 @@ impl Channel { full_window: config::DEFAULT_WINDOW, } } + fn request(&mut self, req: ReqDetails, resp: &mut RespPackets) -> Result<()> { let num = self.send.as_ref().trap()?.num; let r = Req { num, details: req }; @@ -415,6 +554,11 @@ impl Channel { self.recv.num } + fn open_done(&mut self) { + debug_assert!(matches!(self.state, ChanState::InOpen)); + self.state = ChanState::Normal + } + fn finished_input(&mut self, len: usize ) { self.pending_adjust = self.pending_adjust.saturating_add(len) } diff --git a/sshproto/src/conn.rs b/sshproto/src/conn.rs index c213fd66492822f98eacd3e52f852137b0286d76..cc6403097eb3176159279196e2947d7000914921 100644 --- a/sshproto/src/conn.rs +++ b/sshproto/src/conn.rs @@ -18,7 +18,7 @@ use encrypt::KeyState; use packets::{Packet,ParseContext}; use server::Server; use traffic::{Traffic,PacketMaker}; -use channel::{Channel, Channels, ChanEvent, ChanEventMaker}; +use channel::{Channels, ChanEvent, ChanEventMaker}; use config::MAX_CHANNELS; use kex::SessId; @@ -359,7 +359,7 @@ impl<'a> Conn<'a> { | Packet::ChannelFailure(_) // TODO: maybe needs a conn or cliserv argument. => { - let chev = self.channels.dispatch(packet, &mut resp).await?; + let chev = self.channels.dispatch(packet, &mut resp, b).await?; event = chev.map(|c| EventMaker::Channel(c)) } }; diff --git a/sshproto/src/packets.rs b/sshproto/src/packets.rs index 1cba6087e413b6812af00bcbb856ad42338e6b90..2b79ce8c94858c195c51e5e8dc7f5e06c2913207 100644 --- a/sshproto/src/packets.rs +++ b/sshproto/src/packets.rs @@ -359,12 +359,12 @@ pub struct RSA256Sig<'a> { #[derive(Debug, SSHEncode, SSHDecode)] pub struct ChannelOpen<'a> { - // channel_type is implicit in ch below - #[sshwire(variant_name = ch)] + // channel_type is implicit in ty below + #[sshwire(variant_name = ty)] pub num: u32, pub initial_window: u32, pub max_packet: u32, - pub ch: ChannelOpenType<'a>, + pub ty: ChannelOpenType<'a>, } #[derive(Debug, SSHEncode, SSHDecode)] @@ -796,7 +796,7 @@ mod tests { num: 111, initial_window: 50000, max_packet: 20000, - ch: ChannelOpenType::DirectTcpip(DirectTcpip { + ty: ChannelOpenType::DirectTcpip(DirectTcpip { address: "localhost".into(), port: 4444, origin: "somewhere".into(), @@ -809,7 +809,7 @@ mod tests { num: 0, initial_window: 899, max_packet: 14, - ch: ChannelOpenType::Session, + ty: ChannelOpenType::Session, }); test_roundtrip(&p); } @@ -821,7 +821,7 @@ mod tests { num: 0, initial_window: 899, max_packet: 14, - ch: ChannelOpenType::Session, + ty: ChannelOpenType::Session, }); let mut buf1 = vec![88; 1000]; let l = write_ssh(&mut buf1, &p).unwrap(); @@ -842,7 +842,7 @@ mod tests { num: 0, initial_window: 200000, max_packet: 88200, - ch: ChannelOpenType::Unknown(Unknown(b"audio-stream")) + ty: ChannelOpenType::Unknown(Unknown(b"audio-stream")) }); let mut buf1 = vec![88; 1000]; write_ssh(&mut buf1, &p).unwrap(); diff --git a/sshproto/src/sshnames.rs b/sshproto/src/sshnames.rs index 992a6e483b8a07252fb950e4f0ce2c98d0c40638..97b179b5e8a6485ad8683cb7ba9376295c97c387 100644 --- a/sshproto/src/sshnames.rs +++ b/sshproto/src/sshnames.rs @@ -55,7 +55,11 @@ pub const SSH_AUTHMETHOD_INTERACTIVE: &str = "keyboard-interactive"; pub const SSH_EXTENDED_DATA_STDERR: u32 = 1; /// [RFC4254](https://tools.ietf.org/html/rfc4254) -pub const SSH_OPEN_ADMINISTRATIVELY_PROHIBITED: u32 = 1; -pub const SSH_OPEN_CONNECT_FAILED: u32 = 2; -pub const SSH_OPEN_UNKNOWN_CHANNEL_TYPE: u32 = 3; -pub const SSH_OPEN_RESOURCE_SHORTAGE: u32 = 4; +#[allow(non_camel_case_types)] +pub enum ChanFail { + SSH_OPEN_ADMINISTRATIVELY_PROHIBITED = 1, + SSH_OPEN_CONNECT_FAILED = 2, + SSH_OPEN_UNKNOWN_CHANNEL_TYPE = 3, + SSH_OPEN_RESOURCE_SHORTAGE = 4, +} +