Instantly share code, notes, and snippets.
Last active
October 28, 2024 23:16
-
Star
(1)
1
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# sudo systemctl edit plexmediaserver | |
[Unit] | |
AssertPathIsMountPoint=/Volumes/TankSecure | |
[Service] | |
User=REDACTED | |
Group=REDACTED | |
Environment="PLEX_MEDIA_SERVER_APPLICATION_SUPPORT_DIR=/Volumes/REDACTED" | |
UMask=0007 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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