SHARE
TWEET

ddwrt-pia-port-forward.sh

eibgrad Jan 7th, 2018 (edited) 877 Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. #!/bin/sh
  2. #DEBUG= # uncomment/comment to enable/disable
  3.  
  4. #          name: ddwrt-pia-port-forward.sh
  5. #       version: 2.1.1, 11-jul-2019, by eibgrad
  6. #       purpose: establish port forward over PIA openvpn tunnel
  7. #   script type: startup (autostart)
  8. #  installation:
  9. #    1. enable jffs (administration->jffs)
  10. #    2. enable syslog (administration->logging->syslog)
  11. #    3. use shell (telnet/ssh) to execute one of the following commands:
  12. #         curl -kLs bit.ly/ddwrt-installer|tr -d '\r'|sh -s -- --wrap P9nmpyxh startup
  13. #       or
  14. #         wget -qO - bit.ly/ddwrt-installer|tr -d '\r'|sh -s -- --wrap P9nmpyxh startup
  15. #    4. execute wrapped script from shell (telnet/ssh) w/ --help option to
  16. #       review available options:
  17. #         /jffs/etc/config/ddwrt-pia-port-forward.sh --help
  18. #    5. modify wrapper script w/ your preferred options:
  19. #         vi /jffs/etc/config/ddwrt-pia-port-forward.startup
  20. #    6. (optional) modify callback script w/ your preferred actions:
  21. #         vi /jffs/etc/config/ddwrt-pia-port-forward.callback
  22. #    7. reboot
  23. #   limitations:
  24. #    - except for the --reset and --help options, this script will only execute
  25. #      from startup (autostart)
  26. #    - PIA port forwarding is only available from a limited number of servers
  27. #      (visit PIA website for the current list)
  28. #    - PIA only supports one (1) port forward per connection
  29. #    - PIA port forward must be requested within two (2) minutes of the
  30. #      connection being established
  31. #    - PIA port forward is NOT accessible from the same public ip as the
  32. #      openvpn client that initiated the connection
  33. #    - some ddns providers blacklist PIA public ips (e.g., afraid.org)
  34. #
  35. #  NOTE: PIA external port forward is published @ ...
  36. #    http://<router-lan-ip>/user/pia/ext_port_forward.html
  37.  
  38. # ------------------------------ BEGIN OPTIONS ------------------------------- #
  39.  
  40. # internal ip of PIA port forward (default=<router-lan-ip>)
  41. INTERNAL_IP="$(nvram get lan_ipaddr)"
  42.  
  43. # internal port of PIA port forward
  44. INTERNAL_PORT="80"
  45. #INTERNAL_PORT="-" # assign PIA external port to internal port
  46.  
  47. # protocol (tcp|udp|both)
  48. PROTO="tcp"
  49.  
  50. # source ip/network
  51. SOURCE="0.0.0.0/0"
  52.  
  53. # dnsomatic ddns update (optional)
  54. DDNS_USER=""
  55. DDNS_PASS=""
  56. DDNS_HOST="all.dnsomatic.com" # update *all* hostnames
  57.  
  58. # do NOT publish VPN public ip and PIA external port to webserver
  59. #NOPUBLISH= # uncomment/comment to enable/disable
  60.  
  61. # one pass only; do NOT run continously
  62. #ONE_PASS= # uncomment/comment to enable/disable
  63.  
  64. # ------------------------------- END OPTIONS -------------------------------- #
  65.  
  66. # ---------------------- DO NOT CHANGE BELOW THIS LINE ----------------------- #
  67.  
  68. # required for serialization when reentry is possible
  69. LOCK="/tmp/$(basename $0 .sh).lock"
  70. acquire_lock() { while ! mkdir $LOCK > /dev/null 2>&1; do sleep 10; done; }
  71. release_lock() { rmdir $LOCK > /dev/null 2>&1; }
  72.  
  73. PID="/tmp/var/run/$(basename $0 .sh).pid"
  74. OVPN_PID="/tmp/var/run/openvpncl.pid"
  75. CLIENT_ID="openvpncl_pia_client_id" # stored client id (hash/token)
  76.  
  77. PFS_IP="209.222.18.222"  # PIA port forwarding service ip
  78. PFS_PORT="2000"          # PIA port forwarding service port
  79.  
  80. # currently active firewall rules
  81. ACTIVE_RULES="/tmp/$(basename $0 .sh).rules"
  82.  
  83. # callback script for user-defined, post-processing actions
  84. CALLBACK_SCRIPT="${0%.*}.callback"
  85.  
  86. # try curl, fallback to wget
  87. which curl > /dev/null 2>&1 && GET_URL="curl -sLk -m20" || GET_URL="wget -qO - -T20"
  88.  
  89. # execution from cli/console not permitted
  90. NOEXEC=
  91.  
  92. # used for regular expressions and parameter expansion
  93. WS="[[:space:]]"
  94.  
  95. vpn_public_ip="0.0.0.0"
  96. pia_ext_port="0"
  97.  
  98. # function to_lower( string )
  99. to_lower() { echo "$1" | awk '{print tolower($0)}'; }
  100.  
  101. # function validate_options() {
  102. validate_options() {
  103.     local found_error=false
  104.  
  105.     _error() { echo "error: $1"; found_error=true; }
  106.  
  107.     # verify internal ip
  108.     echo $INTERNAL_IP | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}$' || \
  109.         _error "invalid internal ip (--ip): $INTERNAL_IP"
  110.  
  111.     # verify internal port
  112.     if ! echo $INTERNAL_PORT | grep -Eq '^-*$'; then
  113.         if echo ! $INTERNAL_PORT | grep -Eq '^[0-9]*$' || \
  114.                 ! [[ $INTERNAL_PORT -ge 1 && $INTERNAL_PORT -le 65535 ]]; then
  115.             _error "invalid internal port (--port): $INTERNAL_PORT"
  116.         fi
  117.     fi
  118.  
  119.     # verify protocol
  120.     echo $PROTO | grep -iEq '^tcp|udp|both$' || \
  121.         _error "invalid protocol (--proto): $PROTO"
  122.  
  123.     # verify source ip/network
  124.     echo $SOURCE | grep -Eq '^([0-9]{1,3}\.){3}[0-9]{1,3}(/[0-9]{1,2}$|$)' || \
  125.         _error "invalid source ip/network (--source): $SOURCE"
  126.  
  127.     [[ $found_error == true ]] && return 1 || return 0
  128. }
  129.  
  130. # function configure_port_forward()
  131. configure_port_forward() {
  132.     local del_rule int_port
  133.  
  134.     _get_pia_ext_port() {
  135.         __hash128() { echo $(head -n 100 /dev/urandom | md5sum | tr -d ' -'); }
  136.  
  137.         # for develper testing purposes only
  138.         if [ ${TEST+x} ]; then
  139.             # generate random external port (40000-49999)
  140.             echo $((40000 + $(tr -cd 0-9 < /dev/urandom | head -c 4)))
  141.             return
  142.         fi
  143.  
  144.         # retrieve previously generated and saved client hash/token
  145.         local client_id="$(nvram get $CLIENT_ID 2> /dev/null)"
  146.  
  147.         if [ ! $client_id ]; then
  148.             # generate unique 256 bit client hash/token
  149.             client_id="$(__hash128)$(__hash128)"
  150.  
  151.             # make client id persistent
  152.             nvram set $CLIENT_ID="$client_id"
  153.             nvram commit
  154.         fi
  155.  
  156.         local start_time=$(date +%s)
  157.  
  158.         while :; do
  159.             # request external port for port forward from PIA api
  160.             local json="$($GET_URL http://$PFS_IP:$PFS_PORT/?client_id=$client_id)"
  161.  
  162.             [ "$json" ] && { echo $json | grep -o '[0-9]*'; return; }
  163.  
  164.             # must be completed within two (2) minutes of connection establishment
  165.             [ $(($(date +%s) - start_time)) -ge 120 ] && break
  166.  
  167.             sleep 10
  168.         done
  169.  
  170.         # return 0 if PIA external port could not be established
  171.         echo "0"
  172.     }
  173.  
  174.     _ipt() {
  175.         local del_rule="$(echo ${@/-[IA]/-D})"
  176.  
  177.         # precede insert/append w/ deletion to avoid dupes
  178.         while iptables $del_rule 2> /dev/null; do :; done
  179.  
  180.         # add firewall rule
  181.         iptables $@
  182.  
  183.         # save deletion for future rollback
  184.         echo $del_rule >> $ACTIVE_RULES
  185.     }
  186.  
  187.     pia_ext_port="$(_get_pia_ext_port)"
  188.  
  189.     if [ "$pia_ext_port" == "0" ]; then
  190.         echo "error: PIA external port could NOT be established"
  191.         return 1
  192.     fi
  193.  
  194.     echo "info: PIA external port has been established: $pia_ext_port"
  195.  
  196.     # rollback any previously applied firewall rules
  197.     if [ -s $ACTIVE_RULES ]; then
  198.         while read del_rule; do
  199.             while iptables $del_rule 2> /dev/null; do :; done
  200.         done < $ACTIVE_RULES
  201.  
  202.         # clear active rules
  203.         > $ACTIVE_RULES
  204.     fi
  205.  
  206.     # if requested, assign external port to internal port
  207.     echo $INTERNAL_PORT | grep -Eq '^-*$' && \
  208.         int_port="$pia_ext_port" || int_port="$INTERNAL_PORT"
  209.  
  210.     # convert protocol to lowercase
  211.     PROTO="$(to_lower $PROTO)"
  212.  
  213.     if [[ "$PROTO" == "both" || "$PROTO" == "udp" ]]; then
  214.         # add internal udp port forward
  215.         _ipt "-t nat -I PREROUTING -i $dev -p udp -s $SOURCE --dport $pia_ext_port \
  216.            -j DNAT --to $INTERNAL_IP:$int_port"
  217.         _ipt "-I FORWARD -i $dev -p udp -s $SOURCE -d $INTERNAL_IP \
  218.            --dport $int_port -j ACCEPT"
  219.     fi
  220.  
  221.     if [[ "$PROTO" == "both" || "$PROTO" == "tcp" ]]; then
  222.         # add internal tcp port forward
  223.         _ipt "-t nat -I PREROUTING -i $dev -p tcp -s $SOURCE --dport $pia_ext_port \
  224.            -j DNAT --to $INTERNAL_IP:$int_port"
  225.         _ipt "-I FORWARD -i $dev -p tcp -s $SOURCE -d $INTERNAL_IP \
  226.            --dport $int_port -j ACCEPT"
  227.     fi
  228.  
  229.     return 0
  230. }
  231.  
  232. # function get_vpn_public_ip()
  233. get_vpn_public_ip() {
  234.     IP_CHECK_NET="216.239.0.0/16 104.20.0.0/16 146.112.255.205"
  235.     IP_CHECK_URL="ipinfo.io/ip icanhazip.com myip.dnsomatic.com"
  236.     local net i url ip
  237.  
  238.     # briefly bind public ip checker(s) to VPN
  239.     for net in $IP_CHECK_NET; do
  240.         ip route add $net dev $dev
  241.     done
  242.  
  243.     # force routing system to recognize changes
  244.     ip route flush cache
  245.  
  246.     # determine the VPN public ip
  247.     for i in 1 2 3; do
  248.         for url in $IP_CHECK_URL; do
  249.             ip="$($GET_URL $url | grep -Em1 '^([0-9]{1,3}\.){3}[0-9]{1,3}$')"
  250.             [ $ip ] && break 2
  251.         done
  252.         sleep 20
  253.     done
  254.  
  255.     # unbind public ip checker(s) from VPN
  256.     for net in $IP_CHECK_NET; do
  257.         ip route del $net dev $dev
  258.     done
  259.  
  260.     # force routing system to recognize changes
  261.     ip route flush cache
  262.  
  263.     # return 0.0.0.0 if VPN public ip could not be determined
  264.     [ $ip ] && echo $ip || echo "0.0.0.0"
  265. }
  266.  
  267. # function publish_port_forward()
  268. publish_port_forward() {
  269.     local PIA_DIR="/www/user/pia"
  270.     local PIA_PAGE="$PIA_DIR/ext_port_forward.html"
  271.  
  272.     mkdir -p $PIA_DIR
  273.  
  274.     # html intentionally commented out for browser display purposes
  275.     #<!--
  276.     cat <<- EOF > $PIA_PAGE
  277.         <!DOCTYPE html>
  278.         <html lang="en">
  279.           <head>
  280.             <meta charset="utf-8">
  281.             <title>PIA External Port Forward</title>
  282.           </head>
  283.           <body>
  284.             $(echo "$(date): PIA external port forward: $vpn_public_ip:$pia_ext_port")
  285.           </body>
  286.         </html>
  287.         EOF
  288.     #-->
  289. }
  290.  
  291. # function update_ddns()
  292. update_ddns() {
  293.     local ddns_uri="@updates.dnsomatic.com/nic/update?myip=${vpn_public_ip}&hostname="
  294.     local ddns_url="$DDNS_USER:$DDNS_PASS${ddns_uri}"
  295.     local host
  296.  
  297.     # dnsomatic treats a blank hostname the same as all.dnsomatic.com
  298.     [ $DDNS_HOST ] || DDNS_HOST="all.dnsomatic.com"
  299.  
  300.     # dnsomatic doesn't support multiple hostnames in a single update request,
  301.     # so we parse locally and send multiple update requests
  302.  
  303.     # hostnames need to be comma-separated
  304.     for host in ${DDNS_HOST//,/ }; do
  305.         $GET_URL $ddns_url$host | grep -iq "good" && continue
  306.  
  307.         # fall-through means failure
  308.         echo "warning: ddns update failed: $host"
  309.     done
  310. }
  311.  
  312. # function usage()
  313. usage() {
  314.     echo "Usage: ddwrt-pia-port-forward.sh [options]"
  315.     echo
  316.     echo "  Establish port forward over PIA openvpn tunnel."
  317.     echo
  318.     echo "  --ip IP            internal ip of port forward (default=<router-lan-ip>)"
  319.     echo "  --port PORT        internal port of port forward (default=80,"
  320.     echo "                     use - (dash) to assign external port to internal port)"
  321.     echo "  --proto PROTOCOL   (tcp|udp|both) (default=tcp)"
  322.     echo "  --source IP|NET    source ip/network (default=0.0.0.0/0)"
  323.     echo "  --user USERNAME    dnsomatic username"
  324.     echo "  --pass PASSWORD    dnsomatic password"
  325.     echo "  --host HOST[,...]  dnsomatic hostname(s) (default=all.dnsomatic.com)"
  326.     echo "  --nopublish        do NOT publish port forward to webserver"
  327.     echo "  --one-pass         one pass only; do NOT run continously"
  328.     echo "  --exec             allow execution from cli/console"
  329.     echo "  --reset            reset/clear saved client id (hash/token)"
  330.     echo "  --debug            enable debugging mode"
  331.     echo "  -h,--help          this usage information"
  332.     echo
  333.     echo "  # e.g., port forward PIA external ip:port to internal 192.168.1.100:22"
  334.     echo "  ddwrt-pia-port-forward.sh --ip 192.168.1.100 --port 22"
  335.     echo
  336. }
  337.  
  338. # ----------------------------- BEGIN EXECUTION ------------------------------ #
  339.  
  340. # process command line options/arguments
  341. while [ $# -gt 0 ]; do
  342.     case $1 in
  343.         --ip        ) shift; INTERNAL_IP="${1//$WS/}";;
  344.         --port      ) shift; INTERNAL_PORT="${1//$WS/}";;
  345.         --proto     ) shift; PROTO="${1//$WS/}";;
  346.         --source    ) shift; SOURCE="${1//$WS/}";;
  347.         --user      ) shift; DDNS_USER="${1//$WS/}";;
  348.         --pass      ) shift; DDNS_PASS="${1//$WS/}";;
  349.         --host      ) shift; DDNS_HOST="${1//$WS/}";;
  350.         --nopublish ) NOPUBLISH=;;
  351.         --one-pass  ) ONE_PASS=;;
  352.         --exec      ) unset NOEXEC;;
  353.         --reset     ) nvram get $CLIENT_ID > /dev/null 2>&1 && \
  354.                           { nvram unset $CLIENT_ID; nvram commit; };;
  355.         --debug     ) DEBUG=;;
  356.         -h|--help   ) usage; exit 0;;
  357.         --test      ) TEST=; DEBUG=; unset NOEXEC;; # for developer only
  358.         *) echo "error: invalid option/argument: $1"; exit 1;;
  359.     esac
  360.     shift
  361. done
  362.  
  363. # validate options (user input)
  364. validate_options || exit 1
  365.  
  366. # check for interactive shell (not allowed by default)
  367. if echo $PS1 | grep -q '\\u@\\h' && [ ${NOEXEC+x} ]; then
  368.     echo "info: execution from cli/console requires --exec option"
  369.     exit 0
  370. fi
  371. {
  372. [ ${DEBUG+x} ] && set -x
  373.  
  374. # set traps - ignore all interrupts
  375. trap '' SIGHUP SIGINT SIGTERM
  376.  
  377. # one instance at a time
  378. acquire_lock
  379.  
  380. # kill any other running instance of this script
  381. [ -s $PID ] && while kill $(cat $PID) 2> /dev/null; do sleep 10; done
  382.  
  383. # save *this* process id
  384. echo $(sh -c 'echo "$PPID"') > $PID
  385.  
  386. # any concurrent instance(s) may now run
  387. release_lock
  388.  
  389. # reset all traps
  390. trap - SIGHUP SIGINT SIGTERM
  391.  
  392. # wait for syslog to come up
  393. while ! pidof syslogd > /dev/null 2>&1; do sleep 10; done
  394.  
  395. # monitor openvpn client start/restart/stop
  396. while :; do
  397.     # reset all traps
  398.     trap - SIGHUP SIGINT SIGTERM
  399.  
  400.     # wait for openvpn client to start/restart (process id will appear/change)
  401.     while [ "$(cat $OVPN_PID 2> /dev/null)" == "$curr_pid" ]
  402.         do sleep 20; done
  403.  
  404.     curr_pid="$(cat $OVPN_PID 2> /dev/null)"
  405.  
  406.     # wait for openvpn client connection to be established
  407.     while :; do
  408.         # search syslog (from newest to oldest files)
  409.         for file in /var/log/messages*; do
  410.             # search file (from newest to oldest entries)
  411.             sed '1!G;h;$!d' $file | \
  412.                 grep -qi "\[$curr_pid\].*[i]nitialization sequence completed" && break 2
  413.         done
  414.  
  415.         # start over if openvpn process has been stopped/restarted
  416.         ps | grep "^[ ]*$curr_pid " && sleep 20 || continue 2
  417.     done
  418.  
  419.     # openvpn client network interface
  420.     dev="tun1"
  421.  
  422.     # bind PIA port forwarding service to VPN
  423.     if ! ip route | grep -Eq "^$PFS_IP\s+"; then
  424.         while ! ip route add $PFS_IP dev $dev > /dev/null 2>&1; do
  425.             # start over if openvpn process has been stopped/restarted
  426.             ps | grep "^[ ]*$curr_pid " && sleep 10 || continue 2
  427.         done
  428.  
  429.         # force routing system to recognize changes
  430.         ip route flush cache
  431.     fi
  432.  
  433.     # wait for VPN tunnel to become fully operational
  434.     while ! ping -qc1 -w3 $PFS_IP > /dev/null 2>&1; do
  435.         # start over if openvpn process has been stopped/restarted
  436.         ps | grep "^[ ]*$curr_pid " && sleep 10 || continue 2
  437.     done
  438.  
  439.     # set traps - ignore all interrupts
  440.     trap '' SIGHUP SIGINT SIGTERM
  441.  
  442.     # configure PIA port forward
  443.     if configure_port_forward; then
  444.         # determine VPN public ip
  445.         vpn_public_ip="$(get_vpn_public_ip)"
  446.  
  447.         [ $vpn_public_ip == "0.0.0.0" ] && \
  448.             echo "warning: failed to obtain vpn public ip"
  449.  
  450.         # optional: publish port forward
  451.         [ ${NOPUBLISH+x} ] || publish_port_forward
  452.  
  453.         # optional: update ddns
  454.         [[ "$DDNS_USER" && "$DDNS_PASS" ]] && update_ddns
  455.  
  456.         # optional: callback for user-defined, post-processing actions
  457.         [ -f $CALLBACK_SCRIPT ] &&
  458.             $(which nohup) $CALLBACK_SCRIPT \
  459.                 "$vpn_public_ip" "$pia_ext_port" > /dev/null 2>&1 &
  460.     fi
  461.  
  462.     # optional: limit to one pass
  463.     [ ${ONE_PASS+x} ] && { echo "info: done"; exit 0; }
  464. done
  465.  
  466. } 2>&1 | logger $([ ${DEBUG+x} ] && echo "-p user.debug") \
  467.     -t $(echo $(basename $0) | grep -Eo '^.{0,23}')[$$] &
RAW Paste Data
We use cookies for various purposes including analytics. By continuing to use Pastebin, you agree to our use of cookies as described in the Cookies Policy. OK, I Understand
 
Top