建立安全的VPN连接,不仅需要输入用户名和密码,还需要输入动态口令(token)。作为一个懒人,我更喜欢什么都不输入,只敲一个命令就直接连接上VPN那自然是极好的。本文将基于FreeOTP 支持的TOTP(Time-based One-Time Password)算法,介绍如何利用Python代码自动获取动态口令,进而利用Expect实现一个自动连接VPN的Bash脚本。
PyOTP是一套开源的函数库,可用来计算基于OTOP算法的Token。有关OTOP算法,这里不做介绍,请参考这里。
1. 下载PyOTP
huanli@ThinkPadT460:tmp$ git clone https://github.com/pyotp/pyotp.git Cloning into 'pyotp'... remote: Counting objects: 601, done. remote: Total 601 (delta 0), reused 0 (delta 0), pack-reused 601 Receiving objects: 100% (601/601), 165.02 KiB | 207.00 KiB/s, done. Resolving deltas: 100% (297/297), done. huanli@ThinkPadT460:tmp$ huanli@ThinkPadT460:tmp$ tree /tmp/pyotp/src /tmp/pyotp/src └── pyotp ├── compat.py ├── hotp.py ├── __init__.py ├── otp.py ├── totp.py └── utils.py 1 directory, 6 files huanli@ThinkPadT460:tmp$
2. 使用PyOTP
huanli@ThinkPadT460:tmp$ export PYTHONPATH=/tmp/pyotp/src:$PYTHONPATH huanli@ThinkPadT460:tmp$ python ...<snip>... >>> import base64 >>> import pyotp >>> s = 'Hello World' >>> secret = base64.b32encode(s) >>> totp = pyotp.TOTP(secret) >>> token = totp.now() >>> print token 338462 >>>
由此可见,通过pyotp.TOTP()获取token非常容易。下面给出完整的Python脚本:
- vpn_token.py
1 #!/usr/bin/python 2 import sys 3 import datetime 4 import time 5 6 def main(argc, argv): 7 if argc != 3: 8 sys.stderr.write("Usage: %s <token secret> <pyotp path>\n" % argv[0]) 9 return 1 10 11 token_secret = argv[1] 12 pyotp_path = argv[2] 13 14 sys.path.append(pyotp_path) 15 import pyotp 16 totp = pyotp.TOTP(token_secret) 17 18 # 19 # The token is expected to be valid in 5 seconds, 20 # else sleep 5s and retry 21 # 22 while True: 23 tw = datetime.datetime.now() + datetime.timedelta(seconds=5) 24 token = totp.now() 25 if totp.verify(token, tw): 26 print "%s" % token 27 return 0 28 time.sleep(5) 29 30 return 1 31 32 if __name__ == '__main__': 33 sys.exit(main(len(sys.argv), sys.argv))
- 来自Terminal的Token : 797907
- 来自手机的Token
由此可见,跟PyOTP计算出的Token码完全一致。于是,我们就可以利用Expect实现完全自动的VPN连接。例如:
- autovpn.sh
1 #!/bin/bash 2 3 # 4 # This script is to connect VPN without manually inputting password + token. 5 # 6 # Note it requires two utilities, PyOTP [1] and sexpect [2]. 7 # 8 # [1] PyOTP is a Python Python library for generating and verifying one-time 9 # passwords. Here is an example to use it, 10 # 11 # $ git clone https://github.com/pyotp/pyotp.git /tmp/pyotp 12 # $ 13 # $ export PYTHONPATH=/tmp/pyotp/src:$PYTHONPATH 14 # $ python 15 # >>> import pyotp 16 # >>> import base64 17 # >>> token_secret = base64.b32encode('Hello world') 18 # >>> totp = pyotp.TOTP(token_secret) 19 # >>> token = totp.now() 20 # >>> print token 21 # 265040 22 # >>> 23 # 24 # [2] sexpect is another Expect implementation designed in the client/server 25 # model which also supports attach/detach (like GNU screen). 26 # To use it in bash, you have to build it on your system, e.g. 27 # 28 # $ git clone https://github.com/clarkwang/sexpect.git /tmp/sexpect 29 # $ cd /tmp/sexpect 30 # $ make 31 # $ export PATH=/tmp/sexpect:$PATH 32 # $ which sexpect 33 # /tmp/sexpect/sexpect 34 # 35 # And the environment variables in the following should be set in your bashrc 36 # as well, 37 # 38 # o VPN_CONF, e.g. /etc/vpn/ovpn-pek2-tcp.conf 39 # o VPN_PASSWORD or VPN_PASSWORD_HOOK, 40 # e.g. '123456789' 41 # or ~/.vpn/passwd_hook 42 # o VPN_PYOTP_PATH, e.g. /tmp/pyotp/src 43 # o VPN_TOKEN_SECRET or VPN_TOKEN_SECRET_HOOK, 44 # e.g. 'CDOrzXyzOrzXyzOrzXyzOrzXyzOrzXyz' 45 # or ~/.vpn/tokensec_hook 46 # 47 48 function get_vpn_token 49 { 50 typeset f_py_cb=/tmp/.vpn_token.py 51 cat > $f_py_cb << EOF 52 #!/usr/bin/python 53 import sys 54 import datetime 55 import time 56 57 def main(argc, argv): 58 if argc != 3: 59 sys.stderr.write("Usage: %s <token secret> <pyotp path>\\n" % argv[0]) 60 return 1 61 62 token_secret = argv[1] 63 pyotp_path = argv[2] 64 65 sys.path.append(pyotp_path) 66 import pyotp 67 totp = pyotp.TOTP(token_secret) 68 69 # 70 # The token is expected to be valid in 5 seconds, 71 # else sleep 5s and retry 72 # 73 while True: 74 tw = datetime.datetime.now() + datetime.timedelta(seconds=5) 75 token = totp.now() 76 if totp.verify(token, tw): 77 print "%s" % token 78 return 0 79 time.sleep(5) 80 81 return 1 82 83 if __name__ == '__main__': 84 argv = sys.argv 85 argc = len(argv) 86 sys.exit(main(argc, argv)) 87 EOF 88 89 typeset pyotp_path=$VPN_PYOTP_PATH 90 typeset token_secret=$VPN_TOKEN_SECRET 91 if [[ -z $token_secret ]]; then 92 token_secret=$(eval $($VPN_TOKEN_SECRET_HOOK)) 93 fi 94 95 python $f_py_cb $token_secret $pyotp_path 96 typeset ret=$? 97 rm -f $f_py_cb 98 return $ret 99 } 100 101 function get_vpn_user 102 { 103 typeset user=${VPN_USER:-"$(id -un)"} 104 echo "$user" 105 } 106 107 function get_vpn_password 108 { 109 typeset token=$1 110 typeset password=${VPN_PASSWORD:-"$(eval $($VPN_PASSWORD_HOOK))"} 111 echo "$password$token" 112 } 113 114 function get_vpn_conf 115 { 116 typeset conf=$VPN_CONF 117 echo "$conf" 118 } 119 120 vpn_token=$(get_vpn_token) 121 vpn_user=$(get_vpn_user) 122 vpn_password=$(get_vpn_password $vpn_token) 123 vpn_conf=$(get_vpn_conf) 124 125 export SEXPECT_SOCKFILE=/tmp/sexpect-ssh-$$.sock 126 trap '{ sexpect close && sexpect wait; } > /dev/null 2>&1' EXIT 127 128 sexpect spawn sudo openvpn --config $vpn_conf 129 sexpect set -timeout 60 # XXX: 'set' should be invoked after server is running 130 131 while :; do 132 sexpect expect -nocase -re "Username:|Password:" 133 ret=$? 134 if (( $ret == 0 )); then 135 out=$(sexpect expect_out) 136 if [[ $out == *"Username:"* ]]; then 137 sexpect send -enter "$vpn_user" 138 elif [[ $out == *"Password:"* ]]; then 139 sexpect send -enter "$vpn_password" 140 break 141 else 142 echo "*** unknown catch: $out" >&2 143 exit 1 144 fi 145 elif sexpect chkerr -errno $ret -is eof; then 146 sexpect wait 147 exit 0 148 elif sexpect chkerr -errno $ret -is timeout; then 149 sexpect close 150 sexpect wait 151 echo "*** timeout waiting for password prompt" >&2 152 exit 1 153 else 154 echo "*** unknown error: $ret" >&2 155 exit 1 156 fi 157 done 158 159 sexpect interact
- 运行autovpn.sh
huanli@ThinkPadT460:~$ ./autovpn.sh Sat Aug 11 22:32:17 2018 OpenVPN 2.4.6 x86_64-redhat-linux-gnu [SSL (OpenSSL)] [LZO] [LZ4] [EPOLL] [PKCS11] [MH/PKTINFO] [AEAD] built on Apr 26 2018 Sat Aug 11 22:32:17 2018 library versions: OpenSSL 1.1.0h-fips 27 Mar 2018, LZO 2.08 Enter Auth Username: huanli Enter Auth Password: **************** Sat Aug 11 22:32:17 2018 NOTE: the current --script-security setting may allow this configuration to call user-defined scripts ...<snip>... Sat Aug 11 22:32:20 2018 GID set to openvpn Sat Aug 11 22:32:20 2018 UID set to openvpn Sat Aug 11 22:32:20 2018 Initialization Sequence Completed