Home streaming & inetd-style servers
Latest update:
The easiest way to stream a movie is to serve it using a static HTTP
server that supports range requests. For this, even Ruby's Webrick
will do the job. Type this in a directory with your The Sopranos
collection:
$ ruby -run -ehttpd . -b 127.0.0.1 -p 8000
& point mpv or vlc to a particular episode:
$ mpv http://127.0.0.1/s01e01.mp4
This should work as if you're playing a local file. To play a movie
with a web browser, make sure the web server returns correct
Content-Type
headers. A container format counts too: e.g., Chrome
doesn't like mkv.
Can we do something similar without the HTTP server? Depending on the
container format, it's possible to feed mpv with a raw TCP
stream. We'll lose seeking, but if we were creating, say, a YouTube
Shorts or Facebook Reels competitor, this won't matter, for consumers
of these kind of clips don't care much about that.
The most primitive solution requires only 2 utils:
ncat, that can listen on a socket & fork an external program when
someone connects to the former:
$ cat mickeymousetube
#!/bin/sh
export movie="${1:?Usage: ${0##*/} file.mkv [port]}"
port=${2:-61001}
type pv ncat || exit 1
__dirname=$(dirname "$(readlink -f "$0")")
ncat -vlk -e "$__dirname/pv.sh" 127.0.0.1 $port
pv, the famous pipe monitor that can limit a transfer rate;
without the limiter, mpv eats all available bandwidth:
$ cat pv.sh
#!/bin/sh
pv -L2M "$movie"
The -L2M
option means max 2MB/s.
Then run mickeymousetube
in one terminal & mpv tcp://127.0.0.1:61001
in another to play a clip.
tcplol
How hard may it be to replace ncat with our custom script? What
ncat does with -e
option is akin to what inetd did back in the
day:
(The illustration is from Stevens' UNIX Network Programming.)
Instead of creating a server that manages sockets, one writes a
program that simply reads from stdin and outputs to stdout. All the
intricacies of properly handling multiple clients are managed by the
super-duper-server.
There is no (x)inetd package in modern distros like Fedora, as systemd
has superseded it with socket activation.
Suppose we have a script that asks a user for his nickname & greets
him in return:
$ cat hello.sh
#!/bin/sh
uname 1>&2
while [ -z "$name" ]; do
printf "Nickname? "
read -r name || exit 1
done
echo "Hello, $name!"
To expose it to a network, we can either write 2 systemd unit files &
place them in ~/.config/systemd/user/
, or opt for a tiny 37 LOC Ruby script instead:
require 'socket'
usage = 'Usage: tcplol [-2v] [-h 127.0.0.1] -p 1234 program [args...]'
…
server = TCPServer.new opt['h'], opt['p']
loop do
client = server.accept
cid = client.remote_address.ip_unpack.join ':'
warn "Client #{cid}"
pid = fork do
$stdin.reopen client
$stdout.reopen client
$stderr.reopen client if opt['2']
client.close
exec(*ARGV)
end
client.close
Thread.new(cid, pid) do
Process.wait pid
warn "Client #{cid}: disconnect"
end
end
This is a classic fork server that uses a thread for each fork to
watch out for zombies. The linked tcplol script performs an additional
clean-up in case the server gets hit with a SIGINT, for example.
ncat, on the other hand, operates quite differently:
- it creates 2 pipes;
- after each new connection, it forks itself;
- it connects the 2 pipes to the child's stdin/stdout;
- (in the parent process) it listens on a connected socket using
select(2) syscall and transfers data to/from the child using the 2
pipes; we'll talk about select(2) and the concept of multiplexing
later on.
Anyhow, if we run our much simpler "super-server":
$ ./tcplol -v -p 1234 ./hello.sh
& connect to it with 2 socat clients, the process tree under Linux
would look like:
$ pstree `pgrep -f ./tcplol` -ap
ruby,259576 ./tcplol -v -p 8000 ./hello.sh
├─hello.sh,259580 ./hello.sh
├─hello.sh,259587 ./hello.sh
├─{ruby},259583
├─{ruby},259588
└─{ruby},259589
The dialog:
$ socat - TCP4:127.0.0.1:8000
Nickname? Dude
Hello, Dude!
(Why socat? We can use ncat as well, but the latter doesn't close
its end of a connection; it
hangs in CLOSE_WAIT until one presses Ctrl-D.)
To play a movie, run
$ ./tcplol -v -p 8000 ./pv.sh file.mkv
using a modified version of pv.sh script:
#!/bin/sh
echo Streaming "$1" 1>&2
pv -L2M "${1?Usage: pv.sh file}"
Then connect to the server with
$ mpv tcp://127.0.0.1:8000
Mickey mouse SOCKS4 server
inetd-style services can perform various actions, not just humbly
write to stdout. Nothing prevents such a service from opening a
connection to a different machine and relaying bytes from it to the
tcplol clients.
To illustrate the perils of the low-level socket interface, let's
write a crude, allow-everyone socks4 service and test it with
curl. The objective is to retrieve security.txt
file from Google
using a TLS connection like so:
$ curl -L https://google.com/.well-known/security.txt --proxy socks4://127.0.0.1:8000
As a socks4 client, curl sends a request to 127.0.0.1:8000
with an
IP+port to which it wants our service to establish a connection
(meaning we don't have to resolve google.com domain name
ourselves). We decode this and promptly send an acknowledgment
reply. This is the 1st part of socks4.rb which we are going run under
tcplol:
$stdout.sync = true
req = $stdin.read 8 + 1
ver, command, port, ip = req.unpack 'CCnN' # 8 bytes
abort 'Invalid CONNECT' unless ver == 4 && command == 1
ip = ip.to_s(16).scan(/.{2}/).map(&:hex) # [a,b,c,d]
res = [0, 90].pack('C*') + # request granted
[port].pack('n') + ip.pack('C*')
$stdout.write res
What should we do next? As soon as curl gets the value of 'res'
variable, it eagerly starts sending a TLS ClientHello message to
127.0.0.1:8000
. At this point, we don't need to analyse exactly what
it sends--our primary concern is relaying traffic to and fro as
quickly as possible without losing bytes.
To temporarily test that we have correctly negotiated SOCKS4, we can
conclude the script with the ncat call:
exec "ncat", "-v", ip.join('.'), port.to_s
It should work. However, we can also rewrite that line in pure Ruby
using the Kernel.select
method. What we need here is to monitor 2
file descriptors in different modes to react to changes in their
state:
- in reading mode: stdin and a TCP socket to google.com;
- in writing mode: the TCP socket to google.com.
(We assume that stdout is always available.) This kind of
programming--being notified when an IO connection is ready (for
reading or writing, for example) on a set of file descriptors--is
called IO multiplexing. Most web programmers never encounter it
because the socket interface is many levels below the stack they are
working in, but it may be interesting sometimes to see how the sausage
is made.
Replace exec "ncat"
line with:
require 'socket'
s = TCPSocket.new ip.join('.'), port
wbuf = []
BUFSIZ = 1024 * 1024
loop do
sockets = select [$stdin, s], [s], [], 5
sockets[0].each do |socket| # readable
if socket == $stdin
input = $stdin.readpartial BUFSIZ
wbuf << input
else
input = socket.readpartial BUFSIZ
$stdout.write input
end
end
sockets[1].each do |socket| # writable
wbuf.each { |v| socket.write v }
wbuf = []
end
end
We're establishing a connection to google.com and then initiating the
monitoring of two file descriptors in an endless loop. The select
method blocks until one or both of these file descriptors become
available for reading or writing. The last argument to it is a timeout
in seconds.
When select
unblocks, sockets[0]
contains an array of file
descriptors available for reading. If it's stdin, we read the data the
OS kernel thinks is obtainable & save such a chunk to wbuf
array. If
it is a socket to google.com, we read some bytes from it & immediately
write them to stdout for curl to consume.
sockets[1]
contains an array of file descriptors available for
writing. We only have 1 google.com socket here, to which we write the
contents of wbuf
array.
The script terminates when $stdin.readpartial
returns an
EOFError
. This indicates to curl that the other party has closed its
connection.
If you run socks4.rb under tcplol:
./tcplol -v -p 8000 ./socks4.rb
and observe errors tcplol prints from socks4.rb, you'll see that curl
makes 2 requests to google.com, for the first one yields 301.
$ curl -sLI https://google.com/.well-known/security.txt --proxy socks4://127.0.0.1:8000 | grep -E '^HTTP|content-length'
HTTP/2 301
content-length: 244
HTTP/2 200
content-length: 246
Tags: ойті
Authors: ag