pastebin - collaborative debugging tool
rovema.kpaste.net RSS


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

  1. #!/usr/bin/ksh93
  2.  
  3. #
  4. # mount_sshnfs - mount NFSv4 filesystem through ssh tunnel
  5. #
  6.  
  7. #
  8. # Example usage:
  9. # 1. Mount&&unmount /export/home/rmainz on NFS server "/export/home/rmainz"
  10. # $ mkdir -p /foobarmnt
  11. # $ ksh mount_sshnfs.ksh mount ssh+nfs://rmainz@derfwpc5131/export/home/rmainz /foobarmnt
  12. # $ mount_sshnfs.ksh umount /foobarmnt
  13. #
  14.  
  15. #
  16. # Written by Roland Mainz <roland.mainz@nrubsig.org>
  17. #
  18.  
  19. #    
  20. # simple netstat -n parser
  21. #    
  22. function netstat_list_connections
  23. {
  24.         set -o nounset
  25.         nameref data=$1
  26.        
  27.         compound out=( typeset stdout stderr ; integer res )
  28.        
  29.         out.stderr="${ { out.stdout="${ LC_ALL='POSIX' PATH='/usr/bin:/bin' netstat -a -n ; (( out.res=$? )) ; }" ; } 2>&1 ; }"
  30.         if (( out.res != 0 )) || [[ ${out.stderr} != '' ]] ; then
  31.                 return 1
  32.         fi
  33.  
  34.         typeset -a data.connections
  35.         typeset l
  36.         typeset leftover
  37.         integer dci=0 # data.connections array index
  38.  
  39.         while read l ; do
  40.                 leftover="${l/~(Elrx)
  41.                 (?: # non-capturing group
  42.                         #
  43.                         # regex group for tcp,udp
  44.                         #
  45.                         (tcp|tcp6|udp|udp6|raw|raw6|sctp|sctp6) # Proto
  46.                         [[:space:]]+
  47.                         ([[:digit:]]+)                  # Recv-Q
  48.                         [[:space:]]+
  49.                         ([[:digit:]]+)                  # Send-Q
  50.                         [[:space:]]+
  51.                         ([^[:space:]]+)                 # Local Address
  52.                         [[:space:]]+
  53.                         ([^[:space:]]+)                 # Foreign Address
  54.                         (?:
  55.                                 |
  56.                                 [[:space:]]+
  57.                                 ([^[:space:]]*?)        # State (Optional)
  58.                         )
  59.                 |
  60.                         #
  61.                         # regex for unix
  62.                         #
  63.                         (unix)                          # Proto
  64.                         [[:space:]]+
  65.                         ([[:digit:]]+)                  # RefCnt
  66.                         [[:space:]]+
  67.                         (\[.+?\])                       # Flags
  68.                         [[:space:]]+
  69.                         ([^[:space:]]+)                 # Type
  70.                         [[:space:]]+
  71.                         ([^[:space:]]*?)                # State (optional)
  72.                         [[:space:]]+
  73.                         ([[:digit:]]+)                  # I-Node
  74.                         (?:
  75.                                 |
  76.                                 [[:space:]]+
  77.                                 ([^[:space:]]+)         # Path (optional)
  78.                         )
  79.                 )
  80.                         /X}"
  81.        
  82.                 # If the regex above did not match then .sh.match
  83.                 # remains untouched, so we might see data from the
  84.                 # previous round.
  85.                 # So we check the "leftover" var whether it just
  86.                 # contains the dummy value of "X" to indicate a
  87.                 # successful regex match
  88.                 if [[ "$leftover" == 'X' ]] ; then
  89.                         #print -v .sh.match
  90.                        
  91.                         if [[ "${.sh.match[1]-}" != '' ]] ; then
  92.                                 nameref dcn=data.connections[$dci]
  93.  
  94.                                 typeset dcn.proto="${.sh.match[1]}"
  95.                                 typeset dcn.recv_q="${.sh.match[2]}"
  96.                                 typeset dcn.send_q="${.sh.match[3]}"
  97.                                 typeset dcn.local_address="${.sh.match[4]}"
  98.                                 typeset dcn.foreign_address="${.sh.match[5]}"
  99.                                 typeset dcn.state="${.sh.match[6]}"
  100.                                 ((dci++))
  101.                         elif [[ "${.sh.match[7]-}" != '' ]] ; then
  102.                                 nameref dcn=data.connections[$dci]
  103.  
  104.                                 typeset dcn.proto="${.sh.match[7]}"
  105.                                 typeset dcn.refcnt="${.sh.match[8]}"
  106.                                 typeset dcn.flags="${.sh.match[9]}"
  107.                                 typeset dcn.type="${.sh.match[10]}"
  108.                                 [[ "${.sh.match[11]}" != '' ]] && typeset dcn.state="${.sh.match[11]}"
  109.                                 typeset dcn.inode="${.sh.match[12]}"
  110.                                 [[ "${.sh.match[13]}" != '' ]] && typeset dcn.path="${.sh.match[13]}"
  111.                                 ((dci++))
  112.                         fi
  113.                 else
  114.                         true
  115.                         #printf $"leftover=%q\n" "${leftover}"
  116.                 fi
  117.         done <<<"${out.stdout}"
  118.        
  119.         return 0
  120. }
  121.  
  122. function netstat_list_active_local_tcp_connections
  123. {
  124.         set -o nounset
  125.         nameref ar=$1
  126.         compound c
  127.         integer port
  128.         integer i
  129.  
  130.         netstat_list_connections c || return 1
  131.         #print -v c
  132.        
  133.         [[ -v ar ]] || integer -a ar
  134.  
  135.         for i in "${!c.connections[@]}" ; do
  136.                 nameref n=c.connections[$i]
  137.                
  138.                 # look for only for TCP connections which match
  139.                 # 127.0.*.* or IPv6 ::1 for localhost
  140.                 # 0.0.0.0 or IPv6 :: for all addresses (e.g. servers)
  141.                 if [[ "${n.proto}" == ~(El)tcp && \
  142.                         "${n.local_address}" == ~(Elr)((127\.0\..+|::1)|(::|0\.0\.0\.0|)):[[:digit:]]+ ]] ; then
  143.  
  144.                         port="${n.local_address##*:}"
  145.                         #printf $"port = %d\n" port
  146.  
  147.                         (( ar[port]=1 ))
  148.                 fi
  149.         done
  150.  
  151.         return 0
  152. }
  153.  
  154. function netstat_find_next_free_local_tcp_port
  155. {
  156.         set -o nounset
  157.         compound c=( integer -a ar )
  158.         nameref ret_free_port=$1
  159.         integer start_port
  160.         integer end_port
  161.         integer i
  162.  
  163.         netstat_list_active_local_tcp_connections c.ar || return 1
  164.  
  165.         #print -v c
  166.  
  167.         (( start_port=$2 ))
  168.         if (( $# > 2 )) ; then
  169.                 (( end_port=$3 ))
  170.         else
  171.                 (( end_port=65535 ))
  172.         fi
  173.  
  174.         for ((i=start_port ; i < end_port ; i++ )) ; do
  175.                 if [[ ! -v c.ar[i] ]] ; then
  176.                         (( ret_free_port=i ))
  177.                         return 0
  178.                 fi
  179.         done
  180.        
  181.         return 1
  182. }
  183.  
  184.  
  185. #
  186. # parse_rfc1738_url - parse RFC 1838 URLs
  187. #
  188. # Output variables are named after RFC 1838 Section 5 ("BNF for
  189. # specific URL schemes")
  190. #
  191. function parse_rfc1738_url
  192. {
  193.         set -o nounset
  194.  
  195.         typeset url="$2"
  196.         typeset leftover
  197.         nameref data="$1" # output compound variable
  198.        
  199.         # ~(E) is POSIX extended regular expression matching (instead
  200.         # of shell pattern), "x" means "multiline", "l" means "left
  201.         # anchor", "r" means "right anchor"
  202.         leftover="${url/~(Elrx)
  203.                 (.+?)                           # scheme
  204.                 :\/\/                           # '://'
  205.                 (                               # login
  206.                         (?:
  207.                                 (.+?)           # user (optional)
  208.                                 (?::(.+))?      # password (optional)
  209.                                 @
  210.                         )?
  211.                         (                       # hostport
  212.                                 (.+?)           # host
  213.                                 (?::([[:digit:]]+))? # port (optional)
  214.                         )
  215.                 )
  216.                 (?:\/(.*?))?/X}"                # path (optional)
  217.  
  218.         # All parsed data should be captured via eregex in .sh.match - if
  219.         # there is anything left (except the 'X') then the input string did
  220.         # not properly match the eregex
  221.         [[ "$leftover" == 'X' ]] ||
  222.                 { print -u2 -f $"%s: Parser error, leftover=%q\n" \
  223.                         "$0" "$leftover" ; return 1 ; }
  224.  
  225.         data.url="${.sh.match[0]}"
  226.         data.scheme="${.sh.match[1]}"
  227.         data.login="${.sh.match[2]}"
  228.         # FIXME: This should use [[ ! -v .sh.match[3] ]], but ksh93u has bugs
  229.         [[ "${.sh.match[3]-}" != '' ]] && data.user="${.sh.match[3]}"
  230.         [[ "${.sh.match[4]-}" != '' ]] && data.password="${.sh.match[4]}"
  231.         data.hostport="${.sh.match[5]}"
  232.         data.host="${.sh.match[6]}"
  233.         [[ "${.sh.match[7]-}" != '' ]] && integer data.port="${.sh.match[7]}"
  234.         [[ "${.sh.match[8]-}" != '' ]] && data.uripath="${.sh.match[8]}"
  235.  
  236.         return 0
  237. }
  238.  
  239.  
  240. function parse_sshnfs_url
  241. {
  242.         typeset url="$2"
  243.         nameref data="$1"
  244.        
  245.         parse_rfc1738_url data "$url" || return 1
  246.        
  247.         [[ "${data.scheme}" == ~(Elr)(ssh\+nfs|nfs) ]] || \
  248.                 { print -u2 -f $"%s: Not a nfs:// or ssh+nfs:// url\n" "$0" ; return 1 ; }
  249.         [[ "${data.host}" != '' ]] || { print -u2 -f $"%s: NFS hostname missing\n" "$0" ; return 1 ; }
  250.         [[ "${data.uripath}" != '' ]] || { print -u2 -f $"%s: NFS path missing\n" "$0" ; return 1 ; }
  251.        
  252.         return 0
  253. }
  254.  
  255.  
  256. function mountpoint2configfilename
  257. {
  258.         nameref configfilename=$1
  259.         typeset mountpoint="$2"
  260.  
  261.         #
  262.         # FIXME:
  263.         # - We should urlencode more than just '/'
  264.         # - We should strip the leading '/'
  265.         # - We should use realpath(1) for mountpoints here
  266.         #
  267.  
  268.         # .cpv means ComPound Variable"
  269.         configfilename="/tmp/mount_sshnfs/${mountpoint//\//%2f}.cpv"
  270.         return 0
  271. }
  272.  
  273.  
  274. function cmd_mount
  275. {
  276.         set -o nounset
  277.         nameref c=$1
  278.  
  279.         typeset mydebug=false # fixme: should be "bool" for ksh93v
  280.         typeset c.url="${c.args[1]-}"
  281.         typeset c.mountpoint="${c.args[2]-}"
  282.         typeset mount_config_filename
  283.        
  284.         parse_sshnfs_url c.nfs_server "${c.url}" || return 1
  285.  
  286.         mountpoint2configfilename mount_config_filename "${c.mountpoint}"
  287.  
  288.         if [[ -f "${mount_config_filename}" ]] ; then
  289.                 print -u2 -f $"%s: Config file %q for mount point %q found.\n" \
  290.                         "$0" \
  291.                         "$mount_config_filename" \
  292.                         "${c.mountpoint}"
  293.                 return 1
  294.         fi
  295.  
  296.         mkdir -p '/tmp/mount_sshnfs/'
  297.         if [[ ! -w '/tmp/mount_sshnfs/' ]] ; then
  298.                 print -u2 -f $"%s: mount_nfs data directory %q not writeable.\n" \
  299.                         "$0" \
  300.                         '/tmp/mount_sshnfs/'
  301.                 return 1
  302.         fi
  303.  
  304.         ${mydebug} && print -v c
  305.  
  306.         case "${c.nfs_server.scheme}" in
  307.                 'ssh+nfs')
  308.                         #
  309.                         # Find free local forwarding port
  310.                         #
  311.  
  312.                         # port on THIS machine
  313.                         integer c.local_forward_port
  314.  
  315.                         (( i=34049 ))
  316.                         netstat_find_next_free_local_tcp_port c.local_forward_port $i
  317.  
  318.                         c.ssh_control_socket_name="/tmp/mount_sshnfs/mount_sshnfs_ssh-control-socket_logname${LOGNAME}_ppid${PPID}_pid$$"
  319.  
  320.                         #
  321.                         # Find SSH login user name for NFS server
  322.                         #
  323.                         if [[ -v c.nfs_server.user ]] ; then
  324.                                 typeset c.nfsserver_ssh_login_name="${c.nfs_server.user}"
  325.                         fi
  326.                         if [[ ! -v c.nfsserver_ssh_login_name ]] ; then
  327.                                 # default user name if neither URL nor
  328.                                 # "-o NFSServerSSHLoginName=..." were given
  329.                                 typeset c.nfsserver_ssh_login_name="$LOGNAME"
  330.                         fi
  331.  
  332.                         #
  333.                         # Forward NFS port from server to local machine
  334.                         #
  335.                         # Notes:
  336.                         # - We use $ ssh -M ... # here as a way to terminate the port
  337.                         # forwarding process later using "-O exit" without the need
  338.                         # for a pid
  339.                         #
  340.                         print -u2 -f $"# Please enter the login data for NFS server (%s):\n" \
  341.                                 "${c.nfsserver_ssh_login_name}@${c.nfs_server.host}"
  342.  
  343.                         # fixme: c.nfs_server.port is fixed
  344.                         # for ssh+nfs://-URLs, so for now we
  345.                         # have to hardcode TCP/2049 for now
  346.                         ssh \
  347.                                 -L "${c.local_forward_port}:localhost:2049" \
  348.                                 -M -S "${c.ssh_control_socket_name}" \
  349.                                 -N \
  350.                                 -f -o 'ExitOnForwardFailure=yes' \
  351.                                 "${c.ssh_jumphost_args[@]}" \
  352.                                 "${c.nfsserver_ssh_login_name}@${c.nfs_server.host}"
  353.                         if (( $? != 0 )) ; then
  354.                                 print -u2 -f $"%s: NFS forwarding ssh failed with error code %d\n" "$0" $?
  355.                                 return 1
  356.                         fi
  357.  
  358.                         # debug
  359.                         ${mydebug} && \
  360.                                 ssh \
  361.                                         -S "${c.ssh_control_socket_name}" \
  362.                                         -O 'check' \
  363.                                         "${c.nfsserver_ssh_login_name}@${c.nfs_server.host}"
  364.  
  365.  
  366.                         #
  367.                         # Do the mount
  368.                         #
  369.                         mount -vvv -t nfs -o vers=4,port="${c.local_forward_port}" \
  370.                                 "localhost:/${c.nfs_server.uripath}" \
  371.                                 "${c.mountpoint}"
  372.                         (( retval=$? ))
  373.  
  374.                         if (( retval != 0 )) ; then
  375.                                 #
  376.                                 # Quit ssh port forwarding process
  377.                                 #
  378.                                 ssh \
  379.                                         -S "${c.ssh_control_socket_name}" \
  380.                                         -O 'exit' \
  381.                                         "${c.nfsserver_ssh_login_name}@${c.nfs_server.host}"
  382.                                 return $retval
  383.                         fi
  384.  
  385.  
  386.                         #
  387.                         # Save status data
  388.                         #
  389.                         compound mnt_config=(
  390.                                 typeset url="${c.url}"
  391.                                 typeset mountpoint="${c.mountpoint}"
  392.                                 typeset ssh_control_socket_name="${c.ssh_control_socket_name}"
  393.                                 typeset nfsserver_ssh_login_name="${c.nfsserver_ssh_login_name}"
  394.                                 typeset nfsserver_host="${c.nfs_server.host}"
  395.                         )
  396.                         print -v mnt_config >"$mount_config_filename"
  397.  
  398.                         return 0
  399.                         ;;
  400.                 *)
  401.                         print -u2 -f $"%s: Unknown URL scheme %q\n" "$0" "${c.nfs_server.scheme}"
  402.                         return 2
  403.                         ;;
  404.         esac
  405.  
  406.         # notreached
  407. }
  408.  
  409.  
  410. function cmd_umount
  411. {
  412.         set -o nounset
  413.         nameref c=$1
  414.         integer retval
  415.  
  416.         typeset mydebug=false # fixme: should be "bool" for ksh93v
  417.         typeset c.mountpoint="${c.args[1]-}"   
  418.  
  419.         typeset mount_config_filename
  420.         mountpoint2configfilename mount_config_filename "${c.mountpoint}"
  421.  
  422.         if [[ ! -f "${mount_config_filename}" ]] ; then
  423.                 print -u2 -f $"%s: Config file %q for mount point %q not found.\n" \
  424.                         "$0" \
  425.                         "$mount_config_filename" \
  426.                         "${c.mountpoint}"
  427.         fi
  428.  
  429.         compound mnt_config
  430.         read -C mnt_config <"${mount_config_filename}" || return 1
  431.  
  432.         ${mydebug} && print -v mnt_config
  433.  
  434.         umount "${c.mountpoint}"
  435.         (( retval=$? ))
  436.  
  437.         if (( retval != 0 )) ; then
  438.                 return $retval
  439.         fi
  440.  
  441.         #
  442.         # Quit ssh port forwarding process
  443.         #
  444.         ssh \
  445.                 -S "${mnt_config.ssh_control_socket_name}" \
  446.                 -O 'exit' \
  447.                 "${mnt_config.nfsserver_ssh_login_name}@${mnt_config.nfsserver_host}"
  448.  
  449.         rm -f "${mount_config_filename}"
  450.         return 0
  451. }
  452.  
  453.  
  454. function main
  455. {
  456.         set -o nounset
  457.         compound c
  458.  
  459.         #
  460.         # Expand MOUNT_SSHNFS_OPTIONS before arguments given to
  461.         # mount_sshnfs.ksh.
  462.         # By default we use IFS=$' \t\n' for argument splitting
  463.         #
  464.         typeset c.args=( ${MOUNT_SSHNFS_OPTIONS-} "$@" )
  465.  
  466.         #
  467.         # Subcommand dispatcher
  468.         #
  469.         case "${c.args[0]-}" in
  470.                 'mount')
  471.                         cmd_mount c
  472.                         return $?
  473.                         ;;
  474.                 'umount')
  475.                         cmd_umount c
  476.                         return $?
  477.                         ;;
  478.                 'status' | 'restart_forwarding')
  479.                         print -u2 -f $"%s: not implemented yet\n" "$0"
  480.                         return 2
  481.                         ;;
  482.                 *)
  483.                         print -u2 -f $"%s: Unknown command %q\n" \
  484.                                 "$0" "${c.args[0]-}"
  485.                         return 1
  486.                         ;;
  487.         esac
  488.        
  489.         # notreached
  490. }
  491.  
  492.  
  493. #
  494. # main
  495. #
  496. builtin mkdir
  497.  
  498. main "$@"
  499. exit $?
  500.  
  501. # EOF.

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.

Syntax highlighting:

To highlight particular lines, prefix each line with {%HIGHLIGHT}




All content is user-submitted.
The administrators of this site (kpaste.net) are not responsible for their content.
Abuse reports should be emailed to us at