#!/bin/sh # SecuBox SMB/CIFS remote mount manager # Copyright (C) 2025-2026 CyberMind.fr CONFIG="smbfs" usage() { cat <<'EOF' Usage: smbfsctl [args] Share Management: add Add a new SMB share remove Remove a share definition enable Enable auto-mount for a share disable Disable auto-mount for a share credentials Set credentials for a share set Set a share option list List all configured shares Mount Operations: mount Mount a specific share mount-all Mount all enabled shares umount Unmount a specific share umount-all Unmount all shares status Show mount status of all shares test Test connectivity to a share Examples: smbfsctl add movies //nas/movies /mnt/smb/movies smbfsctl credentials movies user mypass smbfsctl set movies read_only 1 smbfsctl mount movies smbfsctl enable movies EOF } require_root() { [ "$(id -u)" -eq 0 ] || { echo "[ERROR] Root required" >&2; exit 1; }; } log_info() { echo "[INFO] $*"; } log_warn() { echo "[WARN] $*" >&2; } log_error() { echo "[ERROR] $*" >&2; } uci_get() { uci -q get ${CONFIG}.$1; } uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; } # Load global config load_global() { mount_base="$(uci_get global.mount_base || echo /mnt/smb)" default_vers="$(uci_get global.cifs_version || echo 3.0)" timeout="$(uci_get global.timeout || echo 10)" } # Get list of configured share section names get_shares() { uci -q show "$CONFIG" | grep "=${CONFIG}\[" | sed "s/.*\.\(.*\)=.*/\1/" | sort -u uci -q show "$CONFIG" | grep "=mount" | sed "s/${CONFIG}\.\(.*\)=mount/\1/" | sort -u } # Check if share section exists share_exists() { local type type="$(uci -q get ${CONFIG}.$1)" [ "$type" = "mount" ] } # Build mount options for a share build_mount_opts() { local name="$1" local username password domain vers ro opts username="$(uci_get ${name}.username)" password="$(uci_get ${name}._password)" domain="$(uci_get ${name}.domain)" vers="$(uci_get ${name}.cifs_version || echo "$default_vers")" ro="$(uci_get ${name}.read_only)" opts="vers=${vers}" if [ -n "$username" ] && [ "$username" != "guest" ]; then opts="${opts},username=${username}" [ -n "$password" ] && opts="${opts},password=${password}" [ -n "$domain" ] && opts="${opts},domain=${domain}" else opts="${opts},guest" fi [ "$ro" = "1" ] && opts="${opts},ro" || opts="${opts},rw" # Reasonable defaults for network mounts opts="${opts},iocharset=utf8,noperm,noserverino" echo "$opts" } # Check if a share is currently mounted is_mounted() { local mountpoint="$1" grep -q " ${mountpoint} cifs " /proc/mounts 2>/dev/null } # ============================================================================= # SHARE MANAGEMENT # ============================================================================= cmd_add() { local name="$1" server="$2" mountpoint="$3" [ -z "$name" ] || [ -z "$server" ] || [ -z "$mountpoint" ] && { echo "Usage: smbfsctl add " >&2 exit 1 } if share_exists "$name"; then log_error "Share '$name' already exists" exit 1 fi load_global uci add ${CONFIG} mount >/dev/null # Rename the unnamed section to the given name local idx idx=$(uci -q show ${CONFIG} | grep "=mount$" | tail -1 | sed "s/${CONFIG}\.\(.*\)=mount/\1/") uci rename ${CONFIG}.${idx}="${name}" uci set ${CONFIG}.${name}.enabled='0' uci set ${CONFIG}.${name}.server="$server" uci set ${CONFIG}.${name}.mountpoint="$mountpoint" uci set ${CONFIG}.${name}.username='guest' uci set ${CONFIG}.${name}._password='' uci set ${CONFIG}.${name}.domain='' uci set ${CONFIG}.${name}.cifs_version="$default_vers" uci set ${CONFIG}.${name}.read_only='1' uci set ${CONFIG}.${name}.auto_mount='0' uci set ${CONFIG}.${name}.description='' uci commit ${CONFIG} log_info "Share '$name' added: $server -> $mountpoint" log_info "Set credentials: smbfsctl credentials $name " } cmd_remove() { local name="$1" [ -z "$name" ] && { echo "Usage: smbfsctl remove " >&2; exit 1; } if ! share_exists "$name"; then log_error "Share '$name' not found" exit 1 fi # Unmount first if mounted local mountpoint mountpoint="$(uci_get ${name}.mountpoint)" if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then umount "$mountpoint" 2>/dev/null || umount -l "$mountpoint" 2>/dev/null fi uci delete ${CONFIG}.${name} uci commit ${CONFIG} log_info "Share '$name' removed" } cmd_enable() { local name="$1" [ -z "$name" ] && { echo "Usage: smbfsctl enable " >&2; exit 1; } share_exists "$name" || { log_error "Share '$name' not found"; exit 1; } uci_set ${name}.enabled '1' uci_set ${name}.auto_mount '1' log_info "Share '$name' enabled for auto-mount" } cmd_disable() { local name="$1" [ -z "$name" ] && { echo "Usage: smbfsctl disable " >&2; exit 1; } share_exists "$name" || { log_error "Share '$name' not found"; exit 1; } uci_set ${name}.enabled '0' uci_set ${name}.auto_mount '0' log_info "Share '$name' disabled" } cmd_credentials() { local name="$1" user="$2" pass="$3" [ -z "$name" ] || [ -z "$user" ] && { echo "Usage: smbfsctl credentials " >&2 exit 1 } share_exists "$name" || { log_error "Share '$name' not found"; exit 1; } uci_set ${name}.username "$user" uci_set ${name}._password "$pass" log_info "Credentials set for '$name' (user: $user)" } cmd_set() { local name="$1" key="$2" value="$3" [ -z "$name" ] || [ -z "$key" ] && { echo "Usage: smbfsctl set " >&2 exit 1 } share_exists "$name" || { log_error "Share '$name' not found"; exit 1; } uci_set ${name}.${key} "$value" log_info "Set ${name}.${key} = $value" } cmd_list() { load_global local found=0 local shares shares="$(get_shares)" if [ -z "$shares" ]; then echo "No SMB shares configured." echo "Add one: smbfsctl add //server/share /mnt/smb/name" return fi printf "%-12s %-8s %-28s %-24s %s\n" "NAME" "STATUS" "SERVER" "MOUNTPOINT" "USER" printf "%-12s %-8s %-28s %-24s %s\n" "----" "------" "------" "----------" "----" for name in $shares; do local enabled server mountpoint username status enabled="$(uci_get ${name}.enabled)" server="$(uci_get ${name}.server)" mountpoint="$(uci_get ${name}.mountpoint)" username="$(uci_get ${name}.username)" if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then status="mounted" elif [ "$enabled" = "1" ]; then status="enabled" else status="disabled" fi printf "%-12s %-8s %-28s %-24s %s\n" "$name" "$status" "$server" "$mountpoint" "${username:-guest}" found=1 done } # ============================================================================= # MOUNT OPERATIONS # ============================================================================= cmd_mount() { require_root local name="$1" [ -z "$name" ] && { echo "Usage: smbfsctl mount " >&2; exit 1; } share_exists "$name" || { log_error "Share '$name' not found"; exit 1; } load_global local server mountpoint server="$(uci_get ${name}.server)" mountpoint="$(uci_get ${name}.mountpoint)" [ -z "$server" ] && { log_error "No server configured for '$name'"; exit 1; } [ -z "$mountpoint" ] && { log_error "No mountpoint configured for '$name'"; exit 1; } if is_mounted "$mountpoint"; then log_info "'$name' already mounted at $mountpoint" return 0 fi # Create mountpoint mkdir -p "$mountpoint" local opts opts="$(build_mount_opts "$name")" log_info "Mounting $server -> $mountpoint" if mount -t cifs "$server" "$mountpoint" -o "$opts" 2>&1; then log_info "Mounted '$name' successfully" else log_error "Failed to mount '$name'" return 1 fi } cmd_mount_all() { require_root load_global local shares count=0 fail=0 shares="$(get_shares)" for name in $shares; do local enabled auto_mount enabled="$(uci_get ${name}.enabled)" auto_mount="$(uci_get ${name}.auto_mount)" [ "$enabled" = "1" ] && [ "$auto_mount" = "1" ] || continue if cmd_mount_single "$name"; then count=$((count + 1)) else fail=$((fail + 1)) fi done log_info "Mounted $count share(s), $fail failure(s)" } # Internal: mount a single share (no arg validation) cmd_mount_single() { local name="$1" local server mountpoint server="$(uci_get ${name}.server)" mountpoint="$(uci_get ${name}.mountpoint)" [ -z "$server" ] || [ -z "$mountpoint" ] && return 1 if is_mounted "$mountpoint"; then return 0 fi mkdir -p "$mountpoint" local opts opts="$(build_mount_opts "$name")" mount -t cifs "$server" "$mountpoint" -o "$opts" 2>/dev/null } cmd_umount() { require_root local name="$1" [ -z "$name" ] && { echo "Usage: smbfsctl umount " >&2; exit 1; } share_exists "$name" || { log_error "Share '$name' not found"; exit 1; } local mountpoint mountpoint="$(uci_get ${name}.mountpoint)" if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then umount "$mountpoint" 2>/dev/null || umount -l "$mountpoint" 2>/dev/null log_info "Unmounted '$name' from $mountpoint" else log_info "'$name' is not mounted" fi } cmd_umount_all() { require_root load_global local shares shares="$(get_shares)" for name in $shares; do local mountpoint mountpoint="$(uci_get ${name}.mountpoint)" if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then umount "$mountpoint" 2>/dev/null || umount -l "$mountpoint" 2>/dev/null log_info "Unmounted '$name'" fi done } cmd_status() { load_global echo "=== SMB/CIFS Mount Status ===" echo "" local shares shares="$(get_shares)" if [ -z "$shares" ]; then echo "No shares configured." return fi for name in $shares; do local enabled server mountpoint desc enabled="$(uci_get ${name}.enabled)" server="$(uci_get ${name}.server)" mountpoint="$(uci_get ${name}.mountpoint)" desc="$(uci_get ${name}.description)" printf "Share: %s" "$name" [ -n "$desc" ] && printf " (%s)" "$desc" echo "" printf " Server: %s\n" "$server" printf " Mountpoint: %s\n" "$mountpoint" printf " Enabled: %s\n" "$([ "$enabled" = "1" ] && echo "yes" || echo "no")" if [ -n "$mountpoint" ] && is_mounted "$mountpoint"; then # Get mount stats local usage usage=$(df -h "$mountpoint" 2>/dev/null | tail -1) printf " Status: MOUNTED\n" if [ -n "$usage" ]; then local size used avail pct size=$(echo "$usage" | awk '{print $2}') used=$(echo "$usage" | awk '{print $3}') avail=$(echo "$usage" | awk '{print $4}') pct=$(echo "$usage" | awk '{print $5}') printf " Disk: %s used / %s total (%s free, %s)\n" "$used" "$size" "$avail" "$pct" fi else printf " Status: NOT MOUNTED\n" fi echo "" done } cmd_test() { local name="$1" [ -z "$name" ] && { echo "Usage: smbfsctl test " >&2; exit 1; } share_exists "$name" || { log_error "Share '$name' not found"; exit 1; } load_global local server server="$(uci_get ${name}.server)" # Extract hostname from //host/share local host host=$(echo "$server" | sed 's|^//||; s|/.*||') log_info "Testing connectivity to $host..." # Test network reachability if ping -c 1 -W "$timeout" "$host" >/dev/null 2>&1; then log_info "Host $host is reachable" else log_error "Host $host is not reachable" return 1 fi # Test SMB port (445) local smb_ok=0 if [ -f /proc/net/tcp ]; then # Try a TCP connection via shell if (echo > /dev/tcp/"$host"/445) 2>/dev/null; then smb_ok=1 fi fi # Fallback: try netstat or just attempt mount if [ "$smb_ok" = "1" ]; then log_info "SMB port 445 is open on $host" else log_warn "Could not verify SMB port 445 (will attempt mount anyway)" fi # Try a test mount require_root local mountpoint="/tmp/smbfs-test-$$" mkdir -p "$mountpoint" local opts opts="$(build_mount_opts "$name")" if mount -t cifs "$server" "$mountpoint" -o "$opts" 2>&1; then log_info "Test mount successful — share is accessible" local count count=$(ls -1 "$mountpoint" 2>/dev/null | wc -l) log_info "Contents: $count items visible" umount "$mountpoint" 2>/dev/null else log_error "Test mount failed — check server, credentials, or share name" rmdir "$mountpoint" 2>/dev/null return 1 fi rmdir "$mountpoint" 2>/dev/null log_info "Test complete: share '$name' is working" } # ============================================================================= # MAIN # ============================================================================= case "${1:-}" in add) shift; cmd_add "$@" ;; remove) shift; cmd_remove "$@" ;; enable) shift; cmd_enable "$@" ;; disable) shift; cmd_disable "$@" ;; credentials) shift; cmd_credentials "$@" ;; set) shift; cmd_set "$@" ;; list) shift; cmd_list "$@" ;; mount) shift; cmd_mount "$@" ;; mount-all) shift; cmd_mount_all "$@" ;; umount) shift; cmd_umount "$@" ;; umount-all) shift; cmd_umount_all "$@" ;; status) shift; cmd_status "$@" ;; test) shift; cmd_test "$@" ;; help|--help|-h|'') usage ;; *) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;; esac