From 5fbf733a68f43a2ec1da1775f4349768e269a618 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Tue, 6 Jan 2026 17:11:28 +0100 Subject: [PATCH] Refactor SMB mount script for clarity and error handling Refactor SMB mount script to improve readability and error handling. Added cleanup for credential files and enhanced logging for mount operations. --- .templates/00-smb_mounts.sh | 325 ++++++++++++++++++++---------------- 1 file changed, 177 insertions(+), 148 deletions(-) diff --git a/.templates/00-smb_mounts.sh b/.templates/00-smb_mounts.sh index 363e8aacd..9f3295a97 100755 --- a/.templates/00-smb_mounts.sh +++ b/.templates/00-smb_mounts.sh @@ -1,9 +1,10 @@ #!/usr/bin/with-contenv bashio # shellcheck shell=bash -# shellcheck disable= +# shellcheck disable=SC2086,SC2001,SC2015,SC2154 + set -e -if ! bashio::supervisor.ping 2> /dev/null; then +if ! bashio::supervisor.ping 2>/dev/null; then bashio::log.blue "Disabled : please use another method" exit 0 fi @@ -14,65 +15,72 @@ bashio::log.notice "This script is used to mount remote smb/cifs/nfs shares. Ins # DEFINE FUNCTIONS # #################### -test_mount() { +cleanup_cred() { + if [[ -n "${CRED_FILE:-}" && -f "${CRED_FILE:-}" ]]; then + rm -f "$CRED_FILE" || true + fi + CRED_FILE="" +} +test_mount() { # Set initial test MOUNTED=false ERROR_MOUNT=false # Exit if not mounted - if ! mountpoint -q /mnt/"$diskname"; then + if ! mountpoint -q "/mnt/$diskname"; then return 0 fi # Exit if can't write - [[ -e "/mnt/$diskname/testaze" ]] && rm -r "/mnt/$diskname/testaze" - # shellcheck disable=SC2015 - mkdir "/mnt/$diskname/testaze" && touch "/mnt/$diskname/testaze/testaze" && rm -r "/mnt/$diskname/testaze" || ERROR_MOUNT=true + [[ -e "/mnt/$diskname/testaze" ]] && rm -rf "/mnt/$diskname/testaze" + mkdir "/mnt/$diskname/testaze" && touch "/mnt/$diskname/testaze/testaze" && rm -rf "/mnt/$diskname/testaze" || ERROR_MOUNT=true # Only CIFS has the noserverino fallback if [[ "$ERROR_MOUNT" == "true" && "$FSTYPE" == "cifs" ]]; then - # Test write permissions if [[ "$MOUNTOPTIONS" == *"noserverino"* ]]; then bashio::log.fatal "Disk is mounted, however unable to write in the shared disk. Please check UID/GID for permissions, and if the share is rw" else - MOUNTOPTIONS="$MOUNTOPTIONS,noserverino" + MOUNTOPTIONS="${MOUNTOPTIONS},noserverino" echo "... testing with noserverino" mount_drive "$MOUNTOPTIONS" return 0 fi fi + # CRITICAL: for non-CIFS too, do not claim success if mounted but not writable + if [[ "$ERROR_MOUNT" == "true" ]]; then + MOUNTED=false + bashio::log.fatal "Disk is mounted, however unable to write in the shared disk. Please check permissions/export options (rw), and UID/GID mapping." + return 0 + fi + # Set correctly mounted bit MOUNTED=true return 0 - } mount_drive() { - # Define options MOUNTED=true MOUNTOPTIONS="$1" - # Try mounting (type depends on detected FSTYPE) + # Try mounting (type depends on (detected) FSTYPE) if [[ "$FSTYPE" == "cifs" ]]; then - mount -t cifs -o "$MOUNTOPTIONS" "$disk" /mnt/"$diskname" 2> ERRORCODE || MOUNTED=false + mount -t cifs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2>"$ERRORCODE_FILE" || MOUNTED=false elif [[ "$FSTYPE" == "nfs" ]]; then - mount -t nfs -o "$MOUNTOPTIONS" "$disk" /mnt/"$diskname" 2> ERRORCODE || MOUNTED=false + mount -t nfs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2>"$ERRORCODE_FILE" || MOUNTED=false fi # Test if successful if [[ "$MOUNTED" == "true" ]]; then - # shellcheck disable=SC2015 test_mount fi - } -#################### +######################## # MOUNT NETWORK SHARES # -#################### +######################## if bashio::config.has_value 'networkdisks'; then @@ -83,16 +91,16 @@ if bashio::config.has_value 'networkdisks'; then bashio::log.warning "------------------------" fi - echo 'Mounting network share(s)...' + echo "Mounting network share(s)..." #################### # Define variables # #################### - # Set variables - MOREDISKS=$(bashio::config 'networkdisks') - USERNAME=$(bashio::config 'cifsusername') - PASSWORD=$(bashio::config 'cifspassword') + MOREDISKS="$(bashio::config 'networkdisks')" + USERNAME="$(bashio::config 'cifsusername')" + PASSWORD="$(bashio::config 'cifspassword')" + SMBVERS="" SECVERS="" CHARSET=",iocharset=utf8" @@ -103,15 +111,15 @@ if bashio::config.has_value 'networkdisks'; then MOREDISKS=${MOREDISKS// /"\040"} # Is domain set (CIFS only) - DOMAIN="" DOMAINCLIENT="" + CIFSDOMAIN="" if bashio::config.has_value 'cifsdomain'; then - echo "... using domain $(bashio::config 'cifsdomain')" - DOMAIN=",domain=$(bashio::config 'cifsdomain')" - DOMAINCLIENT="--workgroup=$(bashio::config 'cifsdomain')" + CIFSDOMAIN="$(bashio::config 'cifsdomain')" + echo "... using domain $CIFSDOMAIN" + DOMAINCLIENT="--workgroup=$CIFSDOMAIN" fi - # Is UID/GID set (used for CIFS mount options) + # UID/GID (used for CIFS mount options) PUID=",uid=$(id -u)" PGID=",gid=$(id -g)" if bashio::config.has_value 'PUID' && bashio::config.has_value 'PGID'; then @@ -124,15 +132,15 @@ if bashio::config.has_value 'networkdisks'; then # Mounting disks # ################## - # shellcheck disable=SC2086 - for disk in ${MOREDISKS//,/ }; do # Separate comma separated values + for disk in ${MOREDISKS//,/ }; do + CRED_FILE="" + cleanup_cred # Clean name of network share - # shellcheck disable=SC2116,SC2001 - disk=$(echo $disk | sed "s,/$,,") # Remove / at end of name - disk="${disk//"\040"/ }" # replace \040 with space + disk="$(echo "$disk" | sed "s,/$,,")" # Remove trailing / + disk="${disk//"\040"/ }" # replace \040 with space - # Detect filesystem type by pattern (CIFS: //ip/share ; NFS: ip:/export[/path] or nfs://ip:/export[/path]) + # Detect filesystem type by pattern FSTYPE="cifs" if [[ "$disk" =~ ^nfs:// ]]; then FSTYPE="nfs" @@ -143,119 +151,139 @@ if bashio::config.has_value 'networkdisks'; then # Determine server for reachability checks if [[ "$FSTYPE" == "cifs" ]]; then - server="$(echo "$disk" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+")" + server="$(echo "$disk" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | head -n 1)" else server="${disk%%:*}" fi diskname="$disk" diskname="${diskname//\\//}" # replace \ with / - diskname="${diskname##*/}" # Get only last part of the name + diskname="${diskname##*/}" # keep only last part of the name + + # CRITICAL: per-disk error file (avoid collisions / missing file reads) + ERRORCODE_FILE="/tmp/mount_error_${diskname//[^a-zA-Z0-9._-]/_}.log" + : >"$ERRORCODE_FILE" || true + MOUNTED=false SMBVERS_FORCE="" SECVERS_FORCE="" + SMBVERS="" + SECVERS="" - # Start echo "... mounting ($FSTYPE) $disk" # Data validation if [[ "$FSTYPE" == "cifs" ]]; then - if [[ ! "$disk" =~ ^.*+[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+[/]+.*+$ ]]; then - bashio::log.fatal "...... the structure of your \"networkdisks\" option : \"$disk\" doesn't seem correct, please use a structure like //123.12.12.12/sharedfolder,//123.12.12.12/sharedfolder2. If you don't use it, you can simply remove the text, this will avoid this error message in the future." - touch ERRORCODE + if [[ ! "$disk" =~ ^//[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/.+ ]]; then + bashio::log.fatal "...... the structure of your \"networkdisks\" option : \"$disk\" doesn't seem correct, please use a structure like //123.12.12.12/sharedfolder,//123.12.12.12/sharedfolder2." + echo "Invalid CIFS path structure: $disk" >"$ERRORCODE_FILE" || true continue fi else if [[ ! "$disk" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:/.+ ]]; then bashio::log.fatal "...... invalid NFS path \"$disk\". Use a structure like 123.12.12.12:/export/path" - touch ERRORCODE + echo "Invalid NFS path structure: $disk" >"$ERRORCODE_FILE" || true continue fi fi # Prepare mount point - mkdir -p /mnt/"$diskname" - chown root:root /mnt/"$diskname" + mkdir -p "/mnt/$diskname" + chown root:root "/mnt/$diskname" + + # Create credentials file only for CIFS (avoids comma/special-char issues in -o) + if [[ "$FSTYPE" == "cifs" ]]; then + CRED_FILE="$(mktemp /tmp/cifs-cred.XXXXXX)" + chmod 600 "$CRED_FILE" + { + printf 'username=%s\n' "$USERNAME" + printf 'password=%s\n' "$PASSWORD" + if [[ -n "${CIFSDOMAIN:-}" ]]; then + printf 'domain=%s\n' "$CIFSDOMAIN" + fi + } >"$CRED_FILE" + fi # Quickly try to mount with defaults if [[ "$FSTYPE" == "cifs" ]]; then - mount_drive "rw,file_mode=0775,dir_mode=0775,username=${USERNAME},password=${PASSWORD},nobrl,mfsymlinks${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}${DOMAIN}" - elif [[ "$FSTYPE" == "nfs" ]]; then + mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}" + else mount_drive "rw,nfsvers=4.2,proto=tcp,hard,timeo=600,retrans=2" fi # Deeper analysis if failed - if [ "$MOUNTED" = false ]; then + if [[ "$MOUNTED" == "false" ]]; then if [[ "$FSTYPE" == "cifs" ]]; then - # Does server exist (SMB port 445) - output="$(nmap -F $server -T5 -oG -)" - if ! echo "$output" | grep 445/open &> /dev/null; then - if echo "$output" | grep /open &> /dev/null; then - bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script" - touch ERRORCODE + if command -v nmap >/dev/null 2>&1; then + output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)" + if ! echo "$output" | grep -q "445/open"; then + if echo "$output" | grep -q "/open"; then + bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script" + else + bashio::log.fatal "...... fatal : $server not reachable, is it correct" + fi + cleanup_cred continue else - bashio::log.fatal "...... fatal : $server not reachable, is it correct" - touch ERRORCODE + echo "...... $server is confirmed reachable" + fi + else + bashio::log.warning "...... nmap not available; skipping SMB port reachability test" + fi + + # Are credentials correct (use server, not share path) + if command -v smbclient >/dev/null 2>&1; then + OUTPUT="$(smbclient -t 2 -L "$server" -U "$USERNAME"%"$PASSWORD" -c "exit" $DOMAINCLIENT 2>&1 || true)" + + if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then + bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop." + if ! smbclient -t 2 -L "$server" -N $DOMAINCLIENT -c "exit" &>/dev/null; then + bashio::log.fatal "...... perhaps a workgroup must be specified" + fi + cleanup_cred continue + elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then + echo "... using SMBv1" + bashio::log.warning "...... share reachable only with legacy SMBv1 (NT1) negotiation. Forcing SMBv1 options." + SMBVERS_FORCE=",vers=1.0" + SECVERS_FORCE=",sec=ntlm" + elif ! echo "$OUTPUT" | grep -q "Disk"; then + echo "... testing path" + bashio::log.fatal "...... no shares found. Invalid or inaccessible SMB path?" + else + echo "...... credentials are valid" fi else - echo "...... $server is confirmed reachable" + bashio::log.warning "...... smbclient not available; skipping SMB credential test" fi - # Are credentials correct - OUTPUT="$(smbclient -t 2 -L "$disk" -U "$USERNAME"%"$PASSWORD" -c "exit" $DOMAINCLIENT 2>&1 || true)" - if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then - bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop." - touch ERRORCODE - # Should there be a workgroup - if ! smbclient -t 2 -L $disk -N $DOMAINCLIENT -c "exit" &> /dev/null; then - bashio::log.fatal "...... perhaps a workgroup must be specified" - touch ERRORCODE - fi - continue - elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then - echo "... using SMBv1" - bashio::log.warning "...... share reachable only with legacy SMBv1 (NT1) negotiation. Forcing SMBv1 options." - SMBVERS_FORCE=",vers=1.0" - SECVERS_FORCE=",sec=ntlm" - elif ! echo "$OUTPUT" | grep -q "Disk"; then - echo "... testing path" - bashio::log.fatal "...... no shares found. Invalid or inaccessible SMB path?" - else - echo "...... credentials are valid" + # Extract SMB dialect from nmap and map to mount.cifs vers= + SMBRAW="" + if command -v nmap >/dev/null 2>&1; then + SMBRAW="$( + nmap --script smb-protocols -p 445 "$server" 2>/dev/null \ + | awk '/SMB2_DIALECT_/ {print $NF}' \ + | sed 's/SMB2_DIALECT_//' \ + | tr -d '_' \ + | sort -V | tail -n 1 || true + )" fi - # Extracting SMB versions and normalize output - # shellcheck disable=SC2210,SC2094 - SMBVERS="$(nmap --script smb-protocols "$server" -p 445 2> 1 | awk '/ [0-9]/' | awk '{print $NF}' | cut -c -3 | sort -V | tail -n 1 || true)" - # Avoid : - SMBVERS="${SMBVERS/:/.}" - # Manage output - if [ -n "$SMBVERS" ]; then - case $SMBVERS in - "202" | "200" | "20") - SMBVERS="2.0" - ;; - 21) - SMBVERS="2.1" - ;; - 302) - SMBVERS="3.02" - ;; - 311) - SMBVERS="3.1.1" - ;; - "3.1") - echo "SMB 3.1 detected, converting to 3.0" - SMBVERS="3.0" - ;; - esac - echo "...... SMB version detected : $SMBVERS" - SMBVERS=",vers=$SMBVERS" - elif smbclient -t 2 -L "$server" -m NT1 -N $DOMAINCLIENT &> /dev/null; then + SMBVERS="" + case "$SMBRAW" in + 311) SMBVERS=",vers=3.1.1" ;; + 302) SMBVERS=",vers=3.02" ;; + 300) SMBVERS=",vers=3.0" ;; + 210) SMBVERS=",vers=2.1" ;; + 202|200) SMBVERS=",vers=2.0" ;; + *) SMBVERS="" ;; + esac + + if [[ -n "$SMBVERS" ]]; then + echo "...... SMB version detected : ${SMBVERS#,vers=}" + elif command -v smbclient >/dev/null 2>&1 && smbclient -t 2 -L "$server" -m NT1 -N $DOMAINCLIENT &>/dev/null; then echo "...... SMB version : only SMBv1 is supported, this can lead to issues" SECVERS=",sec=ntlm" SMBVERS=",vers=1.0" @@ -264,94 +292,95 @@ if bashio::config.has_value 'networkdisks'; then SMBVERS="" fi - # Apply forced SMBv1 options when initial connection required NT1 fallback + # Apply forced SMBv1 options when needed if [[ -n "$SMBVERS_FORCE" ]]; then - if [[ -z "$SMBVERS" ]]; then - SMBVERS="$SMBVERS_FORCE" - fi - if [[ -z "$SECVERS" ]]; then - SECVERS="$SECVERS_FORCE" - fi + [[ -z "$SMBVERS" ]] && SMBVERS="$SMBVERS_FORCE" + [[ -z "$SECVERS" ]] && SECVERS="$SECVERS_FORCE" fi - # Ensure the Samba client allows SMBv1 when those options are required + # Ensure Samba client allows SMBv1 when required if [[ "${SMBVERS}${SMBVERS_FORCE}" == *"vers=1.0"* ]]; then if [[ -f /etc/samba/smb.conf ]]; then bashio::log.warning "...... enabling SMBv1 support in Samba client configuration" sed -i '/\[global\]/!b;n;/client min protocol = NT1/!a\ - client min protocol = NT1' /etc/samba/smb.conf + client min protocol = NT1' /etc/samba/smb.conf || true fi fi - # Test with different security versions - ####################################### - for SECVERS in "$SECVERS" ",sec=ntlmv2" ",sec=ntlmssp" ",sec=ntlmsspi" ",sec=krb5i" ",sec=krb5" ",sec=ntlm" ",sec=ntlmv2i"; do - if [ "$MOUNTED" = false ]; then - mount_drive "rw,file_mode=0775,dir_mode=0775,username=${USERNAME},password=${PASSWORD},nobrl${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}${DOMAIN}" + # Try with different security modes (do not overwrite SECVERS base accidentally) + SECVERS_BASE="$SECVERS" + for SECTRY in "$SECVERS_BASE" ",sec=ntlmv2" ",sec=ntlmssp" ",sec=ntlmsspi" ",sec=krb5i" ",sec=krb5" ",sec=ntlm" ",sec=ntlmv2i"; do + if [[ "$MOUNTED" == "false" ]]; then + mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECTRY}${PUID}${PGID}${CHARSET}" fi done - elif [[ "$FSTYPE" == "nfs" ]]; then - # Add NFS-specific port check (2049) similar to SMB (445) - output="$(nmap -F $server -T5 -oG -)" - if ! echo "$output" | grep -E '(2049|111)/open' &> /dev/null; then - bashio::log.fatal "...... $server is reachable but NFS ports not open" - continue + else + # NFS: check ports (111/2049) and try common versions + if command -v nmap >/dev/null 2>&1; then + output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)" + if ! echo "$output" | grep -Eq '(2049|111)/open'; then + bashio::log.fatal "...... $server is reachable but NFS ports not open" + continue + fi + else + bashio::log.warning "...... nmap not available; skipping NFS port reachability test" fi - # NFS fallback attempts: try common versions until one works + for NFVER in 4.2 4.1 4 3; do - if [ "$MOUNTED" = false ]; then + if [[ "$MOUNTED" == "false" ]]; then mount_drive "rw,nfsvers=${NFVER},proto=tcp" fi done fi fi - # Messages - if [ "$MOUNTED" = true ]; then - + # Messages / finalization + if [[ "$MOUNTED" == "true" ]]; then bashio::log.info "...... $disk successfully mounted to /mnt/$diskname with options ${MOUNTOPTIONS/$PASSWORD/XXXXXXXXXX}" - # Remove errorcode - if [ -f ERRORCODE ]; then - rm ERRORCODE - fi + rm -f "$ERRORCODE_FILE" 2>/dev/null || true - # Alert if smbv1 - if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"1.0"* ]]; then + if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"vers=1.0"* ]]; then bashio::log.warning "" - bashio::log.warning "Your smb system requires smbv1. This is an obsolete protocol. Please correct this to prevent issues." + bashio::log.warning "Your SMB system requires SMBv1. This is an obsolete protocol. Please correct this to prevent issues." bashio::log.warning "" fi + cleanup_cred else # Mounting failed messages if [[ "$FSTYPE" == "cifs" ]]; then - bashio::log.fatal "Error, unable to mount $disk to /mnt/$diskname with username $USERNAME, $PASSWORD. Please check your remote share path, username, password, domain, try putting 0 in UID and GID" + bashio::log.fatal "Error, unable to mount $disk to /mnt/$diskname with username $USERNAME. Please check remote share path, username, password, domain; try UID/GID 0." bashio::log.fatal "Here is some debugging info :" - smbclient -t 2 -L $disk -U "$USERNAME%$PASSWORD" -c "exit" + if command -v smbclient >/dev/null 2>&1; then + smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT || true + else + bashio::log.warning "smbclient not available; cannot print SMB debugging info" + fi + + # last-ditch try: minimal options (still uses credentials file) SMBVERS="" SECVERS="" PUID="" PGID="" CHARSET="" - mount_drive "rw,file_mode=0775,dir_mode=0775,username=${USERNAME},password=${PASSWORD},nobrl${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}${DOMAIN}" - elif [[ "$FSTYPE" == "nfs" ]]; then - bashio::log.fatal "Error, unable to mount NFS share $disk to /mnt/$diskname. Please check the export path and that NFS server allows this client (and NFSv4)." - # last-ditch try with very basic options + mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}" + else + bashio::log.fatal "Error, unable to mount NFS share $disk to /mnt/$diskname. Please check the export path and that the NFS server allows this client (and NFSv4)." mount_drive "rw" fi - bashio::log.fatal "Error read : $(< ERRORCODE), addon will stop in 1 min" + ERR_READ="$(cat "$ERRORCODE_FILE" 2>/dev/null || true)" + bashio::log.fatal "Error read : ${ERR_READ:-unknown error}, addon will stop in 1 min" # clean folder - umount "/mnt/$diskname" 2> /dev/null || true - rmdir "/mnt/$diskname" || true + umount "/mnt/$diskname" 2>/dev/null || true + rmdir "/mnt/$diskname" 2>/dev/null || true + cleanup_cred + rm -f "$ERRORCODE_FILE" 2>/dev/null || true # Stop addon bashio::addon.stop - fi - done - fi