- #!/usr/bin/ksh93
 - #
 - # mount_sshnfs - mount NFSv4 filesystem through ssh tunnel
 - #
 - #
 - # Example usage:
 - # 1. Mount&&unmount /export/home/rmainz on NFS server "/export/home/rmainz"
 - # $ mkdir -p /foobarmnt
 - # $ ksh mount_sshnfs.ksh mount ssh+nfs://rmainz@derfwpc5131/export/home/rmainz /foobarmnt
 - # $ mount_sshnfs.ksh umount /foobarmnt
 - #
 - #
 - # Written by Roland Mainz <roland.mainz@nrubsig.org>
 - #
 - #
 - # simple netstat -n parser
 - #
 - function netstat_list_connections
 - {
 - set -o nounset
 - nameref data=$1
 - compound out=( typeset stdout stderr ; integer res )
 - out.stderr="${ { out.stdout="${ LC_ALL='POSIX' PATH='/usr/bin:/bin' netstat -a -n ; (( out.res=$? )) ; }" ; } 2>&1 ; }"
 - if (( out.res != 0 )) || [[ ${out.stderr} != '' ]] ; then
 - return 1
 - fi
 - typeset -a data.connections
 - typeset l
 - typeset leftover
 - integer dci=0 # data.connections array index
 - while read l ; do
 - leftover="${l/~(Elrx)
 - (?: # non-capturing group
 - #
 - # regex group for tcp,udp
 - #
 - (tcp|tcp6|udp|udp6|raw|raw6|sctp|sctp6) # Proto
 - [[:space:]]+
 - ([[:digit:]]+) # Recv-Q
 - [[:space:]]+
 - ([[:digit:]]+) # Send-Q
 - [[:space:]]+
 - ([^[:space:]]+) # Local Address
 - [[:space:]]+
 - ([^[:space:]]+) # Foreign Address
 - (?:
 - |
 - [[:space:]]+
 - ([^[:space:]]*?) # State (Optional)
 - )
 - |
 - #
 - # regex for unix
 - #
 - (unix) # Proto
 - [[:space:]]+
 - ([[:digit:]]+) # RefCnt
 - [[:space:]]+
 - (\[.+?\]) # Flags
 - [[:space:]]+
 - ([^[:space:]]+) # Type
 - [[:space:]]+
 - ([^[:space:]]*?) # State (optional)
 - [[:space:]]+
 - ([[:digit:]]+) # I-Node
 - (?:
 - |
 - [[:space:]]+
 - ([^[:space:]]+) # Path (optional)
 - )
 - )
 - /X}"
 - # If the regex above did not match then .sh.match
 - # remains untouched, so we might see data from the
 - # previous round.
 - # So we check the "leftover" var whether it just
 - # contains the dummy value of "X" to indicate a
 - # successful regex match
 - if [[ "$leftover" == 'X' ]] ; then
 - #print -v .sh.match
 - if [[ "${.sh.match[1]-}" != '' ]] ; then
 - nameref dcn=data.connections[$dci]
 - typeset dcn.proto="${.sh.match[1]}"
 - typeset dcn.recv_q="${.sh.match[2]}"
 - typeset dcn.send_q="${.sh.match[3]}"
 - typeset dcn.local_address="${.sh.match[4]}"
 - typeset dcn.foreign_address="${.sh.match[5]}"
 - typeset dcn.state="${.sh.match[6]}"
 - ((dci++))
 - elif [[ "${.sh.match[7]-}" != '' ]] ; then
 - nameref dcn=data.connections[$dci]
 - typeset dcn.proto="${.sh.match[7]}"
 - typeset dcn.refcnt="${.sh.match[8]}"
 - typeset dcn.flags="${.sh.match[9]}"
 - typeset dcn.type="${.sh.match[10]}"
 - [[ "${.sh.match[11]}" != '' ]] && typeset dcn.state="${.sh.match[11]}"
 - typeset dcn.inode="${.sh.match[12]}"
 - [[ "${.sh.match[13]}" != '' ]] && typeset dcn.path="${.sh.match[13]}"
 - ((dci++))
 - fi
 - else
 - true
 - #printf $"leftover=%q\n" "${leftover}"
 - fi
 - done <<<"${out.stdout}"
 - return 0
 - }
 - function netstat_list_active_local_tcp_connections
 - {
 - set -o nounset
 - nameref ar=$1
 - compound c
 - integer port
 - integer i
 - netstat_list_connections c || return 1
 - #print -v c
 - [[ -v ar ]] || integer -a ar
 - for i in "${!c.connections[@]}" ; do
 - nameref n=c.connections[$i]
 - # look for only for TCP connections which match
 - # 127.0.*.* or IPv6 ::1 for localhost
 - # 0.0.0.0 or IPv6 :: for all addresses (e.g. servers)
 - if [[ "${n.proto}" == ~(El)tcp && \
 - "${n.local_address}" == ~(Elr)((127\.0\..+|::1)|(::|0\.0\.0\.0|)):[[:digit:]]+ ]] ; then
 - port="${n.local_address##*:}"
 - #printf $"port = %d\n" port
 - (( ar[port]=1 ))
 - fi
 - done
 - return 0
 - }
 - function netstat_find_next_free_local_tcp_port
 - {
 - set -o nounset
 - compound c=( integer -a ar )
 - nameref ret_free_port=$1
 - integer start_port
 - integer end_port
 - integer i
 - netstat_list_active_local_tcp_connections c.ar || return 1
 - #print -v c
 - (( start_port=$2 ))
 - if (( $# > 2 )) ; then
 - (( end_port=$3 ))
 - else
 - (( end_port=65535 ))
 - fi
 - for ((i=start_port ; i < end_port ; i++ )) ; do
 - if [[ ! -v c.ar[i] ]] ; then
 - (( ret_free_port=i ))
 - return 0
 - fi
 - done
 - return 1
 - }
 - #
 - # parse_rfc1738_url - parse RFC 1838 URLs
 - #
 - # Output variables are named after RFC 1838 Section 5 ("BNF for
 - # specific URL schemes")
 - #
 - function parse_rfc1738_url
 - {
 - set -o nounset
 - typeset url="$2"
 - typeset leftover
 - nameref data="$1" # output compound variable
 - # ~(E) is POSIX extended regular expression matching (instead
 - # of shell pattern), "x" means "multiline", "l" means "left
 - # anchor", "r" means "right anchor"
 - leftover="${url/~(Elrx)
 - (.+?) # scheme
 - :\/\/ # '://'
 - ( # login
 - (?:
 - (.+?) # user (optional)
 - (?::(.+))? # password (optional)
 - @
 - )?
 - ( # hostport
 - (.+?) # host
 - (?::([[:digit:]]+))? # port (optional)
 - )
 - )
 - (?:\/(.*?))?/X}" # path (optional)
 - # All parsed data should be captured via eregex in .sh.match - if
 - # there is anything left (except the 'X') then the input string did
 - # not properly match the eregex
 - [[ "$leftover" == 'X' ]] ||
 - { print -u2 -f $"%s: Parser error, leftover=%q\n" \
 - "$0" "$leftover" ; return 1 ; }
 - data.url="${.sh.match[0]}"
 - data.scheme="${.sh.match[1]}"
 - data.login="${.sh.match[2]}"
 - # FIXME: This should use [[ ! -v .sh.match[3] ]], but ksh93u has bugs
 - [[ "${.sh.match[3]-}" != '' ]] && data.user="${.sh.match[3]}"
 - [[ "${.sh.match[4]-}" != '' ]] && data.password="${.sh.match[4]}"
 - data.hostport="${.sh.match[5]}"
 - data.host="${.sh.match[6]}"
 - [[ "${.sh.match[7]-}" != '' ]] && integer data.port="${.sh.match[7]}"
 - [[ "${.sh.match[8]-}" != '' ]] && data.uripath="${.sh.match[8]}"
 - return 0
 - }
 - function parse_sshnfs_url
 - {
 - typeset url="$2"
 - nameref data="$1"
 - parse_rfc1738_url data "$url" || return 1
 - [[ "${data.scheme}" == ~(Elr)(ssh\+nfs|nfs) ]] || \
 - { print -u2 -f $"%s: Not a nfs:// or ssh+nfs:// url\n" "$0" ; return 1 ; }
 - [[ "${data.host}" != '' ]] || { print -u2 -f $"%s: NFS hostname missing\n" "$0" ; return 1 ; }
 - [[ "${data.uripath}" != '' ]] || { print -u2 -f $"%s: NFS path missing\n" "$0" ; return 1 ; }
 - return 0
 - }
 - function mountpoint2configfilename
 - {
 - nameref configfilename=$1
 - typeset mountpoint="$2"
 - #
 - # FIXME:
 - # - We should urlencode more than just '/'
 - # - We should strip the leading '/'
 - # - We should use realpath(1) for mountpoints here
 - #
 - # .cpv means ComPound Variable"
 - configfilename="/tmp/mount_sshnfs/${mountpoint//\//%2f}.cpv"
 - return 0
 - }
 - function cmd_mount
 - {
 - set -o nounset
 - nameref c=$1
 - typeset mydebug=false # fixme: should be "bool" for ksh93v
 - typeset c.url="${c.args[1]-}"
 - typeset c.mountpoint="${c.args[2]-}"
 - typeset mount_config_filename
 - parse_sshnfs_url c.nfs_server "${c.url}" || return 1
 - mountpoint2configfilename mount_config_filename "${c.mountpoint}"
 - if [[ -f "${mount_config_filename}" ]] ; then
 - print -u2 -f $"%s: Config file %q for mount point %q found.\n" \
 - "$0" \
 - "$mount_config_filename" \
 - "${c.mountpoint}"
 - return 1
 - fi
 - mkdir -p '/tmp/mount_sshnfs/'
 - if [[ ! -w '/tmp/mount_sshnfs/' ]] ; then
 - print -u2 -f $"%s: mount_nfs data directory %q not writeable.\n" \
 - "$0" \
 - '/tmp/mount_sshnfs/'
 - return 1
 - fi
 - ${mydebug} && print -v c
 - case "${c.nfs_server.scheme}" in
 - 'ssh+nfs')
 - #
 - # Find free local forwarding port
 - #
 - # port on THIS machine
 - integer c.local_forward_port
 - (( i=34049 ))
 - netstat_find_next_free_local_tcp_port c.local_forward_port $i
 - c.ssh_control_socket_name="/tmp/mount_sshnfs/mount_sshnfs_ssh-control-socket_logname${LOGNAME}_ppid${PPID}_pid$$"
 - #
 - # Find SSH login user name for NFS server
 - #
 - if [[ -v c.nfs_server.user ]] ; then
 - typeset c.nfsserver_ssh_login_name="${c.nfs_server.user}"
 - fi
 - if [[ ! -v c.nfsserver_ssh_login_name ]] ; then
 - # default user name if neither URL nor
 - # "-o NFSServerSSHLoginName=..." were given
 - typeset c.nfsserver_ssh_login_name="$LOGNAME"
 - fi
 - #
 - # Forward NFS port from server to local machine
 - #
 - # Notes:
 - # - We use $ ssh -M ... # here as a way to terminate the port
 - # forwarding process later using "-O exit" without the need
 - # for a pid
 - #
 - print -u2 -f $"# Please enter the login data for NFS server (%s):\n" \
 - "${c.nfsserver_ssh_login_name}@${c.nfs_server.host}"
 - # fixme: c.nfs_server.port is fixed
 - # for ssh+nfs://-URLs, so for now we
 - # have to hardcode TCP/2049 for now
 - ssh \
 - -L "${c.local_forward_port}:localhost:2049" \
 - -M -S "${c.ssh_control_socket_name}" \
 - -N \
 - -f -o 'ExitOnForwardFailure=yes' \
 - "${c.ssh_jumphost_args[@]}" \
 - "${c.nfsserver_ssh_login_name}@${c.nfs_server.host}"
 - if (( $? != 0 )) ; then
 - print -u2 -f $"%s: NFS forwarding ssh failed with error code %d\n" "$0" $?
 - return 1
 - fi
 - # debug
 - ${mydebug} && \
 - ssh \
 - -S "${c.ssh_control_socket_name}" \
 - -O 'check' \
 - "${c.nfsserver_ssh_login_name}@${c.nfs_server.host}"
 - #
 - # Do the mount
 - #
 - mount -vvv -t nfs -o vers=4,port="${c.local_forward_port}" \
 - "localhost:/${c.nfs_server.uripath}" \
 - "${c.mountpoint}"
 - (( retval=$? ))
 - if (( retval != 0 )) ; then
 - #
 - # Quit ssh port forwarding process
 - #
 - ssh \
 - -S "${c.ssh_control_socket_name}" \
 - -O 'exit' \
 - "${c.nfsserver_ssh_login_name}@${c.nfs_server.host}"
 - return $retval
 - fi
 - #
 - # Save status data
 - #
 - compound mnt_config=(
 - typeset url="${c.url}"
 - typeset mountpoint="${c.mountpoint}"
 - typeset ssh_control_socket_name="${c.ssh_control_socket_name}"
 - typeset nfsserver_ssh_login_name="${c.nfsserver_ssh_login_name}"
 - typeset nfsserver_host="${c.nfs_server.host}"
 - )
 - print -v mnt_config >"$mount_config_filename"
 - return 0
 - ;;
 - *)
 - print -u2 -f $"%s: Unknown URL scheme %q\n" "$0" "${c.nfs_server.scheme}"
 - return 2
 - ;;
 - esac
 - # notreached
 - }
 - function cmd_umount
 - {
 - set -o nounset
 - nameref c=$1
 - integer retval
 - typeset mydebug=false # fixme: should be "bool" for ksh93v
 - typeset c.mountpoint="${c.args[1]-}"
 - typeset mount_config_filename
 - mountpoint2configfilename mount_config_filename "${c.mountpoint}"
 - if [[ ! -f "${mount_config_filename}" ]] ; then
 - print -u2 -f $"%s: Config file %q for mount point %q not found.\n" \
 - "$0" \
 - "$mount_config_filename" \
 - "${c.mountpoint}"
 - fi
 - compound mnt_config
 - read -C mnt_config <"${mount_config_filename}" || return 1
 - ${mydebug} && print -v mnt_config
 - umount "${c.mountpoint}"
 - (( retval=$? ))
 - if (( retval != 0 )) ; then
 - return $retval
 - fi
 - #
 - # Quit ssh port forwarding process
 - #
 - ssh \
 - -S "${mnt_config.ssh_control_socket_name}" \
 - -O 'exit' \
 - "${mnt_config.nfsserver_ssh_login_name}@${mnt_config.nfsserver_host}"
 - rm -f "${mount_config_filename}"
 - return 0
 - }
 - function main
 - {
 - set -o nounset
 - compound c
 - #
 - # Expand MOUNT_SSHNFS_OPTIONS before arguments given to
 - # mount_sshnfs.ksh.
 - # By default we use IFS=$' \t\n' for argument splitting
 - #
 - typeset c.args=( ${MOUNT_SSHNFS_OPTIONS-} "$@" )
 - #
 - # Subcommand dispatcher
 - #
 - case "${c.args[0]-}" in
 - 'mount')
 - cmd_mount c
 - return $?
 - ;;
 - 'umount')
 - cmd_umount c
 - return $?
 - ;;
 - 'status' | 'restart_forwarding')
 - print -u2 -f $"%s: not implemented yet\n" "$0"
 - return 2
 - ;;
 - *)
 - print -u2 -f $"%s: Unknown command %q\n" \
 - "$0" "${c.args[0]-}"
 - return 1
 - ;;
 - esac
 - # notreached
 - }
 - #
 - # main
 - #
 - builtin mkdir
 - main "$@"
 - exit $?
 - # EOF.
 
mount_sshnfs - mount NFSv4 filesystem through ssh tunnel
Posted by Anonymous on Mon 24th Jul 2023 12:23
raw | new post
view followups (newest first): mount_sshnfs - mount NFSv4 filesystem through ssh tunnel by Anonymous
Submit a correction or amendment below (click here to make a fresh posting)
After submitting an amendment, you'll be able to view the differences between the old and new posts easily.
 rovema.kpaste.net RSS