A convenient building block for writing such bidirectional pipes is something that connects the stdout and stdin of the current process together. Let us call it ioloop. After calling this function, you only need to start a regular pipe:
ioloop && # stdout -> stdin
cmd1 | cmd2 # stdin -> cmd1 -> cmd2 -> stdout (-> back to stdin)
If you do not want to modify the descriptors of the top-level shell, run this in a subshell:
( ioloop && cmd1 | cmd2 )
Here is a portable implementation of ioloop using a named pipe:
FIFO=$(mktemp -u /tmp/ioloop_$$_XXXXXX ) &&
trap "rm -f $FIFO" EXIT &&
mkfifo $FIFO &&
( : <$FIFO & ) && # avoid deadlock on opening pipe
exec >$FIFO <$FIFO
The named pipe exists in the filesystem only briefly during ioloop setup. This function is not quite POSIX because mktemp is deprecated (and potentially vulnerable to a race attack).
A linux-specific implementation using /proc/ is possible that does not require a named pipe, but I think this one is more than good enough.