Skip to content

Instantly share code, notes, and snippets.

@balupton
Last active October 28, 2024 23:16
Show Gist options
  • Save balupton/3ed5e6ab791e0234ac038d884c51332f to your computer and use it in GitHub Desktop.
Save balupton/3ed5e6ab791e0234ac038d884c51332f to your computer and use it in GitHub Desktop.
Dorothy Command for managing a Sharebox: BTRFS Cluster, GoCryptFS, Samba, Plex Media Server, Syncthing, NordVPN, Transmission
# sudo systemctl edit plexmediaserver
[Unit]
AssertPathIsMountPoint=/Volumes/TankSecure
[Service]
User=REDACTED
Group=REDACTED
Environment="PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR=/Volumes/REDACTED"
UMask=0007
#!/usr/bin/env bash
# https://flood-api.netlify.app/
function sharebox() (
source "$DOROTHY/sources/strict.bash"
# =====================================
# Configuration
SHAREBOX_USER='REDACTED'
SHAREBOX_GROUP='REDACTED'
SHAREBOX_DRIVE_LABEL='REDACTED' # btrfs label of the cluster
SHAREBOX_DRIVE_COUNT='REDACTED' # count of the btrfs drives
SHAREBOX_DRIVE_MOUNT='/Volumes/REDACTED'
SHAREBOX_CIPHER="/Volumes/REDACTED/REDACTEDCIPHER"
SHAREBOX_PLAIN="/Volumes/REDACTED"
SHAREBOX_SYNCTHING_HOME="$SHAREBOX_PLAIN/Syncthing"
SHAREBOX_TRANSMISSION_HOME="$SHAREBOX_PLAIN/Transmission"
SHAREBOX_PLEX_HOME="$SHAREBOX_PLAIN/Plex Media Server"
SHAREBOX_NORDVPN_TOKEN='REDACTED'
SHAREBOX_DNS='REDACTED' # 1.1.1.1 or 192.168.5.20 or whatever
# =====================================
# Verify
# owner configuration
function check_owner_configured {
test -n "$SHAREBOX_USER" -a -n "$SHAREBOX_GROUP"
}
function require_owner_configured {
if ! check_owner_configured; then
echo-error "User/Group not configured, run:"
echo-style --code="$0 --configure"
return 1
fi
}
function check_owner_setup {
check_owner_configured && is-user "$SHAREBOX_USER" && is-group "$SHAREBOX_GROUP"
}
function require_owner_setup {
if ! check_owner_setup; then
echo-error "User/Group not setup, run:"
echo-style --code="$0 owner"
return 1
fi
}
function get_owner {
echo "$(uid "$SHAREBOX_USER"):$(gid "$SHAREBOX_GROUP")"
}
# drive configuration
function check_drive_configured {
test -n "$SHAREBOX_DRIVE_LABEL" -a -n "$SHAREBOX_DRIVE_COUNT" -a -n "$SHAREBOX_DRIVE_MOUNT"
}
function require_drive_configured {
if ! check_drive_configured; then
echo-error "Drive not configured, run:"
echo-style --code="$0 --configure"
return 1
fi
}
function check_drive_mounted {
test -d "$SHAREBOX_DRIVE_MOUNT" && is-mounted --source="$(get_drive)" --target="$SHAREBOX_DRIVE_MOUNT"
}
function require_drive_mounted {
if ! check_drive_mounted; then
echo-error "Drive not mounted, run:"
echo-style --code="$0 mount"
return 1
fi
}
function get_drive {
# verify drive
# /dev/* locations can change, or be missing, so check for that
btrfs-helper verify -- "$SHAREBOX_DRIVE_LABEL" "$SHAREBOX_DRIVE_COUNT"
# get/output the drive for the label
btrfs-helper drive -- "$SHAREBOX_DRIVE_LABEL"
}
# vault configuration
function check_vault_configured {
test -n "$SHAREBOX_CIPHER" -a -n "$SHAREBOX_PLAIN"
}
function require_vault_configured {
if ! check_vault_configured; then
echo-error "Vault not configured, run:"
echo-style --code="$0 --configure"
return 1
fi
}
function check_vault_mounted {
test -d "$SHAREBOX_PLAIN" && is-mounted --source="$SHAREBOX_CIPHER" --target="$SHAREBOX_PLAIN"
}
function require_vault_mounted {
if ! check_vault_mounted; then
echo-error "Vault not mounted, run:"
echo-style --code="$0 mount"
return 1
fi
}
# =====================================
# Actions
# prepare
services=(
'nordvpnd.socket'
'nordvpnd'
'smbd'
"syncthing@$(whoami)"
'plexmediaserver'
'transmission-daemon'
)
services_enable=(
'nordvpnd.socket'
'nordvpnd'
)
services_disable=(
'smbd'
"syncthing@$(whoami)"
'plexmediaserver'
'transmission-daemon'
)
services_start=(
'nordvpnd.socket'
'nordvpnd'
'smbd'
"syncthing@$(whoami)"
'plexmediaserver'
'transmission-daemon'
)
services_vpn_dependent=(
'transmission-daemon'
)
services_stop=(
'transmission-daemon'
'plexmediaserver'
"syncthing@$(whoami)"
'smbd'
)
utilities=(
'gocryptfs'
'samba'
'plex'
'syncthing'
'nordvpn'
'transmission'
)
users=(
"$(whoami)"
'root'
'plex'
'docker'
'transmission'
'debian-transmission'
"$SHAREBOX_USER"
)
groups=(
'nordvpn'
'docker'
'plex'
'transmission'
'debian-transmission'
"$SHAREBOX_GROUP"
)
# utilities
function owner {
require_owner_configured
# create user if necessary
if ! is-user "$SHAREBOX_USER"; then
# create user
sudo-helper -- useradd "$SHAREBOX_USER" || :
fi
# ensure user is only a share user, rather than a login user
sudo-helper -- usermod -L "$SHAREBOX_USER"
# set passwords?
if confirm --negative --ppid=$$ -- "Configure $SHAREBOX_USER password?"; then
sudo-helper -- passwd "$SHAREBOX_USER"
if command-exists smbpasswd; then
sudo-helper -- smbpasswd -a "$SHAREBOX_USER"
fi
fi
# create group if necessary
if ! is-group "$SHAREBOX_GROUP"; then
# create group
sudo-helper -- groupadd "$SHAREBOX_GROUP" || :
fi
# add users to groups
local user group me reload='no'
me="$(whoami)"
for user in "${users[@]}"; do
if ! is-user "$user"; then
continue
fi
for group in "${groups[@]}"; do
if ! is-group "$group"; then
continue
fi
if is-user-in-group --user="$user" --group="$group"; then
echo-style --dim="user [$user] is already inside group [$group]"
continue
fi
if test "$user" = "$me"; then
reload='yes'
fi
sudo-helper -- gpasswd -a "$user" "$group"
done
done
# check if reload is necessary
if test "$reload" = 'yes'; then
cat <<-EOF
$(echo-style --success="The current user [$me] has been added to new groups.")
$(echo-style --notice="You must logout or reboot for the change to apply.")
EOF
fi
}
function config {
echo-file --plain -- \
/etc/systemd/journald.conf \
/etc/log2ram.conf \
/etc/logrotate.conf \
/etc/samba/smb.conf \
/etc/systemd/system/**/override.conf
}
function configure {
# configure
# local item
# for item in "${services[@]}"; do
# sudo systemctl edit "$item"
# done
# samba
#sudo nano '/etc/samba/smb.conf'
#testparm --suppress-prompt # test samba config
# settings
sudo nano "$SHAREBOX_TRANSMISSION_HOME/settings.json"
sudo nano "$SHAREBOX_SYNCTHING_HOME/config.xml"
sudo nano "$SHAREBOX_PLEX_HOME/Preferences.xml"
# reload
sudo-helper -- systemctl daemon-reload
}
function permissions {
local perms='a-xwr,ug+Xrw'
# transmission
local transmission_user="$SHAREBOX_USER" transmission_group="$SHAREBOX_GROUP"
fs-own --optional --permissions="$perms" --user="$transmission_user" --group="$transmission_group" \
-- '/var/lib/transmission-daemon' "$SHAREBOX_TRANSMISSION_HOME"
sudo truncate -s0 "$SHAREBOX_TRANSMISSION_HOME/transmission.log" # clear the log as it goes on forever
# syncthing
local syncthing_user="$SHAREBOX_USER" syncthing_group="$SHAREBOX_GROUP"
fs-own --optional --permissions="$perms" --user="$syncthing_user" --group="$syncthing_group" \
-- "$HOME/Sync" "$XDG_CONFIG_HOME/syncthing" "$SHAREBOX_SYNCTHING_HOME"
# plex
local plex_user="$SHAREBOX_USER" plex_group="$SHAREBOX_GROUP"
fs-own --optional --sudo --permissions="$perms" --user="$plex_user" --group="$plex_group" \
-- '/var/lib/plexmediaserver' "$SHAREBOX_PLEX_HOME"
}
function setup {
local utility
for utility in "${utilities[@]}"; do
setup-util-"$utility"
done
service-helper --disable -- "${services_disable[@]}"
service-helper --enable -- "${services_enable[@]}"
owner
}
function disconnect {
# shutdown and disable nord
if command-exists nordvpn; then
# stop vpn dependent
service-helper --stop --ignore-missing -- "${services_vpn_dependent[@]}"
# nordvpn commands need the nord service running
service-helper --enable --start -- 'nordvpnd.socket' 'nordvpnd' || :
nordvpn set autoconnect off || :
nordvpn set killswitch off || :
nordvpn set firewall off || :
nordvpn disconnect || :
fi
}
function connect {
disconnect
# firewall + vpn: reset
# https://docs.syncthing.net/users/firewall.html#uncomplicated-firewall-ufw
# https://gist.github.com/nmaggioni/45dcca7695d37e6109276b1a6ad8c9c9#file-ufw_plex-md
# https://support.plex.tv/articles/201543147-what-network-ports-do-i-need-to-allow-through-my-firewall/
if confirm --negative --ppid="$$" -- 'Reset UFW rules?'; then
# reset firewall without being booted from SSH
# -F wipes rules, -X wipes chains
# bash -c workaround somehow required to avoid being booted from SSH
# sudo bash -c "ufw --force reset && iptables -F && iptables -X && ip6tables -F && ip6tables -X && ufw allow from 192.168.0.0/16 to any port 22 && ufw --force enable"
sudo ufw --force disable
# sudo iptables -P INPUT ACCEPT
# sudo iptables -P FORWARD ACCEPT
# sudo iptables -P OUTPUT ACCEPT
# sudo iptables -t nat -F
# sudo iptables -t mangle -F
# sudo iptables -F
# sudo iptables -X
# sudo ip6tables -P INPUT ACCEPT
# sudo ip6tables -P FORWARD ACCEPT
# sudo ip6tables -P OUTPUT ACCEPT
# sudo ip6tables -t nat -F
# sudo ip6tables -t mangle -F
# sudo ip6tables -F
# sudo ip6tables -X
sudo ufw --force reset
# ^ this does all the above, and also handles nftables/netfilter (which is the successor of iptables and ip6tables)
# add rules
sudo ufw allow from 192.168.0.0/16 to any port 22 # local ssh
sudo ufw allow from 192.168.0.0/16 to any app samba || : # local samba
sudo ufw allow from 192.168.0.0/16 to any app syncthing || : # local syncthing peers
sudo ufw allow from 192.168.0.0/16 to any app syncthing-gui || : # local syncthing gui
sudo ufw allow from 192.168.0.0/16 to any port 51413 # local transmission peers
sudo ufw allow from 192.168.0.0/16 to any port 9091 # local transmission gui
sudo ufw allow from 192.168.0.0/16 to any port 53 # local dns
sudo ufw allow from 192.168.0.0/16 to any port 22 # local ssh
sudo ufw allow 32400 # remote plex
sudo ufw limit ssh # limit ssh
# disable logging
sudo ufw logging off
# reenable
sudo ufw --force enable
# sudo ufw reload
# these weren't needed
# sudo sysctl net/ipv4/ip_forward=1
# sudo sysctl net/ipv6/conf/default/forwarding=1
# sudo sysctl net/ipv6/conf/all/forwarding=1
eval-helper --no-quiet --wrap \
-- sudo ufw status verbose # verbose shows ports + protocols of apps
eval-helper --no-quiet --wrap \
-- sudo iptables -L -v
fi
# fix dependencies
setup-util-netscript --quiet --uninstall || :
setup-util-nordvpn --quiet || :
# continue with nordvpn
service-helper --enable --start -- 'nordvpnd.socket' 'nordvpnd' || :
if command-exists nordvpn && confirm --negative --ppid="$$" -- 'Reset NordVPN settings?'; then
nordvpn set defaults || :
fi
# nordvpn commands need the nord service running
if command-exists nordvpn; then
# nordvpn login
if ! nordvpn account; then
nordvpn login --token "$SHAREBOX_NORDVPN_TOKEN"
# nordvpn set technology nordlynx
fi
# nordvpn firewall
# both subnets and ports need to be allowed to enable even local access
nordvpn set dns "$SHAREBOX_DNS"
nordvpn whitelist add subnet 192.168.0.0/16 # all local
local nordvpn_port nordvpn_ports=(
# ssh
22
# dns
53
# samba
137
138
139
445
# syncthing
22000
21027
8384
# transmission
51413
9091
# plex
32400
3005
5353
8324
)
for nordvpn_port in "${nordvpn_ports[@]}"; do
nordvpn whitelist add port "$nordvpn_port"
done
# verify that killswitch i spossible
verify_killswitch
# attempt nordvpn connect
nordvpn set autoconnect on P2P
nordvpn connect P2P || {
echo-style --notice="NordVPN failed to connect, here are the logs:"
service-helper --logs -- nordvpnd
nordvpn connect P2P || {
echo-error "NordVPN failed to connect, try rebooting manually."
return 1
}
}
waiter 60
# verify that connetion and killswitch work
verify_connection
fi
# verify
status
}
function verify_killswitch {
# verify firewall, this is curcial for killswitch to work
nordvpn set firewall on || : # or to prevent already enabled errors
if ! nordvpn settings | grep -q 'Firewall: enabled'; then
echo-style --error="Firewall failed to enable. Reboot your machine."
return 2
fi
# verify killswitch
nordvpn set killswitch on || : # or to prevent already enabled errors
if ! nordvpn settings | grep -q 'Kill Switch: enabled'; then
echo-style --error="Killswitch failed to enable. Reboot your machine."
return 2
fi
}
function verify_connection {
verify_killswitch
# verify connection
local vpn_ip vpn_ip3 ip ip3
if nordvpn status | grep -q 'Status: Connected'; then
vpn_ip="$(nordvpn status | rg -o 'Server IP: (.+)' --replace '$1')"
vpn_ip3="$(echo "$vpn_ip" | rg -o '^[0-9]+[.][0-9]+[.][0-9]+[.]')"
ip="$(what-is-my-ip remote)"
ip3="$(echo "$ip" | rg -o '^[0-9]+[.][0-9]+[.][0-9]+[.]')"
if test "$vpn_ip3" = "$ip3"; then
echo-style --success="Successfully connected [$ip] to NordVPN [$vpn_ip]."
else
echo-style --error="Not connected [$ip] to NordVPN [$vpn_ip]."
return 1
fi
else
return 1
fi
verify_killswitch
}
function check {
local ec=0
verify_connection || ec="$?"
if test "$ec" -eq 2; then
# killswitch failed, we must reboot
sharebox stop
echo-style --notice="Failed to activate killswitch. Stopped all services. Ready for reboot:"
echo-style --code="sudo reboot"
return 1
elif test "$ec" -ne 0; then
# failed to connect, try again
service-helper --stop -- "${services_vpn_dependent[@]}"
if connect; then
echo-style --success="Reconnected to NordVPN."
if is-mounted --source="$SHAREBOX_CIPHER" --target="$SHAREBOX_PLAIN"; then
echo-style --notice="Restarting dependent services:"
service-helper --start -- "${services_vpn_dependent[@]}"
echo-style --success="Restarted dependent services."
else
echo-style --notice="Requirements not met to restart dependent services. Start the sharebox."
fi
else
echo-style --error="Failed to connect to NordVPN."
echo-style --notice="As dependent services have already stopped, disabling NordVPN."
disconnect
echo-style --notice="You will have to figure out why NordVPN failed."
fi
fi
}
function status {
# eval-helper --no-quiet --wrap \
# -- sudo ufw status verbose
# eval-helper --no-quiet --wrap \
# -- sudo iptables -L -v
eval-helper --no-quiet --wrap \
-- cat /etc/resolv.conf
eval-helper --no-quiet --wrap \
-- resolvectl status
eval-helper --no-quiet --wrap \
-- resolvectl dns
eval-helper --no-quiet --wrap \
-- nslookup cloudflare.com
eval-helper --no-quiet --wrap \
-- nordvpn status
eval-helper --no-quiet --wrap \
-- fetch 'http://ipecho.net/plain'
eval-helper --no-quiet --wrap \
-- fetch 'https://test.nextdns.io'
eval-helper --no-quiet --wrap \
-- fetch "https://nordvpn.com/wp-admin/admin-ajax.php?action=get_user_info_data&ip=$(what-is-my-ip remote)"
}
function mount {
# check configured
require_owner_setup
require_drive_configured
require_vault_configured
# fetch setup
local owner drive
owner="$(get_owner)"
drive="$(get_drive)"
# drive
eval-helper --no-quiet --wrap --shapeshifter \
-- fs-mount \
--source="$drive" \
--target="$SHAREBOX_DRIVE_MOUNT" \
--owner="$owner" --user="$SHAREBOX_USER" --group="$SHAREBOX_GROUP"
# vault
eval-helper --no-quiet --wrap --shapeshifter \
-- fs-mount \
--source="$SHAREBOX_CIPHER" \
--target="$SHAREBOX_PLAIN" \
--owner="$owner" --user="$SHAREBOX_USER" --group="$SHAREBOX_GROUP"
}
function unmount {
require_drive_configured
require_vault_configured
# vault
eval-helper --no-quiet --wrap --shapeshifter \
-- fs-unmount -- "$SHAREBOX_PLAIN"
# drive
eval-helper --no-quiet --wrap --shapeshifter \
-- fs-unmount -- "$SHAREBOX_DRIVE_MOUNT"
}
function start {
check # also connects
unmount # required as fs-mount is not intelligent enough to know when to keep mounts, and nested mounts
mount
if confirm --negative --ppid="$$" -- 'Configure?'; then
configure
fi
permissions
service-helper --start --status --logs -- "${services_start[@]}"
}
function stop {
service-helper --stop --status --logs -- "${services_stop[@]}"
unmount
disconnect
}
# sudo-helper -- shutdown --reboot
# sudo-helper -- shutdown --poweroff
# # adapt firewall
# if command-exists firewall-cmd; then
# sudo-helper -- firewall-cmd --get-active-zones
# sudo-helper -- firewall-cmd --permanent --zone=FedoraWorkstation --add-service=samba
# sudo-helper -- firewall-cmd --reload
# fi
# =====================================
# Action
# prepare
local actions
actions=(
'config'
'configure'
'connect'
'status'
'disconnect'
'check'
'mount'
'owner'
'permissions'
'setup'
'start'
'stop'
'unmount'
)
function help {
cat <<-EOF >/dev/stderr
USAGE:
sharebox <action>
ACTIONS:
$(echo-lines -- "${actions[@]}")
EOF
if test "$#" -ne 0; then
echo-error "$@"
fi
return 22 # Invalid argument
}
# process
local item action='' args=()
while test "$#" -ne 0; do
item="$1"
shift
case "$item" in
'--help' | '-h') help ;;
'--')
args+=("$@")
shift $#
break
;;
'--'*) help "An unrecognised flag was provided: $item" ;;
*)
if test -z "$action"; then
action="$item"
else
help "An unrecognised argument was provided: $item"
fi
;;
esac
done
# ensure action
action="$(
choose-option --required \
--question='What action to perform?' \
--filter="$action" -- "${actions[@]}"
)"
# act
if test "$(type -t "$action")" = 'function'; then
"$action" "${args[@]}"
return "$?"
else
echo-error "$0: Action not yet implemented: $action"
return 78 # Function not implemented
fi
)
# fire if invoked standalone
if test "$0" = "${BASH_SOURCE[0]}"; then
sharebox "$@"
fi
# sudo systemctl edit transmission-daemon
[Unit]
After=nordvpnd.service
BindsTo=nordvpnd.service
AssertPathIsMountPoint=/Volumes/REDACTED
[Service]
User=REDACTED
Group=REDACTED
Environment="TRANSMISSION_HOME=/Volumes/REDACTED/Transmission"
TimeoutSec=600
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment