489 lines
12 KiB
Bash
489 lines
12 KiB
Bash
#!/bin/sh
|
|
# Setup and run encrypted connection to remote PulseAudio/Pipewire server
|
|
# Copyright (C) 2023 Tilman Kranz <t.kranz@tk-sls.de>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
##
|
|
# Arguments
|
|
|
|
operation=$1
|
|
|
|
##
|
|
# Configuration
|
|
|
|
config_dir="$HOME"/.config/pulseaudio-tcp
|
|
config="$config_dir"/config.inc.sh
|
|
|
|
##
|
|
# Functions
|
|
|
|
_ssh() {
|
|
if ssh -S "$USER"-pulseaudio "$remote_user"@"$remote_ip" "$@" ; then
|
|
return 0
|
|
else
|
|
echo "ERROR: SSH remote_ip=$remote_ip failed." >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Perform setup operation
|
|
do_setup() {
|
|
if test -e "$config" ; then
|
|
read -r -p "config=$config already exists, overwrite it? (Y|n) " answer
|
|
|
|
case "$answer" in
|
|
y|Y|"")
|
|
if ! test -w "$config" ; then
|
|
echo "ERROR: config=$config is not writable." >&2 ;
|
|
return 1 ;
|
|
elif ! test -r "$config" ; then
|
|
echo "ERROR: config=$config is not readable." >&2 ;
|
|
return 1 ;
|
|
else
|
|
. "$config"
|
|
fi
|
|
;;
|
|
*)
|
|
echo "ERROR: Setup aborted." >&2
|
|
return 1
|
|
esac
|
|
elif ! test -e "$config_dir" ; then
|
|
mkdir -p "$config_dir" || {
|
|
echo "ERROR: Could not create config_dir=$config_dir." >&2 ;
|
|
return 1 ;
|
|
}
|
|
elif ! test -d "$config_dir" ; then
|
|
echo "ERROR: config_dir=$config_dir is not a directory." >&2
|
|
return 1
|
|
elif ! test -w "$config_dir" ; then
|
|
echo "ERROR: config_dir=$config_dir is not writable." >&2
|
|
return 1
|
|
fi
|
|
|
|
while true ; do
|
|
read -r -p "Enter IP address of remote host ($remote_ip): " remote_ip_in
|
|
|
|
if test -n "$remote_ip_in" ; then
|
|
break
|
|
elif test -n "$remote_ip" ; then
|
|
remote_ip_in=$remote_ip
|
|
break
|
|
fi
|
|
done
|
|
|
|
while true ; do
|
|
read -r -p "Enter username on remote host $remote_ip ($remote_user): " remote_user_in
|
|
|
|
if test -n "$remote_user_in" ; then
|
|
break
|
|
elif test -n "$remote_user" ; then
|
|
remote_user_in=$remote_user
|
|
break
|
|
fi
|
|
done
|
|
|
|
inbound=${inbound:-true}
|
|
|
|
while true ; do
|
|
if "$inbound" ; then
|
|
prompt="Y/n"
|
|
else
|
|
prompt="y/N"
|
|
fi
|
|
|
|
read -r -p "Enable inbound audio ($prompt): " inbound_in
|
|
|
|
case "$inbound_in" in
|
|
"")
|
|
if test -n "$inbound" ; then
|
|
inbound_in=$inbound
|
|
break 2
|
|
fi
|
|
;;
|
|
y|Y)
|
|
inbound_in=true
|
|
break 2
|
|
;;
|
|
n|N)
|
|
inbound_in=false
|
|
break 2
|
|
;;
|
|
*)
|
|
echo "ERROR: Please type \"y\" or \"n\"." >&2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
outbound=${outbound:-true}
|
|
|
|
while true ; do
|
|
if "$outbound" ; then
|
|
prompt="Y/n"
|
|
else
|
|
prompt="y/N"
|
|
fi
|
|
|
|
read -r -p "Enable outbound audio ($prompt): " outbound_in
|
|
|
|
case "$outbound_in" in
|
|
"")
|
|
if test -n "$outbound" ; then
|
|
outbound_in=$outbound
|
|
break 2
|
|
fi
|
|
;;
|
|
y|Y)
|
|
outbound_in=true
|
|
break 2
|
|
;;
|
|
n|N)
|
|
outbound_in=false
|
|
break 2
|
|
;;
|
|
*)
|
|
echo "ERROR: Please type \"y\" or \"n\"." >&2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
cat > "$config" << EOF
|
|
# Configuration file for pulseaudio-tcp
|
|
# Generated on $(LC_ALL=C date) by $USER using $0
|
|
|
|
# IP address of remote host
|
|
remote_ip="$remote_ip_in"
|
|
|
|
# Username on remote host
|
|
remote_user="$remote_user_in"
|
|
|
|
# Enable inbound audio from $remote_user @$remote_ip?
|
|
inbound=$inbound_in
|
|
|
|
# Enable outbound audio to $remote_user @$remote_ip?
|
|
outbound=$outbound_in
|
|
EOF
|
|
}
|
|
|
|
# Check if SSH port forwarding is running
|
|
check_pa_ssh() {
|
|
for pid in $(pidof ssh) ; do
|
|
if grep -Fq $USER-pulseaudio /proc/"$pid"/cmdline ; then
|
|
grep -Fq -e -L /proc/"$pid"/cmdline || {
|
|
echo "ERROR: No SSH port forwarding to remote server is established." >&2
|
|
return 1 ;
|
|
}
|
|
fi
|
|
done
|
|
|
|
echo "ERROR: No SSH is established." >&2
|
|
return 1
|
|
}
|
|
|
|
# Perform status operation
|
|
do_status() {
|
|
rv=0
|
|
|
|
if ! check_pa_ssh ; then
|
|
rv=1
|
|
fi
|
|
|
|
if ! _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then
|
|
echo "ERROR: PulseAudio module \"module-native-protocol-tcp\" is not loaded on remote_ip=$remote_ip." >&2
|
|
rv=1
|
|
fi
|
|
|
|
if "$outbound" ; then
|
|
if ! pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then
|
|
echo "ERROR: PulseAudio module \"module-tunnel-sink\" is not loaded." >&2
|
|
rv=1
|
|
fi
|
|
|
|
if ! pactl get-default-sink | grep -Fq "tunnel-sink.tcp:127.0.0.1" ; then
|
|
echo "ERROR: \"tunnel-sink.tcp:127.0.0.1\" is not the default PulseAudio sink." >&2
|
|
rv=1
|
|
fi
|
|
fi
|
|
|
|
if "$inbound" ; then
|
|
if ! pactl list modules | grep -Fq "Name: module-tunnel-source" ; then
|
|
echo "ERROR: PulseAudio module \"module-tunnel-source\" is not loaded." >&2
|
|
rv=1
|
|
fi
|
|
fi
|
|
|
|
if test "$rv" -eq 0 ; then
|
|
echo "INFO: All checks passed; pulseaudio-tcp status is okay." >&2
|
|
fi
|
|
|
|
return "$rv"
|
|
}
|
|
|
|
# Acquire PulseAudio cookie from remote host
|
|
sync_pa_cookie() {
|
|
if scp -q "$remote_user"@"$remote_ip":.config/pulse/cookie ~/.config/pulse/cookie ; then
|
|
echo "INFO: Synced PulseAudio cookie from remote_ip=$remote_ip." >&2
|
|
return 0
|
|
else
|
|
echo "ERROR: Unable to sync PulseAudio cookie from remote_ip=$remote_ip." >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Establish SSH port forwarding to PulseAudio TCP server on remote host
|
|
establish_ssh_portforward() {
|
|
_ssh -fNT -L 127.0.0.1:4713:127.0.0.1:4713
|
|
}
|
|
|
|
# Enable PulseAudio TCP tunnel server on remote host
|
|
enable_remote_pa_tunnel_server() {
|
|
if _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then
|
|
echo "INFO: PulseAudio module \"module-native-protocol-tcp\" already loaded on remote_ip=$remote_ip." >&2
|
|
return 0
|
|
elif _ssh pactl load-module module-native-protocol-tcp listen=127.0.0.1 auth-ip-acl=127.0.0.1 ; then
|
|
echo "INFO: Loaded PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2
|
|
return 0
|
|
else
|
|
echo "ERROR: Unable to load PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Enable tunnel sink on local host
|
|
enable_local_pa_tunnel_sink() {
|
|
if pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then
|
|
echo "INFO: PulseAudio module \"module-tunnel-sink\" already loaded." >&2
|
|
return 0
|
|
elif pactl load-module module-tunnel-sink server=tcp:127.0.0.1 ; then
|
|
echo "INFO: Loaded PulseAudio module \"module-tunnel-sink\"." >&2
|
|
return 0
|
|
else
|
|
echo "ERROR: Unable to load PulseAudio module \"module-tunnel-sink\"." >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Enable tunnel source on local host
|
|
enable_local_pa_tunnel_source() {
|
|
if pactl list modules | grep -Fq "Name: module-tunnel-source" ; then
|
|
echo "INFO: PulseAudio module \"module-tunnel-source\" already loaded." >&2
|
|
return 0
|
|
elif pactl load-module module-tunnel-source server=tcp:127.0.0.1 ; then
|
|
echo "INFO: Loaded PulseAudio module \"module-tunnel-source\"." >&2
|
|
return 0
|
|
else
|
|
echo "ERROR: Unable to load PulseAudio module \"module-tunnel-source\"." >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Set tunnel sink as default sink on local host
|
|
set_local_pa_tunnel_sink_as_default() {
|
|
if pactl set-default-sink tunnel-sink.tcp:127.0.0.1 ; then
|
|
echo "INFO: Set \"tunnel-sink.tcp:127.0.0.1\" as default PulseAudio sink." >&2
|
|
return 0
|
|
else
|
|
echo "ERROR: Failed to set \"tunnel-sink.tcp:127.0.0.1\" as default PulseAudio sink." >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Perform start operation
|
|
do_start() {
|
|
sync_pa_cookie || return 1
|
|
establish_ssh_portforward || return 1
|
|
enable_remote_pa_tunnel_server || return 1
|
|
|
|
if "$outbound" ; then
|
|
enable_local_pa_tunnel_sink || return 1
|
|
set_local_pa_tunnel_sink_as_default || return 1
|
|
fi
|
|
|
|
if "$inbound" ; then
|
|
enable_local_pa_tunnel_source || return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Remove PulseAudio TCP tunnel sink on local host
|
|
remove_local_pa_tunnel_sink() {
|
|
if ! pactl list modules | grep -Fq "Name: module-tunnel-sink" ; then
|
|
echo "INFO: PulseAudio module \"module-tunnel-sink\" is not loaded." >&2
|
|
return 0
|
|
elif ! pactl list sinks | grep -Fq "tunnel-sink.tcp:127.0.0.1" ; then
|
|
echo "INFO: No PulseAudio tunnel sink to 127.0.0.1 exists." >&2
|
|
return 0
|
|
else
|
|
owner_module=$(
|
|
pactl --format json list sinks 2>/dev/null | \
|
|
jq '.[] | select(.name=="tunnel-sink.tcp:127.0.0.1") | .owner_module' -r
|
|
)
|
|
|
|
if ! pactl unload-module "$owner_module" ; then
|
|
echo "ERROR: Could not unload owner module $owner_module of PulseAudio sink \"tunnel-sink.tcp:127.0.0.1\"." >&2
|
|
return 1
|
|
else
|
|
echo "INFO: Unloaded owner module $owner_module of PulseAudio sink \"tunnel-sink.tcp:127.0.0.1\"." >&2
|
|
return 0
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Remove PulseAudio TCP tunnel source on local host
|
|
remove_local_pa_tunnel_source() {
|
|
if ! pactl list modules | grep -Fq "Name: module-tunnel-source" ; then
|
|
echo "INFO: PulseAudio module \"module-tunnel-source\" is not loaded." >&2
|
|
return 0
|
|
elif ! pactl list sources | grep -Fq "tunnel-source.tcp:127.0.0.1" ; then
|
|
echo "INFO: No PulseAudio tunnel source from 127.0.0.1 exists." >&2
|
|
return 0
|
|
else
|
|
owner_module=$(
|
|
pactl --format json list sources 2>/dev/null | \
|
|
jq '.[] | select(.name=="tunnel-source.tcp:127.0.0.1") | .owner_module' -r
|
|
)
|
|
|
|
if ! pactl unload-module "$owner_module" ; then
|
|
echo "ERROR: Could not unload owner module $owner_module of PulseAudio source \"tunnel-source.tcp:127.0.0.1\"." >&2
|
|
return 1
|
|
else
|
|
echo "INFO: Unloaded owner module $owner_module of PulseAudio source \"tunnel-source.tcp:127.0.0.1\"." >&2
|
|
return 0
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# Stop PulseAudio TCP tunnel server on remote host.
|
|
disable_remote_pa_tunnel_server() {
|
|
if ! _ssh pactl list modules | grep -Fq "Name: module-native-protocol-tcp" ; then
|
|
echo "INFO: PulseAudio module \"module-native-protocol-tcp\" not loaded on remote_ip=$remote_ip." >&2
|
|
return 0
|
|
elif ! _ssh pactl unload-module module-native-protocol-tcp ; then
|
|
echo "ERROR: Could not unload PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2
|
|
return 1
|
|
else
|
|
echo "INFO: Unloaded PulseAudio module \"module-native-protocol-tcp\" on remote_ip=$remote_ip." >&2
|
|
return 0
|
|
fi
|
|
}
|
|
|
|
# Terminate SSH portforwarding session
|
|
terminate_ssh_portforward() {
|
|
for pid in $(pidof ssh) ; do
|
|
if grep -Fq $USER-pulseaudio /proc/"$pid"/cmdline ; then
|
|
kill -TERM "$pid"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Perform stop operation
|
|
do_stop() {
|
|
if "$outbound" ; then
|
|
remove_local_pa_tunnel_sink || return 1
|
|
fi
|
|
|
|
if "$inbound" ; then
|
|
remove_local_pa_tunnel_source || return 1
|
|
fi
|
|
|
|
disable_remote_pa_tunnel_server || return 1
|
|
terminate_ssh_portforward || return 1
|
|
|
|
return 0
|
|
}
|
|
|
|
##
|
|
# Main Program
|
|
|
|
rv=0
|
|
|
|
if test "$operation" = setup ; then
|
|
echo "Entering setup mode ..."
|
|
else
|
|
if ! test -e "$config" ; then
|
|
echo "ERROR: Configfile $config does not exist (use \"$0 setup\" first)." >&2
|
|
rv=1
|
|
elif ! test -r "$config" ; then
|
|
echo "ERROR: Configfile $config is not readable." >&2
|
|
rv=1
|
|
elif ! test -f "$config" ; then
|
|
echo "ERROR: Configfile $config is not a regular file." >&2
|
|
rv=1
|
|
else
|
|
. "$config"
|
|
|
|
if test -z "$remote_ip" ; then
|
|
echo "ERROR: \"remote_ip=<IP address>\" not set in configfile $config." >&2
|
|
rv=1
|
|
elif test -z "$remote_user" ; then
|
|
echo "ERROR: \"remote_user=<username>\" not set in configfile $config." >&2
|
|
rv=1
|
|
fi
|
|
fi
|
|
|
|
if test -z "$(which jq)" ; then
|
|
echo "ERROR: Required executable \"jq\" not found." >&2
|
|
rv=1
|
|
elif test -z "$(which pactl)" ; then
|
|
echo "ERROR: Required executable \"pactl\" not found." >&2
|
|
rv=1
|
|
elif test -z "$(which ssh)" ; then
|
|
echo "ERROR: Required executable \"ssh\" not found." >&2
|
|
rv=1
|
|
fi
|
|
fi
|
|
|
|
if test "$rv" -ne 0 ; then
|
|
echo "ERROR: Preliminary checks failed, skipping operation." >&2
|
|
else
|
|
case "$operation" in
|
|
""|-h|--help)
|
|
cat << EOF
|
|
Setup and run encrypted connection to remote PulseAudio/Pipewire server
|
|
Usage: $0 restart|setup|start|status|stop
|
|
EOF
|
|
rv=0
|
|
;;
|
|
setup)
|
|
do_setup
|
|
rv=$?
|
|
;;
|
|
start)
|
|
do_start
|
|
rv=$?
|
|
;;
|
|
stop)
|
|
do_stop
|
|
rv=$?
|
|
;;
|
|
restart)
|
|
do_stop
|
|
do_start
|
|
rv=$?
|
|
;;
|
|
status)
|
|
do_status
|
|
rv=$?
|
|
;;
|
|
*)
|
|
echo "ERROR: Usage: $0 restart|setup|start|status|stop" >&2
|
|
rv=1
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
exit "$rv"
|
|
|