summaryrefslogtreecommitdiff
path: root/usr.bin
diff options
context:
space:
mode:
authorMarkus Friedl <markus@cvs.openbsd.org>1999-10-17 16:56:10 +0000
committerMarkus Friedl <markus@cvs.openbsd.org>1999-10-17 16:56:10 +0000
commit0238757d8ee02e7037dc604d487a1629085383ce (patch)
tree9b1500f45db4597e51335ccc02f356a83147a5bf /usr.bin
parent5cbb3086fbb15213b5d1fbc89fbd23641a8a95fa (diff)
re-implement the proto-1.5 channel close protocol, see nchan.ms.
Diffstat (limited to 'usr.bin')
-rw-r--r--usr.bin/ssh/channels.c45
-rw-r--r--usr.bin/ssh/channels.h9
-rw-r--r--usr.bin/ssh/nchan.c231
-rw-r--r--usr.bin/ssh/nchan.h76
-rw-r--r--usr.bin/ssh/nchan.ms46
5 files changed, 260 insertions, 147 deletions
diff --git a/usr.bin/ssh/channels.c b/usr.bin/ssh/channels.c
index 81b30fce868..0d4b1d5eda5 100644
--- a/usr.bin/ssh/channels.c
+++ b/usr.bin/ssh/channels.c
@@ -16,7 +16,7 @@ arbitrary tcp/ip connections, and the authentication agent connection.
*/
#include "includes.h"
-RCSID("$Id: channels.c,v 1.15 1999/10/16 21:19:00 deraadt Exp $");
+RCSID("$Id: channels.c,v 1.16 1999/10/17 16:56:08 markus Exp $");
#include "ssh.h"
#include "packet.h"
@@ -136,10 +136,11 @@ int channel_allocate(int type, int sock, char *remote_name)
buffer_init(&channels[i].output);
channels[i].self = i;
channels[i].type = type;
+ channels[i].x11 = 0;
channels[i].sock = sock;
- channels[i].flags = 0;
channels[i].remote_id = -1;
channels[i].remote_name = remote_name;
+ chan_init_iostates(&channels[i]);
return i;
}
@@ -156,10 +157,11 @@ int channel_allocate(int type, int sock, char *remote_name)
buffer_init(&channels[old_channels].output);
channels[old_channels].self = old_channels;
channels[old_channels].type = type;
+ channels[old_channels].x11 = 0;
channels[old_channels].sock = sock;
- channels[old_channels].flags = 0;
channels[old_channels].remote_id = -1;
channels[old_channels].remote_name = remote_name;
+ chan_init_iostates(&channels[old_channels]);
return old_channels;
}
@@ -213,17 +215,14 @@ void channel_prepare_select(fd_set *readset, fd_set *writeset)
break;
}
/* test whether sockets are 'alive' for read/write */
- if (!(ch->flags & CHAN_SHUT_RD))
+ if (ch->istate == CHAN_INPUT_OPEN)
if (buffer_len(&ch->input) < 32768)
FD_SET(ch->sock, readset);
- if (!(ch->flags & CHAN_SHUT_WR)){
+ if (ch->ostate == CHAN_OUTPUT_OPEN || ch->ostate == CHAN_OUTPUT_WAIT_DRAIN){
if (buffer_len(&ch->output) > 0){
FD_SET(ch->sock, writeset);
- }else if(ch->flags & CHAN_IEOF_RCVD){
- /* if output-buffer empty AND IEOF received,
- we won't get more data for writing */
- chan_shutdown_write(ch);
- chan_send_oclose(ch);
+ }else if(ch->ostate == CHAN_OUTPUT_WAIT_DRAIN) {
+ chan_obuf_empty(ch);
}
}
break;
@@ -319,6 +318,8 @@ void channel_prepare_select(fd_set *readset, fd_set *writeset)
/* Start normal processing for the channel. */
ch->type = SSH_CHANNEL_OPEN;
+ /* Enable X11 Problem FIX */
+ ch->x11 = 1;
goto redo;
reject:
@@ -335,10 +336,10 @@ void channel_prepare_select(fd_set *readset, fd_set *writeset)
packet_put_int(ch->remote_id);
packet_send();
}else{
- chan_shutdown_read(ch); /* shutdown, since close() does not update ch->flags */
- chan_send_ieof(ch); /* no need to wait for output-buffer */
- chan_shutdown_write(ch);
- chan_send_oclose(ch);
+ debug("X11 rejected %d 0x%x 0x%x", ch->self, ch->istate, ch->ostate);
+ chan_read_failed(ch);
+ chan_write_failed(ch);
+ debug("X11 rejected %d 0x%x 0x%x", ch->self, ch->istate, ch->ostate);
}
break;
@@ -461,10 +462,7 @@ void channel_after_select(fd_set *readset, fd_set *writeset)
ch->type = SSH_CHANNEL_INPUT_DRAINING;
debug("Channel %d status set to input draining.", i);
}else{
- buffer_consume(&ch->output, buffer_len(&ch->output));
- chan_shutdown_read(ch);
- /* we have to wait until the input-buffer has been
- sent to our peer before we can send IEOF */
+ chan_read_failed(ch);
}
break;
}
@@ -482,15 +480,12 @@ void channel_after_select(fd_set *readset, fd_set *writeset)
debug("Channel %d status set to input draining.", i);
ch->type = SSH_CHANNEL_INPUT_DRAINING;
}else{
- buffer_consume(&ch->output, buffer_len(&ch->output));
- chan_shutdown_write(ch);
- chan_send_oclose(ch);
+ chan_write_failed(ch);
}
break;
}
buffer_consume(&ch->output, len);
}
- chan_del_if_dead(ch);
break;
case SSH_CHANNEL_OUTPUT_DRAINING:
@@ -553,13 +548,13 @@ void channel_output_poll()
packet_send();
buffer_consume(&ch->input, len);
}
- else if(ch->flags & CHAN_SHUT_RD)
+ else if(ch->istate == CHAN_INPUT_WAIT_DRAIN)
{
if (compat13)
- fatal("cannot happen: CHAN_SHUT_RD set for proto 1.3");
+ fatal("cannot happen: istate == INPUT_WAIT_DRAIN for proto 1.3");
/* input-buffer is empty and read-socket shutdown:
tell peer, that we will not send more data: send IEOF */
- chan_send_ieof(ch);
+ chan_ibuf_empty(ch);
}
}
}
diff --git a/usr.bin/ssh/channels.h b/usr.bin/ssh/channels.h
index a046889d5d3..9cdf10f3111 100644
--- a/usr.bin/ssh/channels.h
+++ b/usr.bin/ssh/channels.h
@@ -1,4 +1,4 @@
-/* RCSID("$Id: channels.h,v 1.2 1999/10/16 22:29:00 markus Exp $"); */
+/* RCSID("$Id: channels.h,v 1.3 1999/10/17 16:56:09 markus Exp $"); */
#ifndef CHANNELS_H
#define CHANNELS_H
@@ -17,17 +17,18 @@
#define SSH_CHANNEL_INPUT_DRAINING 10 /* sending remaining data to conn */
#define SSH_CHANNEL_OUTPUT_DRAINING 11 /* sending remaining data to app */
-
/* Data structure for channel data. This is iniailized in channel_allocate
and cleared in channel_free. */
-typedef struct
+typedef struct Channel
{
int type; /* channel type/state */
int self; /* my own channel identifier */
int remote_id; /* channel identifier for remote peer */
/* peer can be reached over encrypted connection, via packet-sent */
- int flags; /* flags for close in proto 1.5 */
+ int istate;
+ int ostate;
+ int x11;
int sock; /* data socket, linked to this channel */
Buffer input; /* data read from socket, to be sent over encrypted connection */
Buffer output; /* data received over encrypted connection for send on socket */
diff --git a/usr.bin/ssh/nchan.c b/usr.bin/ssh/nchan.c
index 1916894ab06..175cd666f9b 100644
--- a/usr.bin/ssh/nchan.c
+++ b/usr.bin/ssh/nchan.c
@@ -1,101 +1,200 @@
#include "includes.h"
-RCSID("$Id: nchan.c,v 1.2 1999/10/16 22:29:01 markus Exp $");
+RCSID("$Id: nchan.c,v 1.3 1999/10/17 16:56:09 markus Exp $");
#include "ssh.h"
#include "buffer.h"
-#include "channels.h"
#include "packet.h"
+#include "channels.h"
#include "nchan.h"
+static void chan_send_ieof(Channel *c);
+static void chan_send_oclose(Channel *c);
+static void chan_shutdown_write(Channel *c);
+static void chan_shutdown_read(Channel *c);
+static void chan_delele_if_full_closed(Channel *c);
+
+/*
+ * EVENTS: update channel input/ouput states
+ * execute ACTIONS
+ */
+/* events concerning the INPUT from socket for channel (istate) */
void
-dump_chan(Channel *c){
- debug("chan %d type %d flags 0x%x", c->self, c->type, c->flags);
+chan_rcvd_oclose(Channel *c){
+ switch(c->istate){
+ case CHAN_INPUT_WAIT_OCLOSE:
+ debug("channel %d: INPUT_WAIT_CLOSE -> INPUT_CLOSED [rcvd OCLOSE]", c->self);
+ c->istate=CHAN_INPUT_CLOSED;
+ chan_delele_if_full_closed(c);
+ break;
+ case CHAN_INPUT_OPEN:
+ debug("channel %d: INPUT_OPEN -> INPUT_CLOSED [rvcd OCLOSE, send IEOF]", c->self);
+ chan_shutdown_read(c);
+ chan_send_ieof(c);
+ c->istate=CHAN_INPUT_CLOSED;
+ chan_delele_if_full_closed(c);
+ break;
+ default:
+ debug("protocol error: chan_rcvd_oclose %d for istate %d",c->self,c->istate);
+ break;
+ }
}
void
-chan_rcvd_ieof(Channel *c){
- dump_chan(c);
- if(c->flags & CHAN_IEOF_RCVD){
- debug("chan_rcvd_ieof twice: %d",c->self);
- return;
+chan_read_failed(Channel *c){
+ switch(c->istate){
+ case CHAN_INPUT_OPEN:
+ debug("channel %d: INPUT_OPEN -> INPUT_WAIT_DRAIN [read failed]", c->self);
+ chan_shutdown_read(c);
+ c->istate=CHAN_INPUT_WAIT_DRAIN;
+ break;
+ default:
+ debug("internal error: we do not read, but chan_read_failed %d for istate %d",
+ c->self,c->istate);
+ break;
}
- debug("rcvd_CHAN_IEOF %d",c->self);
- c->flags |= CHAN_IEOF_RCVD;
- /* cannot clear input buffer. remaining data has to be sent to client */
- chan_del_if_dead(c);
}
void
-chan_rcvd_oclose(Channel *c){
- dump_chan(c);
- if(c->flags & CHAN_OCLOSE_RCVD){
- debug("chan_rcvd_oclose twice: %d",c->self);
+chan_ibuf_empty(Channel *c){
+ if(buffer_len(&c->input)){
+ debug("internal error: chan_ibuf_empty %d for non empty buffer",c->self);
return;
}
- debug("rcvd_CHAN_OCLOSE %d",c->self);
- c->flags |= CHAN_OCLOSE_RCVD;
- /* our peer can no longer consume, so there is not need to read */
- chan_shutdown_read(c);
- buffer_consume(&c->output, buffer_len(&c->output));
- /* Note: for type==OPEN IEOF is sent by channel_output_poll() */
- chan_del_if_dead(c);
+ switch(c->istate){
+ case CHAN_INPUT_WAIT_DRAIN:
+ debug("channel %d: INPUT_WAIT_DRAIN -> INPUT_WAIT_OCLOSE [inbuf empty, send OCLOSE]", c->self);
+ chan_send_ieof(c);
+ c->istate=CHAN_INPUT_WAIT_OCLOSE;
+ break;
+ default:
+ debug("internal error: chan_ibuf_empty %d for istate %d",c->self,c->istate);
+ break;
+ }
}
+/* events concerning the OUTPUT from channel for socket (ostate) */
void
-chan_send_ieof(Channel *c){
- if(c->flags & CHAN_IEOF_SENT){
- /* this is ok: it takes some time before we get OCLOSE */
- /* debug("send_chan_ieof twice %d", c->self); */
+chan_rcvd_ieof(Channel *c){
+
+ /* X11: if we receive IEOF for X11, then we have to FORCE sending of IEOF,
+ * this is from ssh-1.2.27 debugging output.
+ */
+ if(c->x11){
+ debug("channel %d: OUTPUT_OPEN -> OUTPUT_CLOSED/INPUT_WAIT_OCLOSED [X11 FIX]", c->self);
+ chan_send_ieof(c);
+ c->istate=CHAN_INPUT_WAIT_OCLOSE;
+ chan_send_oclose(c);
+ c->ostate=CHAN_OUTPUT_CLOSED;
+ chan_delele_if_full_closed(c);
return;
}
- debug("send_CHAN_IEOF %d", c->self);
- packet_start(CHAN_IEOF);
- packet_put_int(c->remote_id);
- packet_send();
- c->flags |= CHAN_IEOF_SENT;
- dump_chan(c);
+ switch(c->ostate){
+ case CHAN_OUTPUT_OPEN:
+ debug("channel %d: OUTPUT_OPEN -> OUTPUT_WAIT_DRAIN [rvcd IEOF]", c->self);
+ c->ostate=CHAN_OUTPUT_WAIT_DRAIN;
+ break;
+ case CHAN_OUTPUT_WAIT_IEOF:
+ debug("channel %d: OUTPUT_WAIT_IEOF -> OUTPUT_CLOSED [rvcd IEOF]", c->self);
+ c->ostate=CHAN_OUTPUT_CLOSED;
+ chan_delele_if_full_closed(c);
+ break;
+ default:
+ debug("protocol error: chan_rcvd_ieof %d for ostate %d", c->self,c->ostate);
+ break;
+ }
}
void
-chan_send_oclose(Channel *c){
- if(c->flags & CHAN_OCLOSE_SENT){
- debug("send_chan_oclose twice %d", c->self);
- return;
+chan_write_failed(Channel *c){
+ switch(c->ostate){
+ case CHAN_OUTPUT_OPEN:
+ debug("channel %d: OUTPUT_OPEN -> OUTPUT_WAIT_IEOF [write failed]", c->self);
+ chan_send_oclose(c);
+ c->ostate=CHAN_OUTPUT_WAIT_IEOF;
+ break;
+ case CHAN_OUTPUT_WAIT_DRAIN:
+ debug("channel %d: OUTPUT_WAIT_DRAIN -> OUTPUT_CLOSED [write failed]", c->self);
+ chan_send_oclose(c);
+ c->ostate=CHAN_OUTPUT_CLOSED;
+ chan_delele_if_full_closed(c);
+ break;
+ default:
+ debug("internal error: chan_write_failed %d for ostate %d",c->self,c->ostate);
+ break;
}
- debug("send_CHAN_OCLOSE %d", c->self);
- packet_start(CHAN_OCLOSE);
- packet_put_int(c->remote_id);
- packet_send();
- c->flags |= CHAN_OCLOSE_SENT;
- dump_chan(c);
}
void
-chan_shutdown_write(Channel *c){
- if(c->flags & CHAN_SHUT_WR){
- debug("chan_shutdown_write twice %d",c->self);
+chan_obuf_empty(Channel *c){
+ if(buffer_len(&c->output)){
+ debug("internal error: chan_obuf_empty %d for non empty buffer",c->self);
return;
}
- debug("chan_shutdown_write %d", c->self);
+ switch(c->ostate){
+ case CHAN_OUTPUT_WAIT_DRAIN:
+ debug("channel %d: OUTPUT_WAIT_DRAIN -> OUTPUT_CLOSED [obuf empty, send OCLOSE]", c->self);
+ chan_send_oclose(c);
+ c->ostate=CHAN_OUTPUT_CLOSED;
+ chan_delele_if_full_closed(c);
+ break;
+ default:
+ debug("internal error: chan_obuf_empty %d for ostate %d",c->self,c->ostate);
+ break;
+ }
+}
+/*
+ * ACTIONS: should never update c->istate or c->ostate
+ */
+static void
+chan_send_ieof(Channel *c){
+ switch(c->istate){
+ case CHAN_INPUT_OPEN:
+ case CHAN_INPUT_WAIT_DRAIN:
+ packet_start(SSH_MSG_CHANNEL_INPUT_EOF);
+ packet_put_int(c->remote_id);
+ packet_send();
+ break;
+ default:
+ debug("internal error: channel %d: cannot send IEOF for istate %d",c->self,c->istate);
+ break;
+ }
+}
+static void
+chan_send_oclose(Channel *c){
+ switch(c->ostate){
+ case CHAN_OUTPUT_OPEN:
+ case CHAN_OUTPUT_WAIT_DRAIN:
+ chan_shutdown_write(c);
+ buffer_consume(&c->output, buffer_len(&c->output));
+ packet_start(SSH_MSG_CHANNEL_OUTPUT_CLOSE);
+ packet_put_int(c->remote_id);
+ packet_send();
+ break;
+ default:
+ debug("internal error: channel %d: cannot send IEOF for istate %d",c->self,c->istate);
+ break;
+ }
+}
+/* helper */
+static void
+chan_shutdown_write(Channel *c){
+ debug("channel %d: shutdown_write", c->self);
if(shutdown(c->sock, SHUT_WR)<0)
- error("chan_shutdown_write failed %.100s", strerror(errno));
- c->flags |= CHAN_SHUT_WR;
- /* clear output buffer, since there is noone going to read the data
- we just closed the output-socket */
- /* buffer_consume(&c->output, buffer_len(&c->output)); */
+ error("chan_shutdown_write failed for %d/%d %.100s",
+ c->self, c->sock, strerror(errno));
}
-void
+static void
chan_shutdown_read(Channel *c){
- if(c->flags & CHAN_SHUT_RD){
- /* chan_shutdown_read is called for read-errors and OCLOSE */
- /* debug("chan_shutdown_read twice %d",c->self); */
- return;
- }
- debug("chan_shutdown_read %d", c->self);
+ debug("channel %d: shutdown_read", c->self);
if(shutdown(c->sock, SHUT_RD)<0)
- error("chan_shutdown_read failed %.100s", strerror(errno));
- c->flags |= CHAN_SHUT_RD;
+ error("chan_shutdown_read failed for %d/%d %.100s",
+ c->self, c->sock, strerror(errno));
}
-void
-chan_del_if_dead(Channel *c){
- if(c->flags == CHAN_CLOSED){
- debug("channel %d closing",c->self);
+static void
+chan_delele_if_full_closed(Channel *c){
+ if(c->istate==CHAN_INPUT_CLOSED && c->ostate==CHAN_OUTPUT_CLOSED){
+ debug("channel %d: closing", c->self);
channel_free(c->self);
}
}
+void
+chan_init_iostates(Channel *c){
+ c->ostate=CHAN_OUTPUT_OPEN;
+ c->istate=CHAN_INPUT_OPEN;
+}
diff --git a/usr.bin/ssh/nchan.h b/usr.bin/ssh/nchan.h
index f312b0ec64b..5fb3bc04e90 100644
--- a/usr.bin/ssh/nchan.h
+++ b/usr.bin/ssh/nchan.h
@@ -1,4 +1,4 @@
-/* RCSID("$Id: nchan.h,v 1.2 1999/10/16 22:29:01 markus Exp $"); */
+/* RCSID("$Id: nchan.h,v 1.3 1999/10/17 16:56:09 markus Exp $"); */
#ifndef NCHAN_H
#define NCHAN_H
@@ -25,61 +25,33 @@
* See the debugging output from 'ssh -v' and 'sshd -d' of
* ssh-1.2.27 as an example.
*
- * Details: (for Channel data structure see channels.h)
- *
- * - the output_buffer gets data received from the remote peer and
- * is written to the socket,
- * - the input_buffer gets data from the socket and is sent to remote peer.
- * - the socket represents the local object communicating with an object
- * reachable via the peer
- *
- * PEER A PEER B
- *
- * read(sock, input_buffer) < 0;
- * shutdown_read();
- * flush(input_buffer) =: DATA
- * send(DATA) -> rcvd(DATA)
- * write(sock, output_buffer:=DATA);
- * send(IEOF) -> rcvd(IEOF)
- * shutdown_write() if:
- * a) write fails
- * b) rcvd_IEOF==true &&
- * output_buffer==empty
- * rcvd(OCLOSE) <- send(OCLOSE)
- *
- * The channel is now half closed. No data will flow from A to B.
- *
- * Note that each side can remove the channel only if 2 messages
- * have been sent and received and the associated socket has been
- * shutdown, see below:
*/
-enum {
- /* ssh-proto-1.5 overloads message-types */
- CHAN_IEOF = SSH_MSG_CHANNEL_CLOSE,
- /* there will be no more data from sender */
- CHAN_OCLOSE = SSH_MSG_CHANNEL_CLOSE_CONFIRMATION,
- /* all received data has been written to the socket */
+/* ssh-proto-1.5 overloads prot-1.3-message-types */
+#define SSH_MSG_CHANNEL_INPUT_EOF SSH_MSG_CHANNEL_CLOSE
+#define SSH_MSG_CHANNEL_OUTPUT_CLOSE SSH_MSG_CHANNEL_CLOSE_CONFIRMATION
- /* channel close flags */
- CHAN_IEOF_SENT = 0x01,
- CHAN_IEOF_RCVD = 0x02,
- CHAN_OCLOSE_SENT = 0x04,
- CHAN_OCLOSE_RCVD = 0x08,
- CHAN_SHUT_RD = 0x10,
- CHAN_SHUT_WR = 0x20,
+/* possible input states */
+#define CHAN_INPUT_OPEN 0x01
+#define CHAN_INPUT_WAIT_DRAIN 0x02
+#define CHAN_INPUT_WAIT_OCLOSE 0x04
+#define CHAN_INPUT_CLOSED 0x08
- /* a channel can be removed if ALL the following flags are set: */
- CHAN_CLOSED = CHAN_IEOF_SENT | CHAN_IEOF_RCVD |
- CHAN_OCLOSE_SENT | CHAN_OCLOSE_RCVD |
- CHAN_SHUT_RD | CHAN_SHUT_WR
-};
+/* possible output states */
+#define CHAN_OUTPUT_OPEN 0x10
+#define CHAN_OUTPUT_WAIT_DRAIN 0x20
+#define CHAN_OUTPUT_WAIT_IEOF 0x40
+#define CHAN_OUTPUT_CLOSED 0x80
-void chan_del_if_dead(Channel *c);
-void chan_rcvd_ieof(Channel *c);
+/* EVENTS for the input state */
void chan_rcvd_oclose(Channel *c);
-void chan_send_ieof(Channel *c);
-void chan_send_oclose(Channel *c);
-void chan_shutdown_read(Channel *c);
-void chan_shutdown_write(Channel *c);
+void chan_read_failed(Channel *c);
+void chan_ibuf_empty(Channel *c);
+
+/* EVENTS for the output state */
+void chan_rcvd_ieof(Channel *c);
+void chan_write_failed(Channel *c);
+void chan_obuf_empty(Channel *c);
+
+void chan_init_iostates(Channel *c);
#endif
diff --git a/usr.bin/ssh/nchan.ms b/usr.bin/ssh/nchan.ms
new file mode 100644
index 00000000000..642e07eefe6
--- /dev/null
+++ b/usr.bin/ssh/nchan.ms
@@ -0,0 +1,46 @@
+.TL
+OpenSSH Channel Close Protocol 1.5 Implementation
+.SH
+Channel Input State Diagram
+.PS
+reset
+l=1
+s=1.2
+ellipsewid=s*ellipsewid
+boxwid=s*boxwid
+ellipseht=s*ellipseht
+S1: ellipse "INPUT" "OPEN"
+move right 2*l from last ellipse.e
+S4: ellipse "INPUT" "CLOSED"
+move down l from last ellipse.s
+S3: ellipse "INPUT" "WAIT" "OCLOSED"
+move down l from 1st ellipse.s
+S2: ellipse "INPUT" "WAIT" "DRAIN"
+arrow "" "rcvd OCLOSE/" "shutdown_read" "send IEOF" from S1.e to S4.w
+arrow "ibuf_empty/" "send IEOF" from S2.e to S3.w
+arrow from S1.s to S2.n
+box invis "read_failed/" "shutdown_read" with .e at last arrow.c
+arrow from S3.n to S4.s
+box invis "rcvd OCLOSE/" "-" with .w at last arrow.c
+ellipse wid .9*ellipsewid ht .9*ellipseht at S4
+arrow "start" "" from S1.w+(-0.5,0) to S1.w
+.PE
+.SH
+Channel Output State Diagram
+.PS
+S1: ellipse "OUTPUT" "OPEN"
+move right 2*l from last ellipse.e
+S3: ellipse "OUTPUT" "WAIT" "IEOF"
+move down l from last ellipse.s
+S4: ellipse "OUTPUT" "CLOSED"
+move down l from 1st ellipse.s
+S2: ellipse "OUTPUT" "WAIT" "DRAIN"
+arrow "" "write_failed/" "shutdown_write" "send OCLOSE" from S1.e to S3.w
+arrow "obuf_empty ||" "write_failed/" "shutdown_write" "send OCLOSE" from S2.e to S4.w
+arrow from S1.s to S2.n
+box invis "rcvd IEOF/" "-" with .e at last arrow.c
+arrow from S3.s to S4.n
+box invis "rcvd IEOF/" "-" with .w at last arrow.c
+ellipse wid .9*ellipsewid ht .9*ellipseht at S4
+arrow "start" "" from S1.w+(-0.5,0) to S1.w
+.PE