Secure remote audio in Linux
How i watch videos at home is a very different affair to how many people watch videos. Most people would watch a video on their TV or their PC and interact with it directly. In my particular case i have a bunch of machines running an X server and ssh connected to the network. Centrally at home there is a storage server/Grid (though this is slowly becoming a mesh, with data being moved to/from devices on demand)
To watch a video on my system above i simply ssh into the correct 'screen' and fire up mplayer/mpv. One advantage of this is that i now have a console session open that acts as a remote control so that i may continue on with work on the laptop and still easily play/pause video or music.
One downside of this approach is i typically have to have headphones plugged into the device that is also doing the displaying. This is normally fine with a bit of interior planning but can on occasion be difficult due to lengths involved or force me to sit in particular (bad) positions in order to avoid having the headphones rip out of my ears when i move my head.
I have always been a big fan of pulse audio as it solves a lot of problems that other audio interfaces solve very poorly or not at all (including realtime stream reassignment, different volumes for different sources and being able to have different input devices to output devices for streaming or video chat). The one feature i have always wanted to use however is network audio so that i may use my laptops convenient headphone jack to listen to audio being played anywhere on my network.
There are a couple of ways to solve this problem, i could set up rtp via multicast on my home network, essential giving me audio 'channels' that i can hook into on any given network. I could use TCP and bring up explicit channels on the fly at the cost of a bit more work (This is how most articles on the internet set things up). Alternatively i can use the little known feature of recent ssh's to forward a UNIX socket across ssh.
Having recently set up systemd to automatically establish ssh tunnels to all the important devices on my home network and not liking things going in the clear over my network. O decided to set up a UNIX socket port forward over the existing ssh tunnels. These are implemented as separate systemd jobs that depend on the tunnel connections to ensure ordering and selectively enhance the original connection
This turned out to be the wrong option in the end and i quickly reverted this to a standalone connection. The reasons for this are the 'IPQoS' option below and when pushing large amounts of data over the network, ssh will not share the bandwidth on the connection in the way you would expect Linux to and as such you may get worse delay/stuttering from the connection.
The syntax for an ssh port forward for UNIX sockets is near identical to that of a TCP port forward
$ ssh -L /run/user/1000/pulse/native:/run/user/1000/pulse/native myhost.domain
the only real additions i make to the ssh job is to add "-O IPQoS=lowdelay" for those cases where audio data will be crossing a router (we can compensate for network delays in pulseaudio and audio/video software that can do latency compensation such as gstreamer).
The other useful addition is to call the command 'pactl subscribe' as the command ssh calls on the remote end rather than setting up ssh to idle in the background. this brings better responsiveness to the link for when the remote end disappears, be it the network link or pulse audio being killed during testing. The output of the command is discarded as we are mainly interested in the exit code for letting systemd restart the link.
The next design option to investigate was the 2 methods of connecting to the
remote pulse audio instance. With a simple setup, where you just need to
forward audio to a remote pulse audio instance, setting
$PUSE_SERVER=/run/user/1000/pulse/native is enough. This will cause all
programs to connect to the remote instance directly and appear as different
programs to pulse audio. allowing per application volume control and muting as
well as reassignment on the remote end
The other alternative is to run a pulse audio daemon locally and have it connect to the remote instance over ssh. The downside of this is that there is only one connection to the remote pulse audio and management of the individual applications must be done on the local end before all streams are muxxed and sent to the remote end. this may be useful if you only want to stream some audio to the remote end and don't care about differentiating them or more importantly you want to be able to move and 'recover' audio streams (eg you changed wifi networks and the connection to the remote daemon broke)
To activate this mode, ensure there is a running local pulseaudio instance then load the relevant module as bellow
pactl load-module module-tunnel-sink server=unix:/home/dablitz/.cache/pulse/myhost.domain.socket > /home/dablitz/.cache/pulse/myhost.domain.module_id
you will need to update the paths to match your environment (and on my systemd setup i use polyistanciated services with %i to simplify things). It is important to note to save the output of pactl, this will be an integer of the module instance that is loaded. You will need this in your cleanup action to unload the correct module as specifying the module name will unload all instance of that module. if you maintain connections to multiple servers then you will lose your audio for all remote servers.
At the moment i am using the latter on my laptop and considering using the former to stream from the remote viewing servers/monitors to my laptop (if the connection broke then its likely that i left the area/not watching the stream any more or my control session is dead anyway)
While this setup has some overhead from ssh, in practice it has proven negligible in terms of latency while leaving sound quality unaffected. This is a significantly more secure setup than the TCP setup that is commonly found on the internet when googling and one i would trust to run over the raw internet (which cannot be said for the raw TCP method). As for the RTP method, while i can make this work with VPNs and multicast, this would be vastly more complex and not take advantage of RTP and multicasts strengths (as i would be using it mainly for Point to point connections) but would be an option for multiple listeners such as a network radio station or broadcasting to multiple speakers, in both cases on the he same vlan (the complexity arises when you need to pass through a router)
My (highly incorrect) systemd.service file as it exists on disk at the moment:
[Unit] Description=Add remote pulse audio instances to output devices [Service] ExecStart=/usr/bin/ssh -n -o ControlPath=none -o IPQoS=lowdelay %i -L %h/.cache/ssh/pulse.%i:/run/user/1000/pulse/native -- pactl subscribe ExecStartPost=/bin/sh -c 'pactl load-module module-tunnel-sink server=unix:%h/.cache/ssh/pulse.%i > %h/.cache/ssh/pulse.%i.id' ExecStopPost=/bin/sh -c 'pactl unload-module `cat %h/.cache/ssh/pulse.%i.id`' ; trie ExecStopPost=/bin/rm -f %h/.cache/ssh/pulse.%i.id Type=simple
The above will require adaptation and a bit more work. I am currently refining it and threading it into my existing systemd managed network detection, however in its current state it should be adaptable for implementing the above
While i have not provided copy and paste examples i hope there is enough information provided here to quickly and rapidly piece something together that is not the insecure copy and paste advice on stack overflow and the rest of the internet and also show off a cool usage of the ssh unix socket port forwarding