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,
+}
+