pastebin - collaborative debugging tool
rovema.kpaste.net RSS


sshnfs - ssh with nfs forwarding
Posted by Anonymous on Tue 1st Aug 2023 19:06
raw | new post
modification of post by Anonymous (view diff)

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