├── .gitignore ├── .python-version ├── ChangeLog.md ├── LICENSE.md ├── README.md ├── develop.py ├── gunicorn.py ├── instance ├── application.cfg └── website.db ├── logs └── .gitkeep ├── packaging └── rpm │ ├── Deploy.md │ ├── flexgw.spec │ └── mkrpm.sh ├── rc ├── openvpn-client.conf ├── openvpn-client.ovpn ├── openvpn.conf └── strongswan.conf ├── requirements.txt ├── scripts ├── cert-build ├── db-manage.py ├── doctor ├── initdb.py ├── initflexgw ├── migrations │ ├── README │ ├── alembic.ini │ ├── env.py │ ├── script.py.mako │ └── versions │ │ ├── 313c830f061c_add_c2c_and_duplicate_for_dial_vpn_.py │ │ └── 45c7c3141a21_add_proto_type_settings_for_dial_vpn.py ├── openssl-1.0.0.cnf ├── openvpn-auth.py ├── packconfig └── update ├── website ├── __init__.py ├── account │ ├── __init__.py │ ├── forms.py │ ├── models.py │ ├── services.py │ ├── templates │ │ └── account │ │ │ └── login.html │ └── views.py ├── api │ ├── __init__.py │ └── views.py ├── docs │ ├── __init__.py │ ├── static │ │ └── img │ │ │ ├── dial_01.png │ │ │ ├── dial_02.png │ │ │ ├── dial_03.png │ │ │ ├── dial_04.png │ │ │ ├── dial_05.png │ │ │ ├── dial_06.png │ │ │ ├── dial_07.png │ │ │ ├── dial_08.png │ │ │ ├── dial_09.png │ │ │ ├── dial_10.png │ │ │ ├── ipsec_01.png │ │ │ ├── ipsec_02.png │ │ │ ├── ipsec_03_01.png │ │ │ ├── ipsec_03_02.png │ │ │ ├── ipsec_04.png │ │ │ ├── ipsec_05.png │ │ │ ├── ipsec_06.png │ │ │ ├── snat_01.png │ │ │ ├── snat_02.png │ │ │ └── snat_03.png │ ├── templates │ │ └── docs │ │ │ ├── certificate.html │ │ │ ├── changelog.html │ │ │ ├── debug.html │ │ │ ├── dial.html │ │ │ ├── guide.html │ │ │ ├── ipsec.html │ │ │ ├── sidenav.html │ │ │ ├── snat.html │ │ │ └── update.html │ └── views.py ├── helpers.py ├── services.py ├── snat │ ├── __init__.py │ ├── forms.py │ ├── services.py │ ├── static │ │ └── js │ │ │ └── app.js │ ├── templates │ │ ├── add.html │ │ └── index.html │ └── views.py ├── static │ ├── css │ │ ├── app.css │ │ ├── epoch.0.5.2.min.css │ │ ├── foundation-icons.css │ │ ├── foundation-icons.eot │ │ ├── foundation-icons.svg │ │ ├── foundation-icons.ttf │ │ ├── foundation-icons.woff │ │ ├── foundation.css │ │ ├── foundation.min.css │ │ └── normalize.css │ ├── img │ │ ├── bg.png │ │ ├── grey_@2X.png │ │ ├── light-grid.png │ │ └── loader.gif │ └── js │ │ ├── checkupdate.js │ │ ├── flow.js │ │ ├── foundation.min.js │ │ ├── foundation │ │ ├── foundation.abide.js │ │ ├── foundation.accordion.js │ │ ├── foundation.alert.js │ │ ├── foundation.clearing.js │ │ ├── foundation.dropdown.js │ │ ├── foundation.equalizer.js │ │ ├── foundation.interchange.js │ │ ├── foundation.joyride.js │ │ ├── foundation.js │ │ ├── foundation.magellan.js │ │ ├── foundation.offcanvas.js │ │ ├── foundation.orbit.js │ │ ├── foundation.reveal.js │ │ ├── foundation.slider.js │ │ ├── foundation.tab.js │ │ ├── foundation.tooltip.js │ │ └── foundation.topbar.js │ │ ├── vendor │ │ ├── d3.v3.min.js │ │ ├── epoch.0.5.2.min.js │ │ ├── fastclick.js │ │ ├── jquery.cookie.js │ │ ├── jquery.js │ │ ├── modernizr.js │ │ └── placeholder.js │ │ └── vpnview.js ├── templates │ ├── layout.html │ └── settings.html ├── views.py └── vpn │ ├── __init__.py │ ├── dial │ ├── __init__.py │ ├── forms.py │ ├── helpers.py │ ├── models.py │ ├── services.py │ ├── static │ │ └── openvpn-install-2.4.6-I602.exe │ ├── templates │ │ └── dial │ │ │ ├── add.html │ │ │ ├── client.conf │ │ │ ├── console.html │ │ │ ├── download.html │ │ │ ├── index.html │ │ │ ├── server.conf │ │ │ ├── settings.html │ │ │ ├── sidenav.html │ │ │ └── view.html │ └── views.py │ └── sts │ ├── __init__.py │ ├── forms.py │ ├── helpers.py │ ├── models.py │ ├── services.py │ ├── templates │ └── sts │ │ ├── add.html │ │ ├── console.html │ │ ├── flow.html │ │ ├── index.html │ │ ├── ipsec.conf │ │ ├── ipsec.secrets │ │ ├── sidenav.html │ │ └── view.html │ └── views.py ├── website_console └── websiteconfig.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | *.py[cod] 3 | 4 | # logs 5 | *.swp 6 | *.log 7 | *.pid 8 | logs/* 9 | !logs/.gitkeep 10 | instance/* 11 | !instance/application.cfg 12 | !instance/website.db 13 | 14 | # some private files 15 | artwork/ 16 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | ecs-vpn 2 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | ChangeLog 2 | ========= 3 | 4 | Version 1.1.1 5 | ------------- 6 | 7 | 完善稳定性、细节。 8 | 9 | * IPSec VPN:支持可选的IKEv2/ESP 加密算法、签名算法、DH 组。 10 | * RPM 打包,不再依赖于pyenv。 11 | * 完善Website log 日志,捕获异常消息。 12 | 13 | Version 1.1.0 14 | ------------- 15 | 16 | 更新拨号VPN 配置。 17 | 18 | * 拨号VPN:查看、修改账号时,密码默认隐藏,支持手工显示。 19 | * 拨号VPN:可配置支持客户端间相互通信、单账号同时多个客户端在线。 20 | * 拨号VPN:可配置通信协议为"UDP"或"TCP"。 21 | * 更新文档描述,增加Classic 网络环境应用场景事例。 22 | 23 | Version 1.0.0 24 | ------------- 25 | 26 | First public release. 27 | 28 | * 首次发布,支持IPSec VPN、拨号VPN、SNAT 功能。 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Software License Agreement (BSD License) 2 | ======================================== 3 | 4 | Copyright © 2015 Alibaba Group Holding Ltd. All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of test nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flex GateWay 2 | ============ 3 | 4 | 介绍 5 | ------- 6 | 7 | 本程序提供了VPN、SNAT 基础服务。 8 | 9 | 主要提供以下几点功能: 10 | 11 | 1. IPSec Site-to-Site 功能。可快速的帮助你将两个不同的VPC 私网以IPSec Site-to-Site 的方式连接起来。 12 | 2. 拨号VPN 功能。可让你通过拨号方式,接入VPC 私网,进行日常维护管理。 13 | 3. SNAT 功能。可方便的设置Source NAT,以让VPC 私网内的VM 通过Gateway VM 访问外网。 14 | 15 | 更新内容 16 | ------- 17 | 更改适配操作系统7.x,测试编译环境为centos 7.3 18 | 19 | 依赖软件包可以直接使用系统epel源安装 20 | 21 | yum install -y epel-release 22 | 23 | yum install strongswan openvpn 24 | 25 | 附上打包好的rpm包[下载地址](https://github.com/Ostaer/FlexGW/releases/download/v1.1/flexgw-1.1.0-1.el7.centos.x86_64.rpm) 26 | 27 | 自己编译 28 | ``` 29 | # yum install git 30 | # git clone https://github.com/Ostaer/FlexGW.git 31 | # yum install rpm-build python-pip zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel \ 32 | openssl-devel xz xz-devel libffi-devel gcc gcc-c++ 33 | # pip install python-build 34 | # cd FlexGW/packaging/rpm/ 35 | # sh mkrpm.sh 36 | ``` 37 | 38 | 部署参照原文档 [Deploy.md](https://github.com/Ostaer/FlexGW/blob/master/packaging/rpm/Deploy.md) 39 | 40 | 软件组成 41 | ---------- 42 | 43 | Strongswan 44 | 45 | * 版本:5.1.3 => 更新为 5.6.3 46 | * Website:http://www.strongswan.org 47 | 48 | 49 | OpenVPN 50 | 51 | * 版本:2.3.2 => 更新为 2.4.6 52 | * Website:https://openvpn.net/index.php/open-source.html 53 | 54 | 程序说明 55 | ----------- 56 | 57 | ECS VPN(即本程序) 58 | 59 | * 目录:/usr/local/flexgw 60 | * 数据库文件:/usr/local/flexgw/instance/website.db 61 | * 日志文件:/usr/local/flexgw/logs/website.log 62 | * 启动脚本:/etc/init.d/flexgw 或/usr/local/flexgw/website_console 63 | * 实用脚本:/usr/local/flexgw/scripts 64 | 65 | 保留原来的flexgw的启动方式 service flexgw start/stop/restart 66 | 67 | 「数据库文件」保存了我们所有的VPN 配置,建议定期备份。如果数据库损坏,可通过「实用脚本」目录下的initdb.py 脚本对数据库进行初始化,初始化之后所有的配置将清空。 68 | 69 | Strongswan 70 | 71 | * 目录:/etc/strongswan 72 | * 日志文件:/var/log/strongswan.charon.log 73 | * 启动脚本:/usr/sbin/strongswan 74 | 75 | 如果strongswan.conf 配置文件损坏,可使用备份文件/usr/local/flexgw/rc/strongswan.conf 进行覆盖恢复。 76 | 77 | ipsec.conf 和ipsec.secrets 配置文件,由/usr/local/flexgw/website/vpn/sts/templates/sts 目录下的同名文件自动生成,请勿随便修改。 78 | 79 | OpenVPN 80 | 81 | * 目录:/etc/openvpn 82 | * 日志文件:/etc/openvpn/openvpn.log 83 | * 状态文件:/etc/openvpn/openvpn-status.log 84 | * 原启动脚本:/etc/init.d/openvpn 85 | 86 | OpenVPN启动方式 87 | 88 | 位置变更为 /usr/lib/systemd/system/openvpn-server@.service 89 | 90 | 启动命令为systemctl start openvpn-server@server.service(会读取/etc/openvpn/server/server.conf配置文件) 91 | 92 | server.conf 配置文件,由/usr/local/flexgw/website/vpn/dial/templates/dial 目录下的同名文件自动生成,请勿随便修改。 93 | -------------------------------------------------------------------------------- /develop.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | develop 4 | ~~~~~~~ 5 | 6 | run app in debug mode. 7 | """ 8 | 9 | from website import app 10 | app.run(debug=True, host='0.0.0.0', port=2333) 11 | -------------------------------------------------------------------------------- /gunicorn.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | workers = 2 4 | bind = '0.0.0.0:443' 5 | proc_name = 'website' 6 | pidfile = '%s/website.pid' % os.path.abspath(os.path.dirname(__file__)) 7 | accesslog = '%s/logs/gunicorn-access.log' % os.path.abspath(os.path.dirname(__file__)) 8 | errorlog = '%s/logs/gunicorn-error.log' % os.path.abspath(os.path.dirname(__file__)) 9 | 10 | ca_certs = '%s/instance/ca.crt' % os.path.abspath(os.path.dirname(__file__)) 11 | certfile = '%s/instance/server.crt' % os.path.abspath(os.path.dirname(__file__)) 12 | keyfile = '%s/instance/server.key' % os.path.abspath(os.path.dirname(__file__)) 13 | -------------------------------------------------------------------------------- /instance/application.cfg: -------------------------------------------------------------------------------- 1 | DEBUG = False 2 | TESTING = False 3 | 4 | SQLALCHEMY_ECHO = False 5 | -------------------------------------------------------------------------------- /instance/website.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/instance/website.db -------------------------------------------------------------------------------- /logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/logs/.gitkeep -------------------------------------------------------------------------------- /packaging/rpm/Deploy.md: -------------------------------------------------------------------------------- 1 | Flex GateWay Deploy 2 | =================== 3 | 4 | 环境要求 5 | ------- 6 | 7 | OS: Centos 7.x 8 | 9 | 注:请以root 身份执行下面步骤的命令。 10 | 11 | 设置系统环境 12 | ---------- 13 | 14 | 编辑/etc/sysctl.conf 文件: 15 | 16 | 1. Disable redirects. 17 | 18 | `sysctl -a | egrep "ipv4.*(accept|send)_redirects" | awk -F "=" '{print $1"= 0"}'` 19 | 20 | 请编辑sysctl.conf 文件,将上面配置的值均设为0。配置文件里没有的,请添加上。 21 | 22 | 2. enable ip forward. 23 | 24 | net.ipv4.ip_forward = 1 25 | 26 | 请编辑sysctl.conf 文件,将该配置的值设置为1。 27 | 28 | 3. 执行命令`sysctl -p` 29 | 30 | 安装依赖的软件包 31 | -------------- 32 | 33 | 以root 身份执行: 34 | 35 | 1. yum install strongswan openvpn zip curl wget 36 | 37 | 安装flexgw rpm 包 38 | ---------------- 39 | 40 | 1. rpm -ivh flexgw-1.1.0-1.el7.centos.x86_64.rpm 41 | 42 | 43 | 44 | 初始化配置 45 | --------- 46 | 1. 初始化strongswan 配置文件: 47 | 48 | cp -fv /usr/local/flexgw/rc/strongswan.conf /etc/strongswan/strongswan.conf 49 | 50 | 2. 初始化openvpn 配置文件: 51 | 52 | cp -fv /usr/local/flexgw/rc/openvpn.conf /etc/openvpn/server/server.conf 53 | 54 | 设置strongswan 55 | -------------- 56 | 57 | 1. 将/etc/strongswan/strongswan.d/charon/dhcp.conf 配置文件: 58 | 注释掉“load = yes” 这行。 59 | 60 | 2. 清空密钥配置文件: 61 | 62 | \> /etc/strongswan/ipsec.secrets 63 | 64 | 65 | 测试运行strongswan 66 | ----------------- 67 | 68 | 1. strongswan start 69 | 70 | 2. strongswan status 71 | 72 | 3. strongswan stop 73 | 74 | 设置flexgw 75 | ---------- 76 | 77 | **如果只是测试的话,请不要执行此步骤。** 78 | 79 | 1. ln -s /etc/init.d/initflexgw /etc/rc3.d/S98initflexgw 80 | 2. 关机。 81 | 3. 打快照,制作为镜像。 82 | 83 | 此步骤做完之后,请不要再次开机,否则会初始化flexgw 配置文件到镜像里。 84 | 85 | 关于测试 86 | ------- 87 | 88 | 测试的话,请不要执行「设置flexgw」步骤。仅手工执行以下命令: 89 | 90 | /etc/init.d/initflexgw (喝杯茶休息下。。。) 91 | 92 | 大约10秒左右,flexgw 就会自动配置好,并启动。启动完毕之后,访问`https://公网IP` 即可看到登录界面。 93 | 94 | **测试完毕,请停止服务,并重装flexgw rpm 包:** 95 | 96 | 1. /etc/init.d/flexgw stop 97 | 2. rpm -e flexgw && rpm -rf /usr/local/flexgw/ 98 | 3. rpm -ivh flexgw-1.1.0-1.el7.centos.x86_64.rpm 99 | -------------------------------------------------------------------------------- /packaging/rpm/flexgw.spec: -------------------------------------------------------------------------------- 1 | Name: %{package_name} 2 | Version: %{version} 3 | Vendor: Flex GateWay Project 4 | Release: %{release}%{?dist} 5 | Summary: a vpn, snat web app for ecs. 6 | License: Commercial 7 | Group: Applications/Internet 8 | Source0: %{name}-%{version}.tar.bz2 9 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot 10 | 11 | AutoReqProv: no 12 | 13 | %define __os_install_post \ 14 | /usr/lib/rpm/redhat/brp-compress \ 15 | %{!?__debug_package:/usr/lib/rpm/redhat/brp-strip %{__strip}} \ 16 | /usr/lib/rpm/redhat/brp-strip-static-archive %{__strip} \ 17 | /usr/lib/rpm/redhat/brp-strip-comment-note %{__strip} %{__objdump} \ 18 | %{nil} 19 | %define debug_package %{nil} 20 | 21 | %description 22 | a vpn, snat web app for ecs vpc vm. 23 | 24 | %prep 25 | %setup -q -b 0 26 | 27 | %build 28 | # install python 29 | echo "building python..." 30 | [ -f %{python_dir} ] && rm -rf %{python_dir} 31 | export PYTHON_BUILD_BUILD_PATH=/tmp/rpmbuild/PYTHON/sources 32 | export PYTHON_BUILD_CACHE_PATH=/tmp/rpmbuild/PYTHON/cache 33 | python-build -k %{python_version} %{python_dir} 34 | 35 | # install pip requirements.txt 36 | echo "install requirements..." 37 | export ac_cv_func_malloc_0_nonnull=yes 38 | %{python_dir}/bin/pip install -r %{_builddir}/%{name}-%{version}/requirements.txt 39 | unset ac_cv_func_malloc_0_nonnull 40 | 41 | %install 42 | mkdir -p %{buildroot}/etc/init.d/ 43 | mkdir -p %{buildroot}/usr/local/flexgw/ 44 | mkdir -p %{buildroot}%{python_dir}/ 45 | 46 | mv -fv %{_builddir}/%{name}-%{version}/scripts/initflexgw %{buildroot}/etc/init.d/initflexgw 47 | cp -fv %{_builddir}/%{name}-%{version}/website_console %{buildroot}/etc/init.d/flexgw 48 | cp -rv %{_builddir}/%{name}-%{version}/* %{buildroot}/usr/local/flexgw/ 49 | cp -rv %{python_dir}/* %{buildroot}%{python_dir}/ 50 | 51 | %post 52 | # for upgrade 53 | if [ $1 -gt 1 ]; then 54 | # db migrate 55 | SEED="$(date +%%Y%%m%%d%%H%%M%%S)" 56 | cp -fv "/usr/local/flexgw/instance/website.db" "/usr/local/flexgw/instance/website.db.${SEED}" && 57 | /usr/local/flexgw/scripts/db-manage.py db upgrade --directory "/usr/local/flexgw/scripts/migrations" 1>/dev/null 2>&1 || 58 | { echo "error: upgrade db failed." 59 | echo "backup db is: /usr/local/flexgw/instance/website.db.${SEED}" 60 | exit 1 61 | } >&2 62 | # update strongswan.conf 63 | cp -fv "/etc/strongswan/strongswan.conf" "/etc/strongswan/strongswan.conf.${SEED}" && 64 | cp -fv "/usr/local/flexgw/rc/strongswan.conf" "/etc/strongswan/strongswan.conf" || 65 | { echo "error: upgrade strongswan.conf failed." 66 | echo "backup strongswan.conf is: /etc/strongswan/strongswan.conf.${SEED}" 67 | exit 1 68 | } 69 | fi 70 | 71 | %clean 72 | rm -rf $RPM_BUILD_ROOT 73 | rm -rf ${_builddir} 74 | rm -rf %{buildroot} 75 | rm -rf %{python_dir} 76 | 77 | %files 78 | %defattr(-,root,root) 79 | /usr/local/flexgw/* 80 | %attr(0755,root,root) /etc/init.d/* 81 | %attr(0755,root,root) /usr/local/flexgw/scripts/* 82 | %exclude /usr/local/flexgw/packaging 83 | %exclude /usr/local/flexgw/requirements.txt 84 | %exclude /usr/local/flexgw/develop.py 85 | %config(noreplace) /usr/local/flexgw/instance/* 86 | 87 | %changelog 88 | 89 | * Mon Mar 23 2015 xiong.xiaox - 1.1.0 90 | - Release 1.1 91 | 92 | * Thu Aug 21 2014 xiong.xiaox - 1.0.0 93 | - Release 1.0 94 | -------------------------------------------------------------------------------- /packaging/rpm/mkrpm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | #flexgw_refs="v1.0.0" 5 | flexgw_refs="origin/master" 6 | package_name="flexgw" 7 | flexgw_version="1.1.0" 8 | flexgw_release="1" 9 | python_version="2.7.9" 10 | python_dir="/usr/local/flexgw/python" 11 | 12 | curdir=$(pwd) 13 | 14 | if [ ! -f ./mkrpm.sh ]; then 15 | echo "please run this script in directory where mkrpm.sh located in" 16 | exit 1 17 | fi 18 | 19 | #create necessary directories 20 | mkdir -p /tmp/rpmbuild/SOURCES 21 | mkdir -p /tmp/rpmbuild/PYTHON/cache 22 | mkdir -p /tmp/rpmbuild/PYTHON/sources 23 | 24 | [ -d /tmp/rpmbuild/SOURCES/flexgw ] && rm -rf /tmp/rpmbuild/SOURCES/flexgw 25 | 26 | #clone repositories 27 | git clone https://github.com/Ostaer/FlexGW.git /tmp/rpmbuild/SOURCES/flexgw 28 | 29 | #archive source from git repositories 30 | cd /tmp/rpmbuild/SOURCES/flexgw 31 | git archive --format="tar" --prefix="$package_name-$flexgw_version/" $flexgw_refs|bzip2 > /tmp/rpmbuild/SOURCES/$package_name-$flexgw_version.tar.bz2 32 | 33 | # rpmbuild 34 | cd $curdir 35 | rpmbuild --define "_topdir /tmp/rpmbuild" \ 36 | --define "package_name $package_name" \ 37 | --define "version $flexgw_version" \ 38 | --define "release $flexgw_release" \ 39 | --define "python_version $python_version" \ 40 | --define "python_dir $python_dir" \ 41 | -bb $curdir/flexgw.spec 42 | 43 | rm -rf /tmp/rpmbuild/SOURCES 44 | rm -rf /tmp/rpmbuild/BUILD 45 | -------------------------------------------------------------------------------- /rc/openvpn-client.conf: -------------------------------------------------------------------------------- 1 | client 2 | dev tun 3 | proto udp 4 | 5 | # please replace EIP to your real eip address. 6 | remote EIP 1194 7 | 8 | resolv-retry infinite 9 | pull 10 | nobind 11 | persist-key 12 | persist-tun 13 | comp-lzo 14 | verb 3 15 | ca ca.crt 16 | cipher AES-128-CBC 17 | auth-user-pass 18 | -------------------------------------------------------------------------------- /rc/openvpn-client.ovpn: -------------------------------------------------------------------------------- 1 | client 2 | dev tun 3 | proto udp 4 | 5 | # please replace EIP to your real eip address. 6 | remote EIP 1194 7 | 8 | resolv-retry infinite 9 | pull 10 | nobind 11 | persist-key 12 | persist-tun 13 | comp-lzo 14 | verb 3 15 | ca ca.crt 16 | cipher AES-128-CBC 17 | auth-user-pass 18 | -------------------------------------------------------------------------------- /rc/openvpn.conf: -------------------------------------------------------------------------------- 1 | # OpenVPN 2.0 introduces a new mode ("server") 2 | # which implements a multi-client server capability. 3 | mode server 4 | 5 | # Which local IP address should OpenVPN 6 | # listen on? (optional) 7 | local 0.0.0.0 8 | 9 | # Which TCP/UDP port should OpenVPN listen on? 10 | # If you want to run multiple OpenVPN instances 11 | # on the same machine, use a different port 12 | # number for each one. You will need to 13 | # open up this port on your firewall. 14 | port 1194 15 | 16 | # TCP or UDP server? 17 | proto udp 18 | 19 | # "dev tun" will create a routed IP tunnel, 20 | # "dev tap" will create an ethernet tunnel. 21 | # Use "dev tap0" if you are ethernet bridging 22 | # and have precreated a tap0 virtual interface 23 | # and bridged it with your ethernet interface. 24 | # If you want to control access policies 25 | # over the VPN, you must create firewall 26 | # rules for the the TUN/TAP interface. 27 | # On non-Windows systems, you can give 28 | # an explicit unit number, such as tun0. 29 | # On Windows, use "dev-node" for this. 30 | # On most systems, the VPN will not function 31 | # unless you partially or fully disable 32 | # the firewall for the TUN/TAP interface. 33 | dev tun 34 | 35 | # SSL/TLS root certificate (ca), certificate 36 | # (cert), and private key (key). Each client 37 | # and the server must have their own cert and 38 | # key file. The server and all clients will 39 | # use the same ca file. 40 | # 41 | # See the "easy-rsa" directory for a series 42 | # of scripts for generating RSA certificates 43 | # and private keys. Remember to use 44 | # a unique Common Name for the server 45 | # and each of the client certificates. 46 | # 47 | # Any X509 key management system can be used. 48 | # OpenVPN can also use a PKCS #12 formatted key file 49 | # (see "pkcs12" directive in man page). 50 | ca /etc/openvpn/ca.crt 51 | cert /etc/openvpn/server.crt 52 | key /etc/openvpn/server.key # This file should be kept secret 53 | 54 | # Diffie hellman parameters. 55 | # Generate your own with: 56 | # openssl dhparam -out dh1024.pem 1024 57 | # Substitute 2048 for 1024 if you are using 58 | # 2048 bit keys. 59 | dh /etc/openvpn/dh2048.pem 60 | 61 | # Configure server mode and supply a VPN subnet 62 | # for OpenVPN to draw client addresses from. 63 | # The server will take 10.8.0.1 for itself, 64 | # the rest will be made available to clients. 65 | # Each client will be able to reach the server 66 | # on 10.8.0.1. Comment this line out if you are 67 | # ethernet bridging. See the man page for more info. 68 | server 10.8.0.0 255.255.255.0 69 | 70 | # Maintain a record of client <-> virtual IP address 71 | # associations in this file. If OpenVPN goes down or 72 | # is restarted, reconnecting clients can be assigned 73 | # the same virtual IP address from the pool that was 74 | # previously assigned. 75 | ifconfig-pool-persist ipp.txt 76 | 77 | # Push routes to the client to allow it 78 | # to reach other private subnets behind 79 | # the server. Remember that these 80 | # private subnets will also need 81 | # to know to route the OpenVPN client 82 | # address pool (10.8.0.0/255.255.255.0) 83 | # back to the OpenVPN server. 84 | 85 | # The keepalive directive causes ping-like 86 | # messages to be sent back and forth over 87 | # the link so that each side knows when 88 | # the other side has gone down. 89 | # Ping every 10 seconds, assume that remote 90 | # peer is down if no ping received during 91 | # a 60 second time period. 92 | keepalive 5 60 93 | 94 | # Select a cryptographic cipher. 95 | # This config item must be copied to 96 | # the client config file as well. 97 | cipher AES-128-CBC # AES 98 | 99 | # Enable compression on the VPN link. 100 | # If you enable it here, you must also 101 | # enable it in the client config file. 102 | comp-lzo 103 | 104 | # The persist options will try to avoid 105 | # accessing certain resources on restart 106 | # that may no longer be accessible because 107 | # of the privilege downgrade. 108 | persist-key 109 | persist-tun 110 | 111 | # Output a short status file showing 112 | # current connections, truncated 113 | # and rewritten every minute. 114 | status openvpn-status.log 1 115 | status-version 2 116 | 117 | # By default, log messages will go to the syslog (or 118 | # on Windows, if running as a service, they will go to 119 | # the "\Program Files\OpenVPN\log" directory). 120 | # Use log or log-append to override this default. 121 | # "log" will truncate the log file on OpenVPN startup, 122 | # while "log-append" will append to it. Use one 123 | # or the other (but not both). 124 | log-append openvpn.log 125 | 126 | # Set the appropriate level of log 127 | # file verbosity. 128 | # 129 | # 0 is silent, except for fatal errors 130 | # 4 is reasonable for general usage 131 | # 5 and 6 can help to debug connection problems 132 | # 9 is extremely verbose 133 | verb 3 134 | writepid /var/run/openvpn-server/server.pid 135 | 136 | # Username and Password authentication. 137 | client-cert-not-required 138 | username-as-common-name 139 | script-security 3 140 | auth-user-pass-verify /usr/local/flexgw/scripts/openvpn-auth.py via-env 141 | -------------------------------------------------------------------------------- /rc/strongswan.conf: -------------------------------------------------------------------------------- 1 | # strongswan.conf - strongSwan configuration file 2 | # 3 | # Refer to the strongswan.conf(5) manpage for details 4 | # 5 | # Configuration changes should be made in the included files 6 | 7 | charon { 8 | duplicheck.enable = yes 9 | load_modular = yes 10 | filelog { 11 | /var/log/strongswan.charon.log { 12 | append = no 13 | default = 1 14 | flush_line = yes 15 | ike_name = yes 16 | time_format = %b %e %T 17 | } 18 | } 19 | plugins { 20 | include strongswan.d/charon/*.conf 21 | stroke { 22 | timeout = 4000 23 | } 24 | } 25 | } 26 | 27 | include strongswan.d/*.conf 28 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10.1 2 | Flask-Login==0.2.11 3 | Flask-Migrate==1.2.0 4 | Flask-SQLAlchemy==1.0 5 | Flask-Script==2.0.5 6 | Flask-WTF==0.9.5 7 | Jinja2==2.7.3 8 | Mako==1.0.0 9 | MarkupSafe==0.23 10 | SQLAlchemy==0.9.6 11 | WTForms==1.0.5 12 | Werkzeug==0.9.6 13 | alembic==0.6.7 14 | blinker==1.3 15 | gunicorn==19.1.1 16 | itsdangerous==0.24 17 | simplepam==0.1.5 18 | wsgiref==0.1.2 19 | -------------------------------------------------------------------------------- /scripts/cert-build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | #DIR 4 | export HOME_DIR="$(pwd)" 5 | export KEY_DIR="$HOME_DIR/keys" 6 | export KEY_CONFIG="$HOME_DIR/openssl-1.0.0.cnf" 7 | 8 | 9 | #COMMAND 10 | export OPENSSL="openssl" 11 | export PKCS11TOOL="pkcs11-tool" 12 | export GREP="grep" 13 | 14 | #PARAMS 15 | export PKCS11_MODULE_PATH="dummy" 16 | export PKCS11_PIN="dummy" 17 | 18 | export KEY_SIZE=2048 19 | 20 | # In how many days should the root CA key expire? 21 | export CA_EXPIRE=3650 22 | 23 | # In how many days should certificates expire? 24 | export KEY_EXPIRE=3650 25 | 26 | # These are the default values for fields 27 | # which will be placed in the certificate. 28 | # Don't leave any of these fields blank. 29 | export KEY_COUNTRY="CN" 30 | export KEY_PROVINCE="ZJ" 31 | export KEY_CITY="HZ" 32 | export KEY_ORG="Flex GateWay" 33 | export KEY_EMAIL="me@myhost.mydomain" 34 | export KEY_OU="Flex GateWay" 35 | 36 | # X509 Subject Field 37 | export KEY_NAME="Flex GateWay" 38 | export KEY_CN='Flex GateWay CA' 39 | 40 | 41 | # 42 | #create dir: 43 | # 44 | 45 | if [ "$KEY_DIR" ]; then 46 | rm -rf "$KEY_DIR" 47 | mkdir "$KEY_DIR" && \ 48 | chmod go-rwx "$KEY_DIR" && \ 49 | touch "$KEY_DIR/index.txt" && \ 50 | s=$(dmidecode -s system-uuid) || s=$(cat /proc/sys/kernel/random/uuid) 51 | echo "$s" >"$KEY_DIR/serial" 52 | 53 | fi 54 | 55 | # 56 | #create ca 57 | # 58 | 59 | cd ${KEY_DIR} 60 | 61 | $OPENSSL req -batch -days $CA_EXPIRE -nodes -new -newkey rsa:$KEY_SIZE -x509 -keyout ca.key -out ca.crt -config "$KEY_CONFIG" && chmod 0600 ca.key 62 | 63 | 64 | # 65 | #create dh-param 66 | # 67 | 68 | $OPENSSL dhparam -out ${KEY_DIR}/dh${KEY_SIZE}.pem ${KEY_SIZE} 69 | 70 | # 71 | #create server certificate 72 | # 73 | 74 | export KEY_CN="$(hostname)" 75 | 76 | 77 | $OPENSSL req -batch -nodes -new -newkey rsa:$KEY_SIZE -keyout server.key -out server.csr -extensions server -config "$KEY_CONFIG" 78 | 79 | $OPENSSL ca -batch -days $KEY_EXPIRE -out server.crt -in server.csr -extensions server -config "$KEY_CONFIG" 80 | 81 | chmod 0600 server.key 82 | -------------------------------------------------------------------------------- /scripts/db-manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/flexgw/python/bin/python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | website db migrate manager 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | for manage db migrate. 8 | """ 9 | 10 | 11 | import sys 12 | import os 13 | 14 | from flask.ext.script import Manager 15 | from flask.ext.migrate import Migrate, MigrateCommand 16 | 17 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) 18 | from website import app, db 19 | 20 | 21 | migrate = Migrate(app, db) 22 | 23 | manager = Manager(app) 24 | manager.add_command('db', MigrateCommand) 25 | 26 | 27 | if __name__ == '__main__': 28 | manager.run() 29 | -------------------------------------------------------------------------------- /scripts/doctor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Summary: Verify FlexGW Dependencies. 4 | # 5 | # Usage: doctor 6 | # 7 | 8 | set -e 9 | 10 | STATUS=0 11 | 12 | # zip 13 | if ! command -v zip 1>/dev/null 2>&1; then 14 | echo -e "FlexGW: zip is not installed.\n" 1>&2 15 | STATUS=$((STATUS+1)) 16 | fi 17 | 18 | # curl 19 | if ! command -v curl 1>/dev/null 2>&1; then 20 | echo -e "FlexGW: curl is not installed.\n" 1>&2 21 | STATUS=$((STATUS+1)) 22 | fi 23 | 24 | # wget 25 | if ! command -v wget 1>/dev/null 2>&1; then 26 | echo -e "FlexGW: wget is not installed.\n" 1>&2 27 | STATUS=$((STATUS+1)) 28 | fi 29 | 30 | # dmidecode 31 | if ! command -v dmidecode 1>/dev/null 2>&1; then 32 | echo -e "FlexGW: dmidecode is not installed.\n" 1>&2 33 | STATUS=$((STATUS+1)) 34 | fi 35 | 36 | # openssl 37 | if ! command -v openssl 1>/dev/null 2>&1; then 38 | echo -e "FlexGW: openssl is not installed.\n" 1>&2 39 | STATUS=$((STATUS+1)) 40 | fi 41 | 42 | # strongswan 43 | if ! command -v strongswan 1>/dev/null 2>&1; then 44 | echo -e "FlexGW: strongswan is not installed.\n" 1>&2 45 | STATUS=$((STATUS+1)) 46 | fi 47 | 48 | # openvpn 49 | if ! command -v openvpn 1>/dev/null 2>&1; then 50 | echo -e "FlexGW: openvpn is not installed.\n" 1>&2 51 | STATUS=$((STATUS+1)) 52 | fi 53 | 54 | # redirects 55 | if sysctl -a | egrep "ipv4.*(accept|send)_redirects" | awk -F "=" '{print $2}' | grep -sqw '1' 2>/dev/null; then 56 | sysctl -a | egrep "ipv4.*(accept|send)_redirects" 1>&2 57 | echo -e "FlexGW: found error settings, please set these value to 0.\n" 1>&2 58 | STATUS=$((STATUS+1)) 59 | fi 60 | 61 | # ip_forward 62 | if sysctl -a | grep "net.ipv4.ip_forward" | awk -F "=" '{print $2}' | grep -sqw '0' 2>/dev/null; then 63 | sysctl -a | grep "net.ipv4.ip_forward" 1>&2 64 | echo -e "FlexGW: found error settings, please set this value to 1.\n" 1>&2 65 | STATUS=$((STATUS+1)) 66 | fi 67 | 68 | # result 69 | if [ "${STATUS}" == "0" ]; then 70 | echo -e "\033[0;32mCongratulations! You are ready to go!\033[0m" 71 | else 72 | { echo -e "\033[0;31m\033[0m" 73 | echo -e "\033[0;31mProblem(s) detected while checking system.\033[0m" 74 | } 1>&2 75 | fi 76 | 77 | exit "${STATUS}" 78 | -------------------------------------------------------------------------------- /scripts/initdb.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/flexgw/python/bin/python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | initdb 5 | ~~~~~~ 6 | 7 | initdb for website. 8 | """ 9 | 10 | import sys 11 | import os 12 | 13 | 14 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir))) 15 | from website import db 16 | 17 | 18 | def init_db(): 19 | '''db.drop_all() && db.create_all()''' 20 | db.drop_all() 21 | print 'Drop table ok.' 22 | db.create_all() 23 | print 'Create table ok.' 24 | print 'Done!' 25 | 26 | 27 | if __name__ == '__main__': 28 | init_db() 29 | -------------------------------------------------------------------------------- /scripts/initflexgw: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function doit { 4 | # build cert. 5 | cd /usr/local/flexgw/scripts 6 | ./cert-build 7 | cd /usr/local/flexgw/scripts/keys 8 | cp -fv ca.crt server.crt server.key dh2048.pem /etc/openvpn/ 9 | cp -fv ca.crt server.crt server.key /usr/local/flexgw/instance/ 10 | # init strongswan & openvpn config files. 11 | cp -fv /usr/local/flexgw/rc/strongswan.conf /etc/strongswan/strongswan.conf 12 | cp -fv /usr/local/flexgw/rc/openvpn.conf /etc/openvpn/server/server.conf 13 | # packaging openvpn client config files. 14 | cd /usr/local/flexgw/website/vpn/dial/static 15 | zip -qj windows-openvpn-client.zip /etc/openvpn/ca.crt /usr/local/flexgw/rc/openvpn-client.ovpn 16 | tar -czvf linux-openvpn-client.tar.gz -C /etc/openvpn ca.crt -C /usr/local/flexgw/rc openvpn-client.conf 17 | # setting flask website SECRET_KEY 18 | echo "SECRET_KEY = '$(head -c 32 /dev/urandom | base64 | head -c 32)'" >> /usr/local/flexgw/instance/application.cfg 19 | # add 443/tcp to allow 20 | firewall-cmd --permanent --add-port=443/tcp 21 | firewall-cmd --reload 22 | # set auto start 23 | chkconfig flexgw on 24 | # starting website 25 | /etc/init.d/flexgw start 26 | } 27 | 28 | doit 29 | rm -rf /etc/rc3.d/S98initflexgw 30 | -------------------------------------------------------------------------------- /scripts/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /scripts/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /scripts/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | 6 | # this is the Alembic Config object, which provides 7 | # access to the values within the .ini file in use. 8 | config = context.config 9 | 10 | # Interpret the config file for Python logging. 11 | # This line sets up loggers basically. 12 | fileConfig(config.config_file_name) 13 | 14 | # add your model's MetaData object here 15 | # for 'autogenerate' support 16 | # from myapp import mymodel 17 | # target_metadata = mymodel.Base.metadata 18 | from flask import current_app 19 | config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI')) 20 | target_metadata = current_app.extensions['migrate'].db.metadata 21 | 22 | # other values from the config, defined by the needs of env.py, 23 | # can be acquired: 24 | # my_important_option = config.get_main_option("my_important_option") 25 | # ... etc. 26 | 27 | def run_migrations_offline(): 28 | """Run migrations in 'offline' mode. 29 | 30 | This configures the context with just a URL 31 | and not an Engine, though an Engine is acceptable 32 | here as well. By skipping the Engine creation 33 | we don't even need a DBAPI to be available. 34 | 35 | Calls to context.execute() here emit the given string to the 36 | script output. 37 | 38 | """ 39 | url = config.get_main_option("sqlalchemy.url") 40 | context.configure(url=url) 41 | 42 | with context.begin_transaction(): 43 | context.run_migrations() 44 | 45 | def run_migrations_online(): 46 | """Run migrations in 'online' mode. 47 | 48 | In this scenario we need to create an Engine 49 | and associate a connection with the context. 50 | 51 | """ 52 | engine = engine_from_config( 53 | config.get_section(config.config_ini_section), 54 | prefix='sqlalchemy.', 55 | poolclass=pool.NullPool) 56 | 57 | connection = engine.connect() 58 | context.configure( 59 | connection=connection, 60 | target_metadata=target_metadata 61 | ) 62 | 63 | try: 64 | with context.begin_transaction(): 65 | context.run_migrations() 66 | finally: 67 | connection.close() 68 | 69 | if context.is_offline_mode(): 70 | run_migrations_offline() 71 | else: 72 | run_migrations_online() 73 | 74 | -------------------------------------------------------------------------------- /scripts/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = ${repr(up_revision)} 11 | down_revision = ${repr(down_revision)} 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | ${imports if imports else ""} 16 | 17 | def upgrade(): 18 | ${upgrades if upgrades else "pass"} 19 | 20 | 21 | def downgrade(): 22 | ${downgrades if downgrades else "pass"} 23 | -------------------------------------------------------------------------------- /scripts/migrations/versions/313c830f061c_add_c2c_and_duplicate_for_dial_vpn_.py: -------------------------------------------------------------------------------- 1 | """add c2c and duplicate for dial vpn settings. 2 | 3 | Revision ID: 313c830f061c 4 | Revises: None 5 | Create Date: 2014-09-28 10:56:29.542570 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '313c830f061c' 11 | down_revision = None 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('dial_settings', sa.Column('c2c', sa.Boolean(), nullable=True)) 20 | op.add_column('dial_settings', sa.Column('duplicate', sa.Boolean(), nullable=True)) 21 | ### end Alembic commands ### 22 | 23 | 24 | def downgrade(): 25 | ### commands auto generated by Alembic - please adjust! ### 26 | op.drop_column('dial_settings', 'duplicate') 27 | op.drop_column('dial_settings', 'c2c') 28 | ### end Alembic commands ### 29 | -------------------------------------------------------------------------------- /scripts/migrations/versions/45c7c3141a21_add_proto_type_settings_for_dial_vpn.py: -------------------------------------------------------------------------------- 1 | """add proto type settings for dial vpn. 2 | 3 | Revision ID: 45c7c3141a21 4 | Revises: 313c830f061c 5 | Create Date: 2014-10-10 10:22:23.395475 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '45c7c3141a21' 11 | down_revision = '313c830f061c' 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | 16 | 17 | def upgrade(): 18 | ### commands auto generated by Alembic - please adjust! ### 19 | op.add_column('dial_settings', sa.Column('proto', sa.String(length=80), nullable=True)) 20 | ### end Alembic commands ### 21 | 22 | 23 | def downgrade(): 24 | ### commands auto generated by Alembic - please adjust! ### 25 | op.drop_column('dial_settings', 'proto') 26 | ### end Alembic commands ### 27 | -------------------------------------------------------------------------------- /scripts/openvpn-auth.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/flexgw/python/bin/python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | openvpn-auth 5 | ~~~~~~~~~~~~ 6 | 7 | openvpn account auth scripts. 8 | """ 9 | 10 | 11 | import os 12 | import re 13 | import sys 14 | import sqlite3 15 | 16 | 17 | DATABASE = '%s/instance/website.db' % os.path.abspath(os.path.join(os.path.dirname(__file__), os.path.pardir)) 18 | 19 | 20 | def __query_db(query, args=(), one=False): 21 | conn = sqlite3.connect(DATABASE) 22 | cur = conn.cursor() 23 | cur.row_factory = sqlite3.Row 24 | cur = cur.execute(query, args) 25 | rv = cur.fetchall() 26 | cur.close() 27 | return (rv[0] if rv else None) if one else rv 28 | 29 | 30 | def _auth(name, password): 31 | regex = re.compile(r'^[\w]+$', 0) 32 | if not regex.match(name) or not regex.match(password): 33 | sys.exit(1) 34 | account = __query_db('select * from dial_account where name = ?', [name], one=True) 35 | if account is None: 36 | sys.exit(1) 37 | elif account['password'] == password: 38 | sys.exit(0) 39 | sys.exit(1) 40 | 41 | 42 | if __name__ == '__main__': 43 | _auth(os.environ['username'], os.environ['password']) 44 | -------------------------------------------------------------------------------- /scripts/packconfig: -------------------------------------------------------------------------------- 1 | #!/bin/env bash 2 | # 3 | # use to pack openvpn client config files. 4 | # 5 | 6 | 7 | static_dir="/usr/local/flexgw/website/vpn/dial/static" 8 | 9 | usage() 10 | { 11 | echo "Usage: ${0##.*/} " 12 | exit 1 13 | } 14 | 15 | pack_openvpn_linux_conf_file() 16 | { 17 | tar -czvf linux-openvpn-client.tar.gz -C /etc/openvpn ca.crt -C /usr/local/flexgw/rc openvpn-client.conf 18 | if (( $? != 0)); then 19 | echo "packconfig: pack openvpn linux conf file failed." 20 | return 1 21 | fi 22 | echo "packconfig: linux config files pack ok." 23 | } 24 | 25 | pack_openvpn_windows_conf_file() 26 | { 27 | zip -qj windows-openvpn-client.zip /etc/openvpn/ca.crt /usr/local/flexgw/rc/openvpn-client.ovpn 28 | if (( $? != 0)); then 29 | echo "packconfig: pack openvpn windows conf file failed." 30 | return 1 31 | fi 32 | echo "packconfig: windows config files pack ok." 33 | } 34 | 35 | cd "$static_dir" 2>/dev/null || { 36 | echo "packconfig: cannot change working directory to \`$static_dir'" 37 | exit 1 38 | } >&2 39 | 40 | sed -e 's/$/\r/' /usr/local/flexgw/rc/openvpn-client.conf > /usr/local/flexgw/rc/openvpn-client.ovpn 41 | 42 | command="$1" 43 | case "$command" in 44 | "" | "-h" | "--help" ) 45 | usage >&2 46 | ;; 47 | "windows" ) 48 | pack_openvpn_windows_conf_file 49 | ;; 50 | "linux" ) 51 | pack_openvpn_linux_conf_file 52 | ;; 53 | "all" ) 54 | pack_openvpn_windows_conf_file 55 | pack_openvpn_linux_conf_file 56 | esac 57 | exit $? 58 | -------------------------------------------------------------------------------- /scripts/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -E 4 | 5 | file_is_not_empty() { 6 | local filename="$1" 7 | local line_count="$(wc -l "$filename" 2>/dev/null || true)" 8 | 9 | if [ -n "$line_count" ]; then 10 | words=( $line_count ) 11 | [ "${words[0]}" -gt 0 ] 12 | else 13 | return 1 14 | fi 15 | } 16 | 17 | update_failed() { 18 | { echo 19 | echo "UPDATE FAILED" 20 | echo 21 | 22 | if ! rmdir "${DOWNLOAD_PATH}" 2>/dev/null; then 23 | echo "Inspect or clean up the working tree at ${DOWNLOAD_PATH}" 24 | 25 | if file_is_not_empty "$LOG_PATH"; then 26 | echo "Results logged to ${LOG_PATH}" 27 | echo 28 | echo "Last 10 log lines:" 29 | tail -n 10 "$LOG_PATH" 30 | fi 31 | fi 32 | } >&2 33 | exit 1 34 | } 35 | 36 | compute_sha2() { 37 | local output 38 | if type shasum &>/dev/null; then 39 | output="$(shasum -a 256 -b)" || return 1 40 | echo "${output% *}" 41 | elif type openssl &>/dev/null; then 42 | output="$(openssl dgst -sha256)" || return 1 43 | echo "${output##* }" 44 | elif type sha256sum &>/dev/null; then 45 | output="$(sha256sum --quiet)" || return 1 46 | echo "${output% *}" 47 | else 48 | return 1 49 | fi 50 | } 51 | 52 | compute_md5() { 53 | local output 54 | if type md5 &>/dev/null; then 55 | md5 -q 56 | elif type openssl &>/dev/null; then 57 | output="$(openssl md5)" || return 1 58 | echo "${output##* }" 59 | elif type md5sum &>/dev/null; then 60 | output="$(md5sum -b)" || return 1 61 | echo "${output% *}" 62 | else 63 | return 1 64 | fi 65 | } 66 | 67 | verify_checksum() { 68 | local checksum_command="compute_sha2" 69 | # If the specified filename doesn't exist, return success 70 | local filename="$1" 71 | [ -e "$filename" ] || return 0 72 | 73 | # If there's no expected checksum, return success 74 | local expected_checksum=`echo "$2" | tr [A-Z] [a-z]` 75 | [ -n "$expected_checksum" ] || return 0 76 | 77 | # If the checksum length is 32 chars, assume MD5, otherwise SHA2 78 | if [ "${#expected_checksum}" -eq 32 ]; then 79 | checksum_command="compute_md5" 80 | fi 81 | 82 | # If the computed checksum is empty, return failure 83 | local computed_checksum=`echo "$($checksum_command < "$filename")" | tr [A-Z] [a-z]` 84 | [ -n "$computed_checksum" ] || return 1 85 | 86 | if [ "$expected_checksum" != "$computed_checksum" ]; then 87 | { echo 88 | echo "checksum mismatch: ${filename} (file is corrupt)" 89 | echo "expected $expected_checksum, got $computed_checksum" 90 | echo 91 | } >&4 92 | return 1 93 | fi 94 | } 95 | 96 | http() { 97 | local method="$1" 98 | local url="$2" 99 | local file="$3" 100 | [ -n "$url" ] || return 1 101 | 102 | if type curl &>/dev/null; then 103 | "http_${method}_curl" "$url" "$file" 104 | elif type wget &>/dev/null; then 105 | "http_${method}_wget" "$url" "$file" 106 | else 107 | echo "error: please install \`curl\` or \`wget\` and try again" >&2 108 | exit 1 109 | fi 110 | } 111 | 112 | http_head_curl() { 113 | curl -qsILf "$1" >&4 2>&1 114 | } 115 | 116 | http_get_curl() { 117 | curl -q -o "${2:--}" -sSLf "$1" 118 | } 119 | 120 | http_head_wget() { 121 | wget -q --spider "$1" >&4 2>&1 122 | } 123 | 124 | http_get_wget() { 125 | wget -nv -O "${2:--}" "$1" 126 | } 127 | 128 | fetch_version_file() { 129 | local file_name="$1" 130 | local file_url="$2" 131 | local mirror_url 132 | 133 | local version_filename=$(basename $file_url) 134 | if [ -n "$MIRROR_URL" ]; then 135 | mirror_url="${MIRROR_URL}/$version_filename" 136 | fi 137 | 138 | echo "Downloading $version_filename..." >&2 139 | http head "$mirror_url" && 140 | download_file "$mirror_url" "$file_name" || 141 | download_file "$file_url" "$file_name" 142 | } 143 | 144 | fetch_rpm() { 145 | local package_name="$1" 146 | local package_url="$2" 147 | local mirror_url 148 | local checksum 149 | 150 | if [ "$package_url" != "${package_url/\#}" ]; then 151 | checksum="${package_url#*#}" 152 | package_url="${package_url%%#*}" 153 | 154 | if [ -n "$MIRROR_URL" ]; then 155 | mirror_url="${MIRROR_URL}/$(basename $package_url)" 156 | fi 157 | fi 158 | 159 | echo "Downloading ${package_name}..." >&2 160 | http head "$mirror_url" && 161 | download_file "$mirror_url" "$package_name" "$checksum" || 162 | download_file "$package_url" "$package_name" "$checksum" 163 | } 164 | 165 | download_file() { 166 | local package_url="$1" 167 | [ -n "$package_url" ] || return 1 168 | 169 | local package_filename="$2" 170 | local checksum="$3" 171 | 172 | echo "-> $package_url" >&2 173 | 174 | if http get "$package_url" "$package_filename" >&4 2>&1; then 175 | verify_checksum "$package_filename" "$checksum" >&4 2>&1 || return 1 176 | else 177 | echo "error: failed to download $package_filename" >&2 178 | return 1 179 | fi 180 | } 181 | 182 | upgrade() { 183 | pushd "$DOWNLOAD_PATH" >&4 184 | package_url=$(head -n 1 $VERSION_FILENAME) 185 | package_info=$(basename $package_url) 186 | package_name="${package_info%#*}" 187 | fetch_rpm $package_name $package_url 188 | rpm -Uvh $package_name || 189 | { echo "error: failed to rpm upgrade." 190 | exit 1 191 | } >&2 192 | popd >&4 193 | } 194 | 195 | check() { 196 | local current_package="$(rpm -q flexgw)" 197 | [ -n "$current_package" ] || 198 | { echo "error: failed to get installed flexgw package info." 199 | exit 1 200 | } >&2 201 | 202 | pushd "$DOWNLOAD_PATH" >&4 203 | fetch_version_file "$VERSION_FILENAME" "$MASTER_URL/$VERSION_FILENAME" || update_failed 204 | package_url=$(head -n 1 $VERSION_FILENAME) 205 | popd >&4 206 | 207 | package_name=$(basename $package_url) 208 | version="${package_name%.rpm*}" 209 | if [[ "x$version" > "x$current_package" ]]; then 210 | echo "Found new version: $version" 211 | return 1 212 | else 213 | echo "Already latest version." 214 | return 0 215 | fi 216 | } 217 | 218 | usage() { 219 | { echo "usage: update [-y|--yes] [-c|--check]" 220 | echo " update --check" 221 | } >&2 222 | 223 | if [ -z "$1" ]; then 224 | exit 1 225 | fi 226 | } 227 | 228 | if [ -z "$TMPDIR" ]; then 229 | TMP="/tmp" 230 | else 231 | TMP="${TMPDIR%/}" 232 | fi 233 | 234 | if [ ! -w "$TMP" ] || [ ! -x "$TMP" ]; then 235 | echo "update: TMPDIR=$TMP is set to a non-accessible location" >&2 236 | exit 1 237 | fi 238 | 239 | if [ -z "$MASTER_URL" ]; then 240 | MASTER_URL="http://mirrors.aliyun.com/sre/flexgw" 241 | else 242 | MASTER_URL="${MASTER_URL%/}" 243 | fi 244 | 245 | if [ -z "$MIRROR_URL" ]; then 246 | MIRROR_URL="http://mirrors.aliyuncs.com/sre/flexgw" 247 | else 248 | MIRROR_URL="${MIRROR_URL%/}" 249 | fi 250 | 251 | SEED="$(date "+%Y%m%d%H%M%S").$$" 252 | DOWNLOAD_PATH="${TMP}/flexgw-update.${SEED}" 253 | LOG_PATH="${TMP}/flexgw-update.${SEED}.log" 254 | 255 | exec 4<> "$LOG_PATH" # open the log file at fd 4 256 | 257 | trap update_failed ERR 258 | mkdir -p "$DOWNLOAD_PATH" 259 | VERSION_FILENAME="latest.txt" 260 | command="$1" 261 | case "$command" in 262 | "" | "-h" | "--help" ) 263 | usage without_exiting >&2 264 | ;; 265 | "-y" | "--yes" ) 266 | check || upgrade 267 | ;; 268 | "-c" | "--check" ) 269 | check || true 270 | esac 271 | e_c="$?" 272 | [ -z "${KEEP_DOWNLOAD_PATH}" ] && rm -fr "$DOWNLOAD_PATH" 273 | exit $e_c 274 | trap - ERR 275 | 276 | -------------------------------------------------------------------------------- /website/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website 4 | ~~~~~~~ 5 | 6 | ECS VPN Website. 7 | """ 8 | 9 | __version__ = '1.1.1' 10 | 11 | import os 12 | 13 | from flask import Flask 14 | app = Flask(__name__, instance_relative_config=True) 15 | app.config.from_object('websiteconfig.default_settings') 16 | app.config.from_pyfile('application.cfg', silent=True) 17 | 18 | import logging 19 | from logging import Formatter 20 | from logging.handlers import TimedRotatingFileHandler 21 | website_log = '%s/logs/website.log' % os.path.abspath(os.path.join(os.path.dirname(__file__), 22 | os.path.pardir)) 23 | file_handler = TimedRotatingFileHandler(website_log, 24 | 'W0', 1, backupCount=7) 25 | file_handler.suffix = '%Y%m%d-%H%M' 26 | file_handler.setLevel(logging.INFO) 27 | file_handler.setFormatter(Formatter('%(asctime)s %(levelname)s: %(message)s')) 28 | app.logger.addHandler(file_handler) 29 | app.logger.setLevel(logging.INFO) 30 | 31 | from flask import request_started, got_request_exception 32 | from website.helpers import log_request, log_exception 33 | request_started.connect(log_request, app) 34 | got_request_exception.connect(log_exception, app) 35 | 36 | from flask.ext.sqlalchemy import SQLAlchemy 37 | db = SQLAlchemy(app) 38 | 39 | from flask.ext.login import LoginManager 40 | login_manager = LoginManager() 41 | login_manager.init_app(app) 42 | 43 | from flask_wtf.csrf import CsrfProtect 44 | CsrfProtect(app) 45 | 46 | import website.views 47 | 48 | from website.account.views import account 49 | from website.vpn.sts.views import sts 50 | from website.vpn.dial.views import dial 51 | from website.snat.views import snat 52 | from website.api.views import api 53 | from website.docs.views import docs 54 | app.register_blueprint(account) 55 | app.register_blueprint(sts) 56 | app.register_blueprint(dial) 57 | app.register_blueprint(snat) 58 | app.register_blueprint(api) 59 | app.register_blueprint(docs) 60 | -------------------------------------------------------------------------------- /website/account/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.account 4 | ~~~~~~~~~~~~~~~ 5 | 6 | website account blueprint. 7 | """ 8 | 9 | from website import login_manager 10 | from website.account.services import load_user 11 | 12 | 13 | login_manager.login_view = "account.login" 14 | login_manager.login_message = None 15 | -------------------------------------------------------------------------------- /website/account/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.account.forms 4 | ~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | account forms: 7 | /login 8 | /settings 9 | """ 10 | 11 | 12 | from flask_wtf import Form 13 | from wtforms import TextField, PasswordField, ValidationError 14 | from wtforms.validators import DataRequired, Length 15 | 16 | from website.account.models import User 17 | 18 | 19 | class LoginForm(Form): 20 | account = TextField(u'Account', validators=[DataRequired(message=u'这是一个必选项!'), 21 | Length(max=20, message=u'帐号最长为20个字符!')]) 22 | password = PasswordField(u'Password', validators=[DataRequired(message=u'这是一个必选项!')]) 23 | 24 | def validate_password(self, field): 25 | account = self.account.data 26 | if not User.check_auth(account, field.data): 27 | raise ValidationError(u'无效的帐号或密码!') 28 | -------------------------------------------------------------------------------- /website/account/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.account.models 4 | ~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | account system models. 7 | """ 8 | 9 | 10 | import sys 11 | 12 | from simplepam import authenticate 13 | 14 | from flask import current_app 15 | 16 | from website.services import exec_command 17 | 18 | 19 | class User(object): 20 | 21 | id = None 22 | username = None 23 | 24 | def __init__(self, id, username): 25 | self.id = id 26 | self.username = username 27 | 28 | def __repr__(self): 29 | return ''.format(self.id, self.username) 30 | 31 | def is_active(self): 32 | return True 33 | 34 | def is_authenticated(self): 35 | return True 36 | 37 | def is_anonymous(self): 38 | return False 39 | 40 | def get_id(self): 41 | return unicode(self.id) 42 | 43 | @classmethod 44 | def query_filter_by(cls, id=None, username=None): 45 | if id: 46 | cmd = ['getent', 'passwd', str(id)] 47 | elif username: 48 | cmd = ['id', '-u', str(username)] 49 | else: 50 | return None 51 | try: 52 | r = exec_command(cmd) 53 | except: 54 | current_app.logger.error('[Account System]: exec_command error: %s:%s', cmd, 55 | sys.exc_info()[1]) 56 | return None 57 | if r['return_code'] != 0: 58 | current_app.logger.error('[Account System]: exec_command return: %s:%s:%s', 59 | cmd, r['return_code'], r['stderr']) 60 | return None 61 | if id: 62 | username = r['stdout'].split(':')[0] 63 | return cls(id, username) 64 | if username: 65 | id = int(r['stdout']) 66 | return cls(id, username) 67 | 68 | @classmethod 69 | def check_auth(cls, username, password): 70 | r = authenticate(str(username), str(password), service='sshd') 71 | if not r: 72 | current_app.logger.error('[Account System]: %s pam auth failed return: %s', 73 | username, r) 74 | return r 75 | -------------------------------------------------------------------------------- /website/account/services.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.account.services 4 | ~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | account login validate. 7 | """ 8 | 9 | 10 | from website import login_manager 11 | from website.account.models import User 12 | 13 | 14 | @login_manager.user_loader 15 | def load_user(user_id): 16 | return User.query_filter_by(id=user_id) 17 | -------------------------------------------------------------------------------- /website/account/templates/account/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Login{% endblock title %} 3 | 4 | {% block header_bar %} 5 | {% endblock header_bar %} 6 | 7 | {% block header %} 8 | {% endblock header %} 9 | 10 | {% block trail_all %} 11 | {% endblock trail_all %} 12 | 13 | {% block content %} 14 | 15 |
16 |
17 |

GateWay Login

18 |
19 |
20 |
21 |
22 | {{ form.account.label(class="right inline") }} 23 |
24 |
25 | {% if form.account.errors %} 26 | {{ form.account(class="error", value=form.account.value) }} 27 | {{ form.account.errors[0] }} 28 | {% else %} 29 | {{ form.account(placeholder="user") }} 30 | {% endif %} 31 |
32 |
33 |
34 |
35 | {{ form.password.label(class="right inline") }} 36 |
37 |
38 | {% if form.password.errors %} 39 | {{ form.password(class="error", value=form.password.value) }} 40 | {{ form.password.errors[0] }} 41 | {% else %} 42 | {{ form.password(placeholder="password") }} 43 | {% endif %} 44 |
45 |
46 |
47 |
48 | 49 | 50 |
51 |
52 |
53 |
54 | 55 | {% endblock content %} 56 | 57 | 58 | -------------------------------------------------------------------------------- /website/account/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.account.views 4 | ~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | account views: 7 | /login 8 | /logout 9 | """ 10 | 11 | from flask import Blueprint, render_template 12 | from flask import url_for, session, redirect, request 13 | 14 | from website.account.models import User 15 | from website.account.forms import LoginForm 16 | 17 | from flask.ext.login import login_required, logout_user, login_user 18 | 19 | 20 | account = Blueprint('account', __name__, 21 | template_folder='templates') 22 | 23 | 24 | @account.route('/login', methods=['GET', 'POST']) 25 | def login(): 26 | form = LoginForm() 27 | if form.validate_on_submit(): 28 | user = User.query_filter_by(username=form.account.data) 29 | login_user(user) 30 | return redirect(request.args.get("next") or url_for('default')) 31 | return render_template('account/login.html', form=form) 32 | 33 | 34 | @account.route('/logout') 35 | @login_required 36 | def logout(): 37 | # Remove the user information from the session 38 | logout_user() 39 | # Remove session 40 | session.clear() 41 | return redirect(url_for("account.login")) 42 | -------------------------------------------------------------------------------- /website/api/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.api 4 | ~~~~~~~~~~~ 5 | 6 | website api blueprint. 7 | """ 8 | -------------------------------------------------------------------------------- /website/api/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.api.views 4 | ~~~~~~~~~~~~~~~~~ 5 | 6 | vpn api views: 7 | /api/* 8 | """ 9 | 10 | 11 | import sys 12 | 13 | from flask import Blueprint, jsonify, current_app 14 | 15 | from flask.ext.login import login_required 16 | 17 | from website.services import exec_command 18 | from website.vpn.sts.services import VpnServer 19 | 20 | 21 | api = Blueprint('api', __name__, url_prefix='/api') 22 | 23 | 24 | @api.route('/vpn//traffic/now') 25 | @login_required 26 | def vpn_traffic(tunnel_name): 27 | vpn = VpnServer() 28 | return jsonify(vpn.tunnel_traffic(tunnel_name) or []) 29 | 30 | 31 | @api.route('/vpn//up') 32 | @login_required 33 | def tunnel_up(tunnel_name): 34 | vpn = VpnServer() 35 | return jsonify({'result': vpn.tunnel_up(tunnel_name), 'stdout': vpn.c_stdout}) 36 | 37 | 38 | @api.route('/checkupdate') 39 | def check_update(): 40 | cmd = ['/usr/local/flexgw/scripts/update', '--check'] 41 | try: 42 | r = exec_command(cmd, timeout=10) 43 | except: 44 | current_app.logger.error('[API]: exec_command error: %s:%s', cmd, 45 | sys.exc_info()[1]) 46 | return jsonify({"message": u"执行命令:`/usr/local/flexgw/scripts/update --check' 失败!"}), 500 47 | if r['return_code'] != 0: 48 | current_app.logger.error('[API]: exec_command return: %s:%s:%s', cmd, 49 | r['return_code'], r['stderr']) 50 | return jsonify({"message": u"检查更新失败,请手工执行命令:`/usr/local/flexgw/scripts/update --check'"}), 504 51 | for line in r['stdout'].split('\n'): 52 | if ' new ' in line: 53 | info = u"发现新版本:%s!" % (line.split(':')[1]) 54 | return jsonify({"message": info}) 55 | return jsonify({"message": u"已经是最新版本了!"}), 404 56 | -------------------------------------------------------------------------------- /website/docs/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.docs 4 | ~~~~~~~~~~~~ 5 | 6 | website docs blueprint. 7 | """ 8 | -------------------------------------------------------------------------------- /website/docs/static/img/dial_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/dial_01.png -------------------------------------------------------------------------------- /website/docs/static/img/dial_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/dial_02.png -------------------------------------------------------------------------------- /website/docs/static/img/dial_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/dial_03.png -------------------------------------------------------------------------------- /website/docs/static/img/dial_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/dial_04.png -------------------------------------------------------------------------------- /website/docs/static/img/dial_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/dial_05.png -------------------------------------------------------------------------------- /website/docs/static/img/dial_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/dial_06.png -------------------------------------------------------------------------------- /website/docs/static/img/dial_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/dial_07.png -------------------------------------------------------------------------------- /website/docs/static/img/dial_08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/dial_08.png -------------------------------------------------------------------------------- /website/docs/static/img/dial_09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/dial_09.png -------------------------------------------------------------------------------- /website/docs/static/img/dial_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/dial_10.png -------------------------------------------------------------------------------- /website/docs/static/img/ipsec_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/ipsec_01.png -------------------------------------------------------------------------------- /website/docs/static/img/ipsec_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/ipsec_02.png -------------------------------------------------------------------------------- /website/docs/static/img/ipsec_03_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/ipsec_03_01.png -------------------------------------------------------------------------------- /website/docs/static/img/ipsec_03_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/ipsec_03_02.png -------------------------------------------------------------------------------- /website/docs/static/img/ipsec_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/ipsec_04.png -------------------------------------------------------------------------------- /website/docs/static/img/ipsec_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/ipsec_05.png -------------------------------------------------------------------------------- /website/docs/static/img/ipsec_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/ipsec_06.png -------------------------------------------------------------------------------- /website/docs/static/img/snat_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/snat_01.png -------------------------------------------------------------------------------- /website/docs/static/img/snat_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/snat_02.png -------------------------------------------------------------------------------- /website/docs/static/img/snat_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/docs/static/img/snat_03.png -------------------------------------------------------------------------------- /website/docs/templates/docs/certificate.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Docs{% endblock %} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • Docs
  • 7 |
  • 关于VPN 证书
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "docs/sidenav.html" %} 12 | 13 | 14 |
    15 |
    16 |
    17 |
    18 |

    关于VPN 证书

    19 |
    20 |

    考虑到证书的唯一性、安全性,我们的证书文件是VM 第一次启动时,通过脚本自动生成的,其原始目录为:/usr/local/flexgw/scripts/keys,下面所提到的证书文件,均为该目录下的证书文件的拷贝。

    21 |

    IPSec Site-to-Site 隧道:

    22 |

    IPSec Site-to-Site VPN 采用的时PSK 方式加密连接,并不使用证书认证。

    23 |

    拨号VPN:

    24 |

    拨号VPN 的证书,位于/etc/openvpn 下:

    25 |
      26 |
    1. ca.crt:是根证书,服务器和客户端都需要保存。
    2. 27 |
    3. server.crt:是服务器的证书,由CA 证书进行签名。
    4. 28 |
    5. server.key:是服务器的证书对应秘钥。
    6. 29 |
    7. dh1024.pem:DH 算法参数文件。
    8. 30 |
    31 |

    网站HTTPS 证书:

    32 |

    网站的证书,位于/usr/local/flexgw/instance 目录下:

    33 |
      34 |
    1. ca.crt:是根证书。
    2. 35 |
    3. server.crt:是服务器的证书,由CA 证书进行签名。
    4. 36 |
    5. server.key:是服务器的证书对应秘钥。
    6. 37 |
    38 |

    使用自己的证书

    39 |
    40 |

    如果你希望使用自己的证书,可以使用openssl命令生成自己的根证书、服务器证书、以及DH算法参数文件。

    41 |

    拨号VPN:

    42 |
      43 |
    1. 将生成的证书文件,拷贝到在/etc/openvpn 目录下替换掉相应的证书文件。
    2. 44 |
    3. 客户端也要使用新的CA 证书来替换掉原来的根证书。可通过/usr/local/flexgw/scripts/packconfig 脚本,重新打包openvpn client 配置文件,打包后的配置文件位于:/usr/local/flexgw/website/vpn/dial/static 目录下。请将配置文件,重新分发给客户端。
    4. 45 |
    5. 通过「VPN 服务管理」页面重启拨号VPN 服务。
    6. 46 |
    47 |

    网站HTTPS 证书:

    48 |
      49 |
    1. 将生成的证书文件,拷贝到在/usr/local/flexgw/instance 目录下替换掉相应的证书文件。
    2. 50 |
    3. 重启网站:/etc/init.d/flexgw restart
    4. 51 |
    52 |
    53 |
    54 |
    55 |
    56 | 57 | {% endblock content %} 58 | -------------------------------------------------------------------------------- /website/docs/templates/docs/changelog.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Docs{% endblock %} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • Docs
  • 7 |
  • ChangeLog
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "docs/sidenav.html" %} 12 | 13 | 14 |
    15 |
    16 |
    17 |
    18 |

    ChangeLog

    19 |
    20 |
    1.1.0 - 2014.10.11
    21 |
      22 |
    • 拨号VPN:查看、修改账号时,密码默认隐藏,支持手工显示。
    • 23 |
    • 拨号VPN:可配置支持客户端间相互通信、单账号同时多个客户端在线。
    • 24 |
    • 拨号VPN:可配置通信协议为"UDP"或"TCP"。
    • 25 |
    • 更新文档描述,增加Classic 网络环境应用场景事例。
    • 26 |
    27 |
    1.0.0 - 2014.09.05
    28 |
      29 |
    • 首次发布,支持IPSec VPN、拨号VPN、SNAT 功能。
    • 30 |
    31 |
    32 |
    33 |
    34 |
    35 | 36 | {% endblock content %} 37 | -------------------------------------------------------------------------------- /website/docs/templates/docs/debug.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Docs{% endblock %} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • Docs
  • 7 |
  • 问题排查
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "docs/sidenav.html" %} 12 | 13 | 14 |
    15 |
    16 |
    17 |
    18 |

    问题排查指南

    19 |
    20 |

    IPSec Site-to-Site 隧道:

    21 |
    22 |
    1、隧道建立后无法连接?
    23 |
    请检查两端隧道ID和共享秘钥是否相同。
    24 | 请检查对端EIP是否有误。
    25 | 请尝试重新启动VPN服务。 26 |
    27 |
    2、隧道连接成功,但是两端子网无法相互访问?
    28 |
    请检查是否已将子网的流量路由到了VPN VM 上。
    29 | 请检查配置中对端子网是否有误。
    30 | 请尝试重新启动VPN服务。 31 |
    32 |
    33 |

    拨号VPN:

    34 |
    35 |
    1、客户接连接超时,请按以下步骤依次检查尝试排除:
    36 |
    请检查配置文件服务器地址是否有误。
    37 | 请检查服务是否开启。
    38 | 请检查客户端是否使用正确的根证书(ca.crt)。
    39 | 请确认是否更改过服务器证书文件或配置。
    40 | 请检查客户端是否有防火墙过滤。
    41 | 请尝试重新启动拨号VPN服务。 42 |
    43 |
    2、客户端连接失败,提示‘AUTH_FAILED’?
    44 |
    请检查输入账号密码是否正确。
    45 | 请检查服务器是否已经添加账号密码。
    46 | 请尝试重新启动拨号VPN服务。 47 |
    48 |
    3、客户端已连接上,但是无法访问内网VM?
    49 |
    请尝试PING服务器虚拟IP(如10.8.0.1)。
    50 | 请检查是否正确开启SNAT。
    51 | 请尝试重新启动拨号VPN服务。 52 |
    53 |
    54 |
    55 |
    56 |
    57 |
    58 | 59 | {% endblock content %} 60 | -------------------------------------------------------------------------------- /website/docs/templates/docs/dial.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Docs{% endblock %} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • Docs
  • 7 |
  • 拨号VPN
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "docs/sidenav.html" %} 12 | 13 | 14 |
    15 |
    16 |
    17 |
    18 |

    拨号 VPN 使用指南

    19 |
    20 |
    应用场景:
    21 |
      22 |
    1. Classic 网络场景。跨账号、跨地域的云服务器之间内网互通。
    2. 23 |
    3. VPC 网络场景。管理员接入VPC 进行私网访问、管理。
    4. 24 |
    25 | 26 |

    Classic 网络场景

    27 |
    28 | 29 |

    如上图所示,用户在杭州、北京、青岛区域用不同的账号各买了1台ECS VM,现在想让这3台VM 之间进行内网通信,我们需要把这3台VM 拨入到同一个VPN 网络中,用VPN 分配的地址进行通信。
    30 | 本例:杭州的VM 选为VPN GateWay,北京和青岛的VM 拨入到杭州的VPN 中。使用VPN 分配的地址10.8.8.7、10.8.8.9 进行相互通信。

    31 |
    第一、启动拨号VPN 服务
    32 |
    33 |

    进入拨号VPN 的「VPN 服务管理」页面,确保GateWay VM启动了拨号VPN 服务。

    34 | 35 |
      36 |
    • 启动VPN 服务:仅启动本机的拨号VPN。
    • 37 |
    • 停止VPN 服务:停止本机的拨号VPN。已经连接上的隧道将全部断开。
    • 38 |
    • 配置下发&重载:进行拨号VPN 「设置」时,该动作会自动进行。但某些情况下,如果你想重新生成VPN 服务端配置,可手动执行该操作。
    • 39 |
    40 |
    第二、设置
    41 |
    42 | 43 |
      44 |
    • 通信协议:可选"UDP"、"TCP"。注:每次修改保存后,请重新下载客户端配置文件。
    • 45 |
    • 虚拟IP 地址池:即VPN Server 分配给客户端的虚拟IP 地址池。本例为:10.8.8.0/24
    • 46 |
    • 允许client 间通信:本例子中,这里请选“是”。
    • 47 |
    • 允许单个账号同时在线:可选“是”或“否”。
    • 48 |
    • 子网网段:即允许拨号client 访问的子网。本例不需要client 访问子网,填写VPN GateWay VM 私网IP 即可:10.171.112.120/32。
    • 49 |
    50 |
    第四、添加拨号VPN 账号
    51 |
    52 |

    点击「新增账号」按钮,即可新增账号:

    53 | 54 |
      55 |
    • 账号名:只可包含如下字符:数字、字母、下划线。
    • 56 |
    • 密码:只可包含如下字符:数字、字母、下划线。
    • 57 |
    58 |
    第五、配置客户端
    59 |
    60 |

    点击「客户端下载」按钮,可以下载VPN 客户端和相应的配置文件。

    61 | 62 |
      63 |
    • 修改配置文件:将配置文件中的「remote IP」字段修改为GateWay VM 的公网地址。
    • 64 |
    • Windows 平台:安装完客户端后,将配置文件client.ovpn和ca.crt 文件放到安装目录下的config 文件夹中。然后启动openvpn-gui.exe,根据提示进行连接。
    • 65 |
    • Linux 平台:在配置文件client.conf 和ca.crt 的目录下执行命令:openvpn client.conf,根据提示进行连接。若要以daemon 形式在后台执行,请执行:openvpn client.conf & 来建立连接。
    • 66 |
    • 注:在Linux 平台下载客户端时,需要关闭证书验证。wget 请加上参数--no-check-certificate, curl 请加上参数--insecure。
    • 67 |
    68 |
    第六、查看账号列表
    69 |
    70 |

    点击「账号列表」按钮,可以查看已经添加的账号列表。如果该账号已经拨入VPN,将看到更明细的信息:

    71 | 72 |
      73 |
    • 状态:由于VPN 的keepalive 机制,会有1分钟左右的延时。
    • 74 |
    75 |
    第七、使用VPN IP 进行通信
    76 |
    77 |

    现在,即可使用VPN 分配的地址10.8.8.7、10.8.8.9 进行相互通信了。

    78 | 79 |

    VPC 网络场景

    80 |
    81 | 82 |

    如上图所示,管理员想接入VPC2 的私网内,以便管理维护VM1和VM2。其中,VPC2 中有一台使用VPN/SNAT 镜像安装的GateWay VM,并绑定了EIP。
    83 | 本例:管理员从公网通过VPN 隧道访问VPC2 的192.168.0.3 。

    84 |
    第一、启动拨号VPN 服务
    85 |
    86 |

    进入拨号VPN 的「VPN 服务管理」页面,确保VPC 的GateWay VM启动了拨号VPN 服务。

    87 | 88 |
      89 |
    • 启动VPN 服务:仅启动本机的拨号VPN。
    • 90 |
    • 停止VPN 服务:停止本机的拨号VPN。已经连接上的隧道将全部断开。
    • 91 |
    • 配置下发&重载:进行拨号VPN 「设置」时,该动作会自动进行。但某些情况下,如果你想重新生成VPN 服务端配置,可手动执行该操作。
    • 92 |
    93 |
    第二、设置
    94 |
    95 | 96 |
      97 |
    • 通信协议:可选"UDP"、"TCP"。注:每次修改保存后,请重新下载客户端配置文件。
    • 98 |
    • 虚拟IP 地址池:即VPN Server 分配给客户端的虚拟IP 地址池。
    • 99 |
    • 允许client 间通信:可“是”或“否”。
    • 100 |
    • 允许单个账号同时在线:可选“是”或“否”。
    • 101 |
    • 子网网段:即我们VPC2 的子网192.168.0.0/24。
    • 102 |
    103 |
    第三、配置SNAT
    104 |
    105 |

    进行拨号VPN「设置」之后,为了让管理员能够访问VPC2 的私网,需要手工调整相应的SNAT 设置!

    106 | 107 |

    在上面的例子中,虚拟地址池为10.8.0.0/24,子网网段为192.168.0.0/24,则需要配置SNAT: 10.8.0.0/24 ➔ 192.168.0.1

    108 |
    第四、添加拨号VPN 账号
    109 |
    110 |

    点击「新增账号」按钮,即可新增账号:

    111 | 112 |
      113 |
    • 账号名:只可包含如下字符:数字、字母、下划线。
    • 114 |
    • 密码:只可包含如下字符:数字、字母、下划线。
    • 115 |
    116 |
    第五、配置客户端
    117 |
    118 |

    点击「客户端下载」按钮,可以下载VPN 客户端和相应的配置文件。

    119 | 120 |
      121 |
    • 修改配置文件:将配置文件中的「remote IP」字段修改为GateWay VM 的EIP 地址。
    • 122 |
    • Windows 平台:安装完客户端后,将配置文件client.ovpn和ca.crt 文件放到安装目录下的config 文件夹中。然后启动openvpn-gui.exe,根据提示进行连接。
    • 123 |
    • Linux 平台:在配置文件client.conf 和ca.crt 的目录下执行命令:openvpn client.conf,根据提示进行连接。
    • 124 |
    125 |
    第六、查看账号列表
    126 |
    127 |

    点击「账号列表」按钮,可以查看已经添加的账号列表。如果该账号已经拨入VPN,将看到更明细的信息:

    128 | 129 |
      130 |
    • 状态:由于VPN 的keepalive 机制,会有1分钟左右的延时。
    • 131 |
    132 |
    133 |
    134 |
    135 |
    136 | 137 | {% endblock content %} 138 | -------------------------------------------------------------------------------- /website/docs/templates/docs/guide.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Docs{% endblock %} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • Docs
  • 7 |
  • 介绍
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "docs/sidenav.html" %} 12 | 13 | 14 |
    15 |
    16 |
    17 |
    18 |

    介绍

    19 |
    20 |

    本程序提供了VPN、SNAT 基础服务。

    21 |
    主要提供以下几点功能:
    22 |
      23 |
    1. IPSec Site-to-Site 功能。可快速的帮助你将两个不同的ECS 私网以IPSec Site-to-Site 的方式连接起来。
    2. 24 |
    3. 拨号VPN 功能。可让你通过拨号方式,接入ECS 私网,进行日常维护管理。
    4. 25 |
    5. SNAT 功能。可方便的设置Source NAT,以让ECS 私网内的VM 通过Gateway VM 访问外网。
    6. 26 |
    27 |
    典型的应用场景包括:
    28 |
      29 |
    1. VPC 用户通过VPN 将云上环境和用户侧网络打通(Site-to-Site)。
    2. 30 |
    3. 同一用户名下多个VPC(包括同Region/不同Region)之间通过VPN 打通(Site-to-Site)。
    4. 31 |
    5. 跨账号、跨区域的云服务器之间内网互通(拨号VPN。可将不同账号下的云服务器拨入同一个FlexGW VPN 内,然后用VPN 分配的私网地址进行互通)。
    6. 32 |
    33 |
    登陆说明:
    34 |
      35 |
    • 登陆地址:https://VM公网IP
    • 36 |
    • 使用VM的系统账号密码即可登入系统,即所有可通过SSH登陆主机的用户都可以登入该系统。
    • 37 |
    38 | 39 |

    软件组成

    40 |
    41 |
    Strongswan
    42 |
      43 |
    • 版本:5.1.3
    • 44 |
    • Website:http://www.strongswan.org
    • 45 |
    46 |
    OpenVPN
    47 |
      48 |
    • 版本:2.3.2
    • 49 |
    • Website:https://openvpn.net/index.php/open-source.html
    • 50 |
    51 | 52 |

    程序说明

    53 |
    54 |
    FlexGW(即本程序)
    55 |
      56 |
    • 目录:/usr/local/flexgw
    • 57 |
    • 数据库文件:/usr/local/flexgw/instance/website.db
    • 58 |
    • 启动脚本:/etc/init.d/flexgw 或/usr/local/flexgw/website_console
    • 59 |
    • 日志文件目录:/usr/local/flexgw/logs
    • 60 |
    • 实用脚本目录:/usr/local/flexgw/scripts
    • 61 |
    62 |

    「数据库文件」保存了我们所有的VPN 配置,建议定期备份。如果数据库损坏,可通过「实用脚本目录」下的initdb.py 脚本对数据库进行初始化,初始化之后所有的配置将清空。

    63 |
    Strongswan
    64 |
      65 |
    • 目录:/etc/strongswan
    • 66 |
    • 日志文件:/var/log/strongswan.charon.log
    • 67 |
    • 启动脚本:/usr/sbin/strongswan
    • 68 |
    69 |

    如果strongswan.conf 配置文件损坏,可使用备份文件/usr/local/flexgw/rc/strongswan.conf 进行覆盖恢复。

    70 |

    ipsec.conf 和ipsec.secrets 配置文件,由/usr/local/flexgw/website/vpn/sts/templates/sts 目录下的同名文件自动生成,请勿随便修改。

    71 |
    OpenVPN
    72 |
      73 |
    • 目录:/etc/openvpn
    • 74 |
    • 日志文件:/etc/openvpn/openvpn.log
    • 75 |
    • 状态文件:/etc/openvpn/openvpn-status.log
    • 76 |
    • 启动脚本:/etc/init.d/openvpn
    • 77 |
    78 |

    server.conf 配置文件,由/usr/local/flexgw/website/vpn/dial/templates/dial 目录下的同名文件自动生成,请勿随便修改。

    79 |
    80 |
    81 |
    82 |
    83 | 84 | {% endblock content %} 85 | -------------------------------------------------------------------------------- /website/docs/templates/docs/ipsec.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Docs{% endblock %} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • Docs
  • 7 |
  • IPSec VPN
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "docs/sidenav.html" %} 12 | 13 | 14 |
    15 |
    16 |
    17 |
    18 |

    IPSec Site-to-Site VPN 使用指南(VPC 网络场景)

    19 |
    20 | 21 |

    如上图所示,VPC1私网为:172.16.0.0/24,VPC2私网为:192.168.0.0/24。其中,两个VPC 中各有一台使用VPN/SNAT镜像安装的GateWay VM,并绑定了EIP。
    22 | 现在想让两个VPC 的私网VM之间 能够相互访问,我们将需要在VPC1 GateWay VM 和VPC2 GateWay VM 之间建立一条IPSec Site-to-Site隧道。
    23 | 本例:从VPC1 的172.16.0.3 访问VPC2 的192.168.0.3 。

    24 |
    第一、启动IPSec VPN 服务
    25 |
    26 |

    进入IPSec 「VPN 服务管理」页面,确保VPC 两端的GateWay VM1、GateWay VM2 均启动了IPSec VPN 服务。

    27 | 28 |
      29 |
    • 启动VPN 服务:仅启动本机的IPSec VPN。启动时,启动类型为「自动连接」的隧道将自动尝试连接对端VPN。
    • 30 |
    • 停止VPN 服务:停止本机的IPSec VPN。已经连接上的隧道将全部断开。
    • 31 |
    • 配置下发&重载:一般情况下,该动作在新增、修改或删除隧道时会自动进行。但某些情况下,如果你想重新生成VPN 配置,可手动执行该操作。
    • 32 |
    33 |
    第二、新增隧道
    34 |
    35 |
    VPC1 GateWay VM:
    36 | 37 |
    VPC2 GateWay VM:
    38 | 39 |
      40 |
    • 两边的隧道ID、预共享密钥必须一致才能建立连接。
    • 41 |
    • 本端子网、对端子网:即前面例子中的192.168.0.0/24,172.16.0.0/24。
    • 42 |
    • 对端EIP:对端GateWay 所绑定的EIP。
    • 43 |
    44 |
    第三、查看隧道列表
    45 |
    46 |

    在VPC1和VPC2 的GateWay VM 上将隧道添加完毕之后,进入「隧道列表」页面。对我们刚刚配置好的隧道,点击「连接」,即可看到:

    47 | 48 |
      49 |
    • 连接:连接隧道。在两台GateWay VM任意一端操作即可。
    • 50 |
    • 断开:断开隧道。在两台GateWay VM任意一端操作即可。
    • 51 |
    52 |
    第四、查看隧道实时流量
    53 |
    54 |

    点击上图的「流量」按钮,即可看到隧道的实时流量:

    55 | 56 |
      57 |
    • 颜色:代表in、out 方向的流量。
    • 58 |
    • 单位:Bytes。
    • 59 |
    60 |
    第五、修改或删除隧道
    61 |
    62 |

    点击对应的隧道ID 进入修改、删除页面:

    63 | 64 |
      65 |
    • 保存:修改后,点击保存,配置将立即生效,但不会影响已经连接上的隧道。需要手工断开、再连接隧道。
    • 66 |
    • 删除:点击删除,将该隧道删除,同时会自动断开该隧道,立即生效。
    • 67 |
    68 |
    69 |
    70 |
    71 |
    72 | 73 | {% endblock content %} 74 | -------------------------------------------------------------------------------- /website/docs/templates/docs/sidenav.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 | {% set side_bar = [ 4 | ('docs.index', 'list', '介绍'), 5 | ('docs.ipsec', 'list', 'IPSec VPN'), 6 | ('docs.dial', 'list', '拨号VPN'), 7 | ('docs.snat', 'list', 'SNAT使用'), 8 | ('docs.certificate', 'list', '关于VPN 证书'), 9 | ('docs.debug', 'list', '问题排查'), 10 | ('docs.update', 'list', '升级指南'), 11 | ('docs.changelog', 'list', 'ChangeLog') 12 | ] -%} 13 |
      14 | {% for endpoint, icon, caption in side_bar %} 15 | {{ caption }} 16 | {% endfor %} 17 |
    18 |
    19 | 20 | -------------------------------------------------------------------------------- /website/docs/templates/docs/snat.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Docs{% endblock %} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • Docs
  • 7 |
  • SNAT
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "docs/sidenav.html" %} 12 | 13 | 14 |
    15 |
    16 |
    17 |
    18 |

    SNAT 使用指南

    19 |
    20 | 21 |

    如上图所示,VPC1私网为:172.16.0.0/24。其中,VPC1 中有一台使用VPN/SNAT 镜像安装的GateWay VM,并绑定了EIP。
    22 | 现在想让VPC1 的私网VM 能够访问公网,我们将需要在VPC1 GateWay VM 上进行SNAT 配置。
    23 | 本例:从VPC1 的172.16.0.3 访问公网。

    24 |
    第一、添加SNAT 条目
    25 |
    26 |

    进入SNAT 的「SNAT 新增」页面:

    27 | 28 |
      29 |
    • 需转换的源IP(或网段):为VPC1 中需要访问公网的私网网段。本例中为:172.16.0.0/24
    • 30 |
    • 转换后的IP:为VPC1 中GateWay VM 的私网IP,而非EIP。本例中为:172.16.0.1
    • 31 |
    32 | 33 |
    第二、查看SNAT 列表
    34 |
    35 |

    新增SNAT 条目之后,会自动跳转到「SNAT 列表」页面,即可看到:

    36 | 37 |
      38 |
    • 需转换的源IP(或网段):为VPC1 中需要访问公网的私网网段。本例中为:172.16.0.0/24
    • 39 |
    • 转换后的IP:为VPC1 中GateWay VM 的私网IP,而非EIP。本例中为:172.16.0.1
    • 40 |
    • 删除:点击「删除」按钮,即可删除该SNAT 条目,且立即生效。
    • 41 |
    42 |
    43 |
    44 |
    45 |
    46 | 47 | {% endblock content %} 48 | -------------------------------------------------------------------------------- /website/docs/templates/docs/update.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Docs{% endblock %} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • Docs
  • 7 |
  • 升级指南
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "docs/sidenav.html" %} 12 | 13 | 14 |
    15 |
    16 |
    17 |
    18 |

    升级指南

    19 |
    20 |

    当检测到升级信息时,请按如下方法进行升级:

    21 |
      22 |
    1. 通过ssh 或者VNC 管理终端登录VM,切换到root 账号,或者sudo 权限。
    2. 23 |
    3. 关闭flexgw,执行命令:/etc/init.d/flexgw stop
    4. 24 |
    5. 升级,执行命令:/usr/local/flexgw/scripts/update --yes
    6. 25 |
    7. 根据提示进行升级。
    8. 26 |
    9. 升级完成,启动flexgw:/etc/init.d/flexgw start
    10. 27 |
    28 |

    注意:升级前,建议备份/usr/local/flexgw/instance/* 目录下的数据文件!

    29 |
    30 |
    31 |
    32 |
    33 | 34 | {% endblock content %} 35 | -------------------------------------------------------------------------------- /website/docs/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.docs.views 4 | ~~~~~~~~~~~~~~~~~~ 5 | 6 | vpn views: 7 | /docs 8 | """ 9 | 10 | 11 | from flask import Blueprint, render_template 12 | 13 | from flask.ext.login import login_required 14 | 15 | 16 | docs = Blueprint('docs', __name__, url_prefix='/docs', 17 | template_folder='templates', 18 | static_folder='static') 19 | 20 | 21 | @docs.route('/') 22 | @login_required 23 | def index(): 24 | return render_template('docs/guide.html') 25 | 26 | 27 | @docs.route('/ipsec') 28 | @login_required 29 | def ipsec(): 30 | return render_template('docs/ipsec.html') 31 | 32 | 33 | @docs.route('/dial') 34 | @login_required 35 | def dial(): 36 | return render_template('docs/dial.html') 37 | 38 | 39 | @docs.route('/snat') 40 | @login_required 41 | def snat(): 42 | return render_template('docs/snat.html') 43 | 44 | 45 | @docs.route('/certificate') 46 | @login_required 47 | def certificate(): 48 | return render_template('docs/certificate.html') 49 | 50 | 51 | @docs.route('/debug') 52 | @login_required 53 | def debug(): 54 | return render_template('docs/debug.html') 55 | 56 | 57 | @docs.route('/update') 58 | @login_required 59 | def update(): 60 | return render_template('docs/update.html') 61 | 62 | 63 | @docs.route('/changelog') 64 | @login_required 65 | def changelog(): 66 | return render_template('docs/changelog.html') 67 | -------------------------------------------------------------------------------- /website/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.helpers 4 | ~~~~~~~~~~~~~~~ 5 | 6 | top level helpers. 7 | """ 8 | 9 | 10 | from flask import request 11 | 12 | 13 | def log_request(sender, **extra): 14 | sender.logger.info('[Request Message]: %s %s %s', 15 | request.method, 16 | request.url, 17 | request.data) 18 | 19 | 20 | def log_exception(sender, exception, **extra): 21 | sender.logger.error('[Exception Request]: %s', exception) 22 | -------------------------------------------------------------------------------- /website/services.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.services 4 | ~~~~~~~~~~~~~~~~ 5 | 6 | top level services api. 7 | """ 8 | 9 | 10 | import subprocess 11 | from flask import current_app 12 | 13 | from threading import Timer 14 | 15 | 16 | def exec_command(cmd, timeout=5, stdout=subprocess.PIPE): 17 | current_app.logger.info("执行命令 => {}".format(cmd)) 18 | proc = subprocess.Popen(cmd, stdout=stdout, 19 | stderr=subprocess.PIPE) 20 | # settings exec timeout 21 | timer = Timer(timeout, proc.kill) 22 | timer.start() 23 | stdout, stderr = proc.communicate() 24 | timer.cancel() 25 | current_app.logger.info("命令结果 => {}".format({'return_code': proc.returncode, 'stdout': stdout, 26 | 'stderr': stderr})) 27 | return {'return_code': proc.returncode, 'stdout': stdout, 28 | 'stderr': stderr} 29 | -------------------------------------------------------------------------------- /website/snat/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.snat 4 | ~~~~~~~~~~~~ 5 | 6 | website snat blueprint. 7 | """ 8 | -------------------------------------------------------------------------------- /website/snat/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.snat.forms 4 | ~~~~~~~~~~~~~~~~~~ 5 | 6 | vpn forms: 7 | /sant 8 | """ 9 | 10 | 11 | from flask_wtf import Form 12 | from wtforms import TextField, ValidationError 13 | from wtforms.validators import Required, IPAddress 14 | 15 | 16 | def IPorNet(message=u"无效的IP 或网段!"): 17 | def _ipornet(form, field): 18 | value = field.data 19 | ip = value.split('/')[0] 20 | if '/' in value: 21 | try: 22 | mask = int(value.split('/')[1]) 23 | except: 24 | raise ValidationError(message) 25 | if mask < 0 or mask > 32: 26 | raise ValidationError(message) 27 | parts = ip.split('.') 28 | if len(parts) == 4 and all(x.isdigit() for x in parts): 29 | numbers = list(int(x) for x in parts) 30 | if not all(num >= 0 and num < 256 for num in numbers): 31 | raise ValidationError(message) 32 | return True 33 | raise ValidationError(message) 34 | return _ipornet 35 | 36 | 37 | class SnatForm(Form): 38 | source = TextField(u'需转换的源IP(或网段)', 39 | validators=[Required(message=u'这是一个必选项!'), 40 | IPorNet(message=u"无效的IP 或网段!")]) 41 | gateway = TextField(u'转换后的IP', 42 | validators=[Required(message=u'这是一个必选项!'), 43 | IPAddress(message=u'无效的IP 地址!')]) 44 | -------------------------------------------------------------------------------- /website/snat/services.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.snat.services 4 | ~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | snat services api. 7 | """ 8 | 9 | 10 | import sys 11 | 12 | from flask import flash, current_app 13 | 14 | from website.services import exec_command 15 | 16 | 17 | def iptables_get_snat_rules(message=True): 18 | cmd = ['iptables', '-t', 'nat', '--list-rules'] 19 | try: 20 | r = exec_command(cmd) 21 | except: 22 | current_app.logger.error('[SNAT]: exec_command error: %s:%s', cmd, 23 | sys.exc_info()[1]) 24 | if message: 25 | flash(u'iptables 程序异常,无法调用,请排查操作系统相关设置!', 'alert') 26 | return False 27 | if r['return_code'] != 0: 28 | current_app.logger.error('[SNAT]: exec_command return: %s:%s:%s', cmd, 29 | r['return_code'], r['stderr']) 30 | if message: 31 | message = u"获取规则失败:%s" % r['stderr'] 32 | flash(message, 'alert') 33 | return False 34 | rules = [] 35 | for item in r['stdout'].split('\n'): 36 | if '-j SNAT' in item: 37 | t = item.split() 38 | rules.append((t[t.index('-s')+1], t[t.index('--to-source')+1])) 39 | return rules 40 | 41 | 42 | def iptables_set_snat_rules(method, source, gateway, message=True): 43 | methods = {'add': '-A', 'del': '-D'} 44 | #: check rule exist while add rule 45 | rules = iptables_get_snat_rules() 46 | if isinstance(rules, bool) and not rules: 47 | return False 48 | if method == 'add' and (source, gateway) in rules: 49 | if message: 50 | message = u"该规则已经存在:%s ==> %s" % (source, gateway) 51 | flash(message, 'alert') 52 | return False 53 | #: add rule to iptables 54 | cmd = 'iptables -t nat %s POSTROUTING -s %s -j SNAT --to-source %s' % (methods[method], source, gateway) 55 | save_rules = 'iptables-save -t nat' 56 | try: 57 | with open('/usr/local/flexgw/instance/snat-rules.iptables', 'w') as f: 58 | results = exec_command(cmd.split()), exec_command(save_rules.split(), stdout=f) 59 | except: 60 | current_app.logger.error('[SNAT]: exec_command error: %s:%s', cmd, 61 | sys.exc_info()[1]) 62 | if message: 63 | flash(u'iptables 程序异常,无法调用,请排查操作系统相关设置!', 'alert') 64 | return False 65 | 66 | #: check result 67 | for r, c in zip(results, [cmd, save_rules]): 68 | if r['return_code'] == 0: 69 | continue 70 | elif message: 71 | message = u"设置规则失败:%s" % r['stderr'] 72 | flash(message, 'alert') 73 | current_app.logger.error('[SNAT]: exec_command return: %s:%s:%s', c, 74 | r['return_code'], r['stderr']) 75 | return False 76 | 77 | return True 78 | -------------------------------------------------------------------------------- /website/snat/static/js/app.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | $(document).on('click', 'button.snat-del', function() { 3 | var tr = $(this).closest("tr"); 4 | $.ajax({ 5 | type: 'POST', 6 | url: '/snat/del', 7 | headers: { 8 | 'X-CSRFToken': csrftoken 9 | }, 10 | data: { 11 | source: tr.find(".snat-source").text(), 12 | gateway: tr.find(".snat-gateway").text() 13 | }, 14 | success: function(res, status, xhr) { 15 | if (status === "success") { 16 | tr.fadeOut(); 17 | } 18 | var alertBox = '
    ' + 19 | '删除SNAT 规则成功:' + 20 | res.rules.source + ' ==> ' + res.rules.gateway + 21 | '×
    ' 22 | $(".alert-box").remove(); 23 | $("#snat-list").prepend(alertBox).foundation(); 24 | }, 25 | error: function(xhr, status, err) { 26 | console.error('Del snat failure: ' + err); 27 | } 28 | }); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /website/snat/templates/add.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}SNAT 新增{% endblock %} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • SNAT
  • 7 |
  • SNAT 新增
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | 12 |
    13 | 17 |
    18 | 19 | 20 | 21 |
    22 | {% with messages = get_flashed_messages(with_categories=true) %} 23 | {% if messages %} 24 | {% for category, message in messages %} 25 |
    26 | {{ message }} 27 | × 28 |
    29 | {% endfor %} 30 | {% endif %} 31 | {% endwith %} 32 |
    33 | 注意:「转换后的IP」为本机私网IP,非本机绑定的公网地址或EIP 地址。 34 |
    35 |
    36 |
    37 |
    38 | {{ form.source.label(class="right inline") }} 39 |
    40 |
    41 | {% if form.source.errors %} 42 | {{ form.source(class="error", value=form.source.value) }} 43 | {{ form.source.errors[0] }} 44 | {% else %} 45 | {{ form.source(placeholder="10.8.8.0/24") }} 46 | {% endif %} 47 |
    48 |
    49 |
    50 |
    51 | {{ form.gateway.label(class="right inline") }} 52 |
    53 |
    54 | {% if form.gateway.errors %} 55 | {{ form.gateway(class="error", value=form.source.value) }} 56 | {{ form.gateway.errors[0] }} 57 | {% else %} 58 | {{ form.gateway(placeholder="192.168.0.1") }} 59 | {% endif %} 60 |
    61 |
    62 |
    63 |
    64 | 65 | 66 |
    67 |
    68 |
    69 |
    70 | 71 | {% endblock content %} 72 | 73 | -------------------------------------------------------------------------------- /website/snat/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}SNAT 列表{% endblock %} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • SNAT
  • 7 |
  • SNAT 列表
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | 12 |
    13 | 17 |
    18 | 19 | 20 | 21 |
    22 | {% with messages = get_flashed_messages(with_categories=true) %} 23 | {% if messages %} 24 | {% for category, message in messages %} 25 |
    26 | {{ message }} 27 | × 28 |
    29 | {% endfor %} 30 | {% endif %} 31 | {% endwith %} 32 |
    33 |
    34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {% if rules %} 44 | {% for source, gateway in rules %} 45 | 46 | 47 | 48 | 56 | 57 | {% endfor %} 58 | {% endif %} 59 | 60 |
    需转换的源IP(或网段)转换后的IP操作
    {{ source }}{{ gateway }} 49 |
    50 | 51 | 52 | 53 | 54 |
    55 |
    61 |
    62 |
    63 |
    64 | 65 | {% endblock content %} 66 | -------------------------------------------------------------------------------- /website/snat/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.snat.views 4 | ~~~~~~~~~~~~~~~~~~ 5 | 6 | vpn views: 7 | /snat 8 | """ 9 | 10 | 11 | from flask import Blueprint, render_template 12 | from flask import url_for, redirect, flash 13 | from flask import request 14 | 15 | from flask.ext.login import login_required 16 | 17 | from website.snat.forms import SnatForm 18 | from website.snat.services import iptables_get_snat_rules, iptables_set_snat_rules 19 | 20 | 21 | snat = Blueprint('snat', __name__, url_prefix='/snat', 22 | template_folder='templates', 23 | static_folder='static') 24 | 25 | 26 | @snat.route('/') 27 | @login_required 28 | def index(): 29 | rules = iptables_get_snat_rules() 30 | if isinstance(rules, list) and not rules: 31 | flash(u'目前没有任何SNAT配置,如有需要请添加。', 'info') 32 | return render_template('index.html', rules=rules) 33 | 34 | 35 | @snat.route('/add', methods=['GET', 'POST']) 36 | @login_required 37 | def add(): 38 | form = SnatForm() 39 | if form.validate_on_submit(): 40 | if iptables_set_snat_rules('add', form.source.data, form.gateway.data): 41 | message = u'添加SNAT 规则成功:%s ==> %s' % (form.source.data, form.gateway.data) 42 | flash(message, 'success') 43 | return redirect(url_for('snat.index')) 44 | return render_template('add.html', form=form) 45 | 46 | 47 | @snat.route('/del', methods=['POST']) 48 | @login_required 49 | def delete(): 50 | source = request.form['source'] 51 | gateway = request.form['gateway'] 52 | if iptables_set_snat_rules('del', source, gateway): 53 | message = u'删除SNAT 规则成功:%s ==> %s' % (source, gateway) 54 | flash(message, 'success') 55 | return redirect(url_for('snat.index')) 56 | -------------------------------------------------------------------------------- /website/static/css/app.css: -------------------------------------------------------------------------------- 1 | /* overrides */ 2 | 3 | 4 | /* ----------------------------------------- 5 | For a, body, header, footer 6 | ----------------------------------------- */ 7 | body, 8 | header.contain-to-grid { 9 | background: white url(../img/light-grid.png) repeat; 10 | } 11 | 12 | header.contain-to-grid { 13 | margin: 0 auto; 14 | padding-top: 1rem; 15 | padding-bottom: 1rem; 16 | text-decoration: none; 17 | } 18 | 19 | footer.row { 20 | margin-top: 1.25rem; 21 | padding: 1rem 0; 22 | border-top: 1px solid #AAA; 23 | } 24 | 25 | footer > * a { 26 | font-size: 60%; 27 | } 28 | 29 | 30 | /* ----------------------------------------- 31 | For nav 32 | ----------------------------------------- */ 33 | #nav .top-bar, 34 | #nav .top-bar-section ul, 35 | #nav .top-bar-section li:not(.has-form) a:not(.button), 36 | #nav .top-bar-section ul li > a, 37 | #nav .top-bar.expanded .title-area { 38 | background: #379F7A url(../img/bg.png) repeat; 39 | color: white; 40 | text-decoration: none; 41 | } 42 | 43 | #nav .top-bar-section li:not(.has-form) a:not(.button):hover, 44 | #nav .top-bar-section .dropdown li:not(.has-form):hover > a:not(.button) { 45 | background: #11766D; 46 | } 47 | 48 | #nav .top-bar-section .dropdown li:not(.has-form) a:not(.button) { 49 | color: white; 50 | background: #61ADA0; 51 | } 52 | 53 | #nav .top-bar-section li.active:not(.has-form) a:not(.button) { 54 | background: #11766D; 55 | } 56 | 57 | #trail { 58 | margin-top: 1.25rem; 59 | } 60 | 61 | .breadcrumbs { 62 | padding: 0; 63 | background-color: transparent; 64 | border-color: transparent; 65 | } 66 | 67 | .breadcrumbs > *:before { 68 | content: "›"; 69 | } 70 | 71 | .breadcrumbs > * a { 72 | color: #00A8C6; 73 | } 74 | 75 | .side-nav { 76 | padding: 0; 77 | } 78 | 79 | .side-nav li.active > a:first-child:not(.button) { 80 | color: #FFF; 81 | background: #379F7A; 82 | } 83 | 84 | .side-nav li a, 85 | .side-nav li a:not(.button) { 86 | color: #00A8C6; 87 | background: #efefef; 88 | } 89 | 90 | .side-nav li a:not(.button):hover, 91 | .side-nav li a:not(.button):focus { 92 | color: #40C0CB; 93 | } 94 | 95 | ul.square, ol { 96 | font-size: 0.8125rem; 97 | } 98 | 99 | 100 | /* ----------------------------------------- 101 | For content, table 102 | ----------------------------------------- */ 103 | table.columns { 104 | border-spacing: 0; 105 | padding: 0; 106 | } 107 | 108 | td > .button, 109 | td form > .button { 110 | margin: 0; 111 | } 112 | 113 | td form { 114 | margin: 0; 115 | } 116 | 117 | td a { 118 | color: #00A8C6; 119 | } 120 | 121 | .button { 122 | background: #00A8C6; 123 | } 124 | 125 | .button.bitty { 126 | padding: 5px; 127 | font-size: 0.6875rem; 128 | } 129 | 130 | .button.alert { 131 | background: #F1396D; 132 | } 133 | 134 | .form-label { 135 | margin-top: 0.5625rem; 136 | } 137 | 138 | .switch { 139 | padding: 0.5625rem 0; 140 | } 141 | 142 | .switch .has-tip { 143 | border-bottom: none; 144 | } 145 | 146 | .pricing-table .title { 147 | background: whitesmoke; 148 | color: #222; 149 | } 150 | 151 | .pricing-table .description { 152 | min-height: 80px; 153 | color: #424242; 154 | border-bottom: dashed 1px #DDD; 155 | } 156 | 157 | .pricing-table .cta-button .alert { 158 | background: #F35F55; 159 | } 160 | 161 | .alert-box.info { 162 | background-color: #4DBCE9; 163 | border-color: #4DBCE9; 164 | color: #FFF; 165 | } 166 | 167 | .alert-box.alert { 168 | background-color: #FF4E50; 169 | border-color: #FF4E50; 170 | color: #FFF; 171 | } 172 | 173 | .alert-box.success { 174 | background-color: #00C176; 175 | border-color: #00C176; 176 | color: #FFF; 177 | } 178 | 179 | .alert-box.loader { 180 | background-color: transparent; 181 | border-color: transparent; 182 | text-align: center; 183 | } 184 | 185 | a.box-link { 186 | color: #FFF; 187 | text-decoration: underline; 188 | } 189 | 190 | #showpass { 191 | background: #E7E7E7; 192 | color: #000; 193 | } 194 | 195 | /* ----------------------------------------- 196 | For login 197 | ----------------------------------------- */ 198 | #login { 199 | margin-top: 5rem; 200 | padding: 0; 201 | background: #efefef; 202 | box-shadow: 1px 1px 10px #c7c7c7; 203 | -webkit-box-shadow: 1px 1px 10px #c7c7c7; 204 | -moz-box-shadow: 1px 1px 10px #c7c7c7; 205 | } 206 | 207 | #login .header { 208 | font-size: 2rem; 209 | padding: 0.5625rem; 210 | background: #379F7A; 211 | text-align: center; 212 | } 213 | 214 | #login .header h4 { 215 | color: #FFF; 216 | } 217 | 218 | #login form { 219 | padding: 1rem; 220 | } 221 | 222 | 223 | /* ----------------------------------------- 224 | For docs 225 | ----------------------------------------- */ 226 | .panel img { 227 | margin-bottom: 10px; 228 | } 229 | -------------------------------------------------------------------------------- /website/static/css/foundation-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/static/css/foundation-icons.eot -------------------------------------------------------------------------------- /website/static/css/foundation-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/static/css/foundation-icons.ttf -------------------------------------------------------------------------------- /website/static/css/foundation-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/static/css/foundation-icons.woff -------------------------------------------------------------------------------- /website/static/img/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/static/img/bg.png -------------------------------------------------------------------------------- /website/static/img/grey_@2X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/static/img/grey_@2X.png -------------------------------------------------------------------------------- /website/static/img/light-grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/static/img/light-grid.png -------------------------------------------------------------------------------- /website/static/img/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/static/img/loader.gif -------------------------------------------------------------------------------- /website/static/js/checkupdate.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | function showMessage(message, type) { 3 | $('#check-update-info a.close').trigger('click.fndtn.alert'); 4 | var alertBox = '
    ' + message + 6 | '× ' + '
    '; 7 | $(".large-10").prepend(alertBox).foundation(); 8 | } 9 | 10 | function showLoading() { 11 | var loading = '
    Loading...
    '; 12 | $(".large-10").prepend(loading); 13 | } 14 | 15 | function hideLoading() { 16 | $('#loading').remove(); 17 | } 18 | 19 | $("#checkupdate").bind('click', function (event) { 20 | event.preventDefault(); 21 | $.ajax({ 22 | url: "/api/checkupdate", 23 | type: "get", 24 | dataType: "json", 25 | beforeSend: function (xhr) { 26 | $('#check-update-info a.close').trigger('click.fndtn.alert'); 27 | showLoading(); 28 | }, 29 | success: function (res, status, xhr) { 30 | console.log(res); 31 | var message = res.message + '点此查看升级方法。' 32 | showMessage(message, 'success'); 33 | }, 34 | complete: function (xhr, status) { 35 | hideLoading(); 36 | }, 37 | error: function (xhr, status, thrown) { 38 | var err = xhr.responseJSON; 39 | console.log(err); 40 | showMessage(err ? err.message : '检测更新失败!', 'alert'); 41 | } 42 | }); 43 | }); 44 | })(); 45 | -------------------------------------------------------------------------------- /website/static/js/flow.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var RealTimeData = function(tunnelName) { 3 | this.tunnelName = tunnelName; 4 | this.timestamp = ((new Date()).getTime() / 1000)|0; 5 | }; 6 | 7 | RealTimeData.prototype.history = function() { 8 | var history = [ 9 | { label: 'rx_pkts', values: []}, 10 | { label: 'tx_pkts', values: []} 11 | ]; 12 | for (var i = 0; i < history.length; i++) { 13 | history[i].values.push({time: this.timestamp, y: 0}); 14 | this.timestamp--; 15 | } 16 | return history; 17 | } 18 | 19 | RealTimeData.prototype.next = function() { 20 | var next = []; 21 | $.ajax({ 22 | type: "get", 23 | url: "/api/vpn/" + this.tunnelName + "/traffic/now", 24 | async: false, 25 | cache: false, 26 | success: function(res, status, xhr) { 27 | next.push({time: res.time, y: res.tx_pkts}); 28 | next.push({time: res.time, y: res.rx_pkts}); 29 | } 30 | }); 31 | return next; 32 | } 33 | window.RealTimeData = RealTimeData; 34 | })(); 35 | -------------------------------------------------------------------------------- /website/static/js/foundation/foundation.accordion.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 'use strict'; 3 | 4 | Foundation.libs.accordion = { 5 | name : 'accordion', 6 | 7 | version : '5.3.0', 8 | 9 | settings : { 10 | active_class: 'active', 11 | multi_expand: false, 12 | toggleable: true, 13 | callback : function () {} 14 | }, 15 | 16 | init : function (scope, method, options) { 17 | this.bindings(method, options); 18 | }, 19 | 20 | events : function () { 21 | var self = this; 22 | var S = this.S; 23 | S(this.scope) 24 | .off('.fndtn.accordion') 25 | .on('click.fndtn.accordion', '[' + this.attr_name() + '] > dd > a', function (e) { 26 | var accordion = S(this).closest('[' + self.attr_name() + ']'), 27 | target = S('#' + this.href.split('#')[1]), 28 | siblings = S('dd > .content', accordion), 29 | aunts = $('dd', accordion), 30 | groupSelector = self.attr_name() + '=' + accordion.attr(self.attr_name()), 31 | settings = accordion.data(self.attr_name(true) + '-init'), 32 | active_content = S('dd > .content.' + settings.active_class, accordion); 33 | e.preventDefault(); 34 | 35 | if (accordion.attr(self.attr_name())) { 36 | siblings = siblings.add('[' + groupSelector + '] dd > .content'); 37 | aunts = aunts.add('[' + groupSelector + '] dd'); 38 | } 39 | 40 | if (settings.toggleable && target.is(active_content)) { 41 | target.parent('dd').toggleClass(settings.active_class, false); 42 | target.toggleClass(settings.active_class, false); 43 | settings.callback(target); 44 | target.triggerHandler('toggled', [accordion]); 45 | accordion.triggerHandler('toggled', [target]); 46 | return; 47 | } 48 | 49 | if (!settings.multi_expand) { 50 | siblings.removeClass(settings.active_class); 51 | aunts.removeClass(settings.active_class); 52 | } 53 | 54 | target.addClass(settings.active_class).parent().addClass(settings.active_class); 55 | settings.callback(target); 56 | target.triggerHandler('toggled', [accordion]); 57 | accordion.triggerHandler('toggled', [target]); 58 | }); 59 | }, 60 | 61 | off : function () {}, 62 | 63 | reflow : function () {} 64 | }; 65 | }(jQuery, window, window.document)); 66 | -------------------------------------------------------------------------------- /website/static/js/foundation/foundation.alert.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 'use strict'; 3 | 4 | Foundation.libs.alert = { 5 | name : 'alert', 6 | 7 | version : '5.3.0', 8 | 9 | settings : { 10 | callback: function (){} 11 | }, 12 | 13 | init : function (scope, method, options) { 14 | this.bindings(method, options); 15 | }, 16 | 17 | events : function () { 18 | var self = this, 19 | S = this.S; 20 | 21 | $(this.scope).off('.alert').on('click.fndtn.alert', '[' + this.attr_name() + '] a.close', function (e) { 22 | var alertBox = S(this).closest('[' + self.attr_name() + ']'), 23 | settings = alertBox.data(self.attr_name(true) + '-init') || self.settings; 24 | 25 | e.preventDefault(); 26 | if (Modernizr.csstransitions) { 27 | alertBox.addClass("alert-close"); 28 | alertBox.on('transitionend webkitTransitionEnd oTransitionEnd', function(e) { 29 | S(this).trigger('close').trigger('close.fndtn.alert').remove(); 30 | settings.callback(); 31 | }); 32 | } else { 33 | alertBox.fadeOut(300, function () { 34 | S(this).trigger('close').trigger('close.fndtn.alert').remove(); 35 | settings.callback(); 36 | }); 37 | } 38 | }); 39 | }, 40 | 41 | reflow : function () {} 42 | }; 43 | }(jQuery, window, window.document)); 44 | -------------------------------------------------------------------------------- /website/static/js/foundation/foundation.equalizer.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 'use strict'; 3 | 4 | Foundation.libs.equalizer = { 5 | name : 'equalizer', 6 | 7 | version : '5.3.0', 8 | 9 | settings : { 10 | use_tallest: true, 11 | before_height_change: $.noop, 12 | after_height_change: $.noop, 13 | equalize_on_stack: false 14 | }, 15 | 16 | init : function (scope, method, options) { 17 | Foundation.inherit(this, 'image_loaded'); 18 | this.bindings(method, options); 19 | this.reflow(); 20 | }, 21 | 22 | events : function () { 23 | this.S(window).off('.equalizer').on('resize.fndtn.equalizer', function(e){ 24 | this.reflow(); 25 | }.bind(this)); 26 | }, 27 | 28 | equalize: function(equalizer) { 29 | var isStacked = false, 30 | vals = equalizer.find('[' + this.attr_name() + '-watch]:visible'), 31 | settings = equalizer.data(this.attr_name(true)+'-init'); 32 | 33 | if (vals.length === 0) return; 34 | var firstTopOffset = vals.first().offset().top; 35 | settings.before_height_change(); 36 | equalizer.trigger('before-height-change').trigger('before-height-change.fndth.equalizer'); 37 | vals.height('inherit'); 38 | vals.each(function(){ 39 | var el = $(this); 40 | if (el.offset().top !== firstTopOffset) { 41 | isStacked = true; 42 | } 43 | }); 44 | 45 | if (settings.equalize_on_stack === false) { 46 | if (isStacked) return; 47 | }; 48 | 49 | var heights = vals.map(function(){ return $(this).outerHeight(false) }).get(); 50 | 51 | if (settings.use_tallest) { 52 | var max = Math.max.apply(null, heights); 53 | vals.css('height', max); 54 | } else { 55 | var min = Math.min.apply(null, heights); 56 | vals.css('height', min); 57 | } 58 | settings.after_height_change(); 59 | equalizer.trigger('after-height-change').trigger('after-height-change.fndtn.equalizer'); 60 | }, 61 | 62 | reflow : function () { 63 | var self = this; 64 | 65 | this.S('[' + this.attr_name() + ']', this.scope).each(function(){ 66 | var $eq_target = $(this); 67 | self.image_loaded(self.S('img', this), function(){ 68 | self.equalize($eq_target) 69 | }); 70 | }); 71 | } 72 | }; 73 | })(jQuery, window, window.document); 74 | 75 | -------------------------------------------------------------------------------- /website/static/js/foundation/foundation.magellan.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 'use strict'; 3 | 4 | Foundation.libs['magellan-expedition'] = { 5 | name : 'magellan-expedition', 6 | 7 | version : '5.3.0', 8 | 9 | settings : { 10 | active_class: 'active', 11 | threshold: 0, // pixels from the top of the expedition for it to become fixes 12 | destination_threshold: 20, // pixels from the top of destination for it to be considered active 13 | throttle_delay: 30, // calculation throttling to increase framerate 14 | fixed_top: 0 // top distance in pixels assigend to the fixed element on scroll 15 | }, 16 | 17 | init : function (scope, method, options) { 18 | Foundation.inherit(this, 'throttle'); 19 | this.bindings(method, options); 20 | }, 21 | 22 | events : function () { 23 | var self = this, 24 | S = self.S, 25 | settings = self.settings; 26 | 27 | // initialize expedition offset 28 | self.set_expedition_position(); 29 | 30 | S(self.scope) 31 | .off('.magellan') 32 | .on('click.fndtn.magellan', '[' + self.add_namespace('data-magellan-arrival') + '] a[href^="#"]', function (e) { 33 | e.preventDefault(); 34 | var expedition = $(this).closest('[' + self.attr_name() + ']'), 35 | settings = expedition.data('magellan-expedition-init'), 36 | hash = this.hash.split('#').join(''), 37 | target = $("a[name='"+hash+"']"); 38 | 39 | if (target.length === 0) { 40 | target = $('#'+hash); 41 | } 42 | 43 | // Account for expedition height if fixed position 44 | var scroll_top = target.offset().top - settings.destination_threshold; 45 | scroll_top = scroll_top - expedition.outerHeight(); 46 | 47 | $('html, body').stop().animate({ 48 | 'scrollTop': scroll_top 49 | }, 700, 'swing', function () { 50 | if(history.pushState) { 51 | history.pushState(null, null, '#'+hash); 52 | } 53 | else { 54 | location.hash = '#'+hash; 55 | } 56 | }); 57 | }) 58 | .on('scroll.fndtn.magellan', self.throttle(this.check_for_arrivals.bind(this), settings.throttle_delay)); 59 | 60 | $(window) 61 | .on('resize.fndtn.magellan', self.throttle(this.set_expedition_position.bind(this), settings.throttle_delay)); 62 | }, 63 | 64 | check_for_arrivals : function() { 65 | var self = this; 66 | self.update_arrivals(); 67 | self.update_expedition_positions(); 68 | }, 69 | 70 | set_expedition_position : function() { 71 | var self = this; 72 | $('[' + this.attr_name() + '=fixed]', self.scope).each(function(idx, el) { 73 | var expedition = $(this), 74 | settings = expedition.data('magellan-expedition-init'), 75 | styles = expedition.attr('styles'), // save styles 76 | top_offset; 77 | 78 | expedition.attr('style', ''); 79 | top_offset = expedition.offset().top + settings.threshold; 80 | 81 | expedition.data(self.data_attr('magellan-top-offset'), top_offset); 82 | expedition.attr('style', styles); 83 | }); 84 | }, 85 | 86 | update_expedition_positions : function() { 87 | var self = this, 88 | window_top_offset = $(window).scrollTop(); 89 | 90 | $('[' + this.attr_name() + '=fixed]', self.scope).each(function() { 91 | var expedition = $(this), 92 | settings = expedition.data('magellan-expedition-init'), 93 | top_offset = expedition.data('magellan-top-offset'); 94 | 95 | if (window_top_offset >= top_offset) { 96 | // Placeholder allows height calculations to be consistent even when 97 | // appearing to switch between fixed/non-fixed placement 98 | var placeholder = expedition.prev('[' + self.add_namespace('data-magellan-expedition-clone') + ']'); 99 | if (placeholder.length === 0) { 100 | placeholder = expedition.clone(); 101 | placeholder.removeAttr(self.attr_name()); 102 | placeholder.attr(self.add_namespace('data-magellan-expedition-clone'),''); 103 | expedition.before(placeholder); 104 | } 105 | expedition.css({position:'fixed', top: settings.fixed_top}); 106 | } else { 107 | expedition.prev('[' + self.add_namespace('data-magellan-expedition-clone') + ']').remove(); 108 | expedition.attr('style','').removeClass('fixed'); 109 | } 110 | }); 111 | }, 112 | 113 | update_arrivals : function() { 114 | var self = this, 115 | window_top_offset = $(window).scrollTop(); 116 | 117 | $('[' + this.attr_name() + ']', self.scope).each(function() { 118 | var expedition = $(this), 119 | settings = expedition.data(self.attr_name(true) + '-init'), 120 | offsets = self.offsets(expedition, window_top_offset), 121 | arrivals = expedition.find('[' + self.add_namespace('data-magellan-arrival') + ']'), 122 | active_item = false; 123 | offsets.each(function(idx, item) { 124 | if (item.viewport_offset >= item.top_offset) { 125 | var arrivals = expedition.find('[' + self.add_namespace('data-magellan-arrival') + ']'); 126 | arrivals.not(item.arrival).removeClass(settings.active_class); 127 | item.arrival.addClass(settings.active_class); 128 | active_item = true; 129 | return true; 130 | } 131 | }); 132 | 133 | if (!active_item) arrivals.removeClass(settings.active_class); 134 | }); 135 | }, 136 | 137 | offsets : function(expedition, window_offset) { 138 | var self = this, 139 | settings = expedition.data(self.attr_name(true) + '-init'), 140 | viewport_offset = window_offset; 141 | 142 | return expedition.find('[' + self.add_namespace('data-magellan-arrival') + ']').map(function(idx, el) { 143 | var name = $(this).data(self.data_attr('magellan-arrival')), 144 | dest = $('[' + self.add_namespace('data-magellan-destination') + '=' + name + ']'); 145 | if (dest.length > 0) { 146 | var top_offset = dest.offset().top - settings.destination_threshold - expedition.outerHeight(); 147 | return { 148 | destination : dest, 149 | arrival : $(this), 150 | top_offset : top_offset, 151 | viewport_offset : viewport_offset 152 | } 153 | } 154 | }).sort(function(a, b) { 155 | if (a.top_offset < b.top_offset) return -1; 156 | if (a.top_offset > b.top_offset) return 1; 157 | return 0; 158 | }); 159 | }, 160 | 161 | data_attr: function (str) { 162 | if (this.namespace.length > 0) { 163 | return this.namespace + '-' + str; 164 | } 165 | 166 | return str; 167 | }, 168 | 169 | off : function () { 170 | this.S(this.scope).off('.magellan'); 171 | this.S(window).off('.magellan'); 172 | }, 173 | 174 | reflow : function () { 175 | var self = this; 176 | // remove placeholder expeditions used for height calculation purposes 177 | $('[' + self.add_namespace('data-magellan-expedition-clone') + ']', self.scope).remove(); 178 | } 179 | }; 180 | }(jQuery, window, window.document)); 181 | -------------------------------------------------------------------------------- /website/static/js/foundation/foundation.offcanvas.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 'use strict'; 3 | 4 | Foundation.libs.offcanvas = { 5 | name : 'offcanvas', 6 | 7 | version : '5.3.0', 8 | 9 | settings : { 10 | open_method: 'move', 11 | close_on_click: true 12 | }, 13 | 14 | init : function (scope, method, options) { 15 | this.bindings(method, options); 16 | }, 17 | 18 | events : function () { 19 | var self = this, 20 | S = self.S, 21 | move_class = '', 22 | right_postfix = '', 23 | left_postfix = ''; 24 | 25 | if (this.settings.open_method === 'move') { 26 | move_class = 'move-'; 27 | right_postfix = 'right'; 28 | left_postfix = 'left'; 29 | } else if (this.settings.open_method === 'overlap') { 30 | move_class = 'offcanvas-overlap'; 31 | } 32 | 33 | S(this.scope).off('.offcanvas') 34 | .on('click.fndtn.offcanvas', '.left-off-canvas-toggle', function (e) { 35 | self.click_toggle_class(e, move_class + right_postfix); 36 | }) 37 | .on('click.fndtn.offcanvas', '.left-off-canvas-menu a', function (e) { 38 | var settings = self.get_settings(e); 39 | if (settings.close_on_click) { 40 | self.hide.call(self, move_class + right_postfix, self.get_wrapper(e)); 41 | } 42 | }) 43 | .on('click.fndtn.offcanvas', '.right-off-canvas-toggle', function (e) { 44 | self.click_toggle_class(e, move_class + left_postfix); 45 | }) 46 | .on('click.fndtn.offcanvas', '.right-off-canvas-menu a', function (e) { 47 | var settings = self.get_settings(e); 48 | if (settings.close_on_click) { 49 | self.hide.call(self, move_class + left_postfix, self.get_wrapper(e)); 50 | } 51 | }) 52 | .on('click.fndtn.offcanvas', '.exit-off-canvas', function (e) { 53 | self.click_remove_class(e, move_class + left_postfix); 54 | if (right_postfix) self.click_remove_class(e, move_class + right_postfix); 55 | }); 56 | 57 | }, 58 | 59 | toggle: function(class_name, $off_canvas) { 60 | $off_canvas = $off_canvas || this.get_wrapper(); 61 | if ($off_canvas.is('.' + class_name)) { 62 | this.hide(class_name, $off_canvas); 63 | } else { 64 | this.show(class_name, $off_canvas); 65 | } 66 | }, 67 | 68 | show: function(class_name, $off_canvas) { 69 | $off_canvas = $off_canvas || this.get_wrapper(); 70 | $off_canvas.trigger('open').trigger('open.fndtn.offcanvas'); 71 | $off_canvas.addClass(class_name); 72 | }, 73 | 74 | hide: function(class_name, $off_canvas) { 75 | $off_canvas = $off_canvas || this.get_wrapper(); 76 | $off_canvas.trigger('close').trigger('close.fndtn.offcanvas'); 77 | $off_canvas.removeClass(class_name); 78 | }, 79 | 80 | click_toggle_class: function(e, class_name) { 81 | e.preventDefault(); 82 | var $off_canvas = this.get_wrapper(e); 83 | this.toggle(class_name, $off_canvas); 84 | }, 85 | 86 | click_remove_class: function(e, class_name) { 87 | e.preventDefault(); 88 | var $off_canvas = this.get_wrapper(e); 89 | this.hide(class_name, $off_canvas); 90 | }, 91 | 92 | get_settings: function(e) { 93 | var offcanvas = this.S(e.target).closest('[' + this.attr_name() + ']'); 94 | return offcanvas.data(this.attr_name(true) + '-init') || this.settings; 95 | }, 96 | 97 | get_wrapper: function(e) { 98 | var $off_canvas = this.S(e ? e.target : this.scope).closest('.off-canvas-wrap'); 99 | 100 | if ($off_canvas.length === 0) { 101 | $off_canvas = this.S('.off-canvas-wrap'); 102 | } 103 | return $off_canvas; 104 | }, 105 | 106 | reflow : function () {} 107 | }; 108 | }(jQuery, window, window.document)); 109 | -------------------------------------------------------------------------------- /website/static/js/foundation/foundation.tab.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 'use strict'; 3 | 4 | Foundation.libs.tab = { 5 | name : 'tab', 6 | 7 | version : '5.3.0', 8 | 9 | settings : { 10 | active_class: 'active', 11 | callback : function () {}, 12 | deep_linking: false, 13 | scroll_to_content: true, 14 | is_hover: false 15 | }, 16 | 17 | default_tab_hashes: [], 18 | 19 | init : function (scope, method, options) { 20 | var self = this, 21 | S = this.S; 22 | 23 | this.bindings(method, options); 24 | this.handle_location_hash_change(); 25 | 26 | // Store the default active tabs which will be referenced when the 27 | // location hash is absent, as in the case of navigating the tabs and 28 | // returning to the first viewing via the browser Back button. 29 | S('[' + this.attr_name() + '] > .active > a', this.scope).each(function () { 30 | self.default_tab_hashes.push(this.hash); 31 | }); 32 | }, 33 | 34 | events : function () { 35 | var self = this, 36 | S = this.S; 37 | 38 | S(this.scope) 39 | .off('.tab') 40 | // Click event: tab title 41 | .on('click.fndtn.tab', '[' + this.attr_name() + '] > * > a', function (e) { 42 | var settings = S(this).closest('[' + self.attr_name() +']').data(self.attr_name(true) + '-init'); 43 | if (!settings.is_hover || Modernizr.touch) { 44 | e.preventDefault(); 45 | e.stopPropagation(); 46 | self.toggle_active_tab(S(this).parent()); 47 | } 48 | }) 49 | // Hover event: tab title 50 | .on('mouseenter.fndtn.tab', '[' + this.attr_name() + '] > * > a', function (e) { 51 | var settings = S(this).closest('[' + self.attr_name() +']').data(self.attr_name(true) + '-init'); 52 | if (settings.is_hover) self.toggle_active_tab(S(this).parent()); 53 | }); 54 | 55 | // Location hash change event 56 | S(window).on('hashchange.fndtn.tab', function (e) { 57 | e.preventDefault(); 58 | self.handle_location_hash_change(); 59 | }); 60 | }, 61 | 62 | handle_location_hash_change : function () { 63 | var self = this, 64 | S = this.S; 65 | 66 | S('[' + this.attr_name() + ']', this.scope).each(function () { 67 | var settings = S(this).data(self.attr_name(true) + '-init'); 68 | if (settings.deep_linking) { 69 | // Match the location hash to a label 70 | var hash = self.scope.location.hash; 71 | if (hash != '') { 72 | // Check whether the location hash references a tab content div or 73 | // another element on the page (inside or outside the tab content div) 74 | var hash_element = S(hash); 75 | if (hash_element.hasClass('content') && hash_element.parent().hasClass('tab-content')) { 76 | // Tab content div 77 | self.toggle_active_tab($('[' + self.attr_name() + '] > * > a[href=' + hash + ']').parent()); 78 | } else { 79 | // Not the tab content div. If inside the tab content, find the 80 | // containing tab and toggle it as active. 81 | var hash_tab_container_id = hash_element.closest('.content').attr('id'); 82 | if (hash_tab_container_id != undefined) { 83 | self.toggle_active_tab($('[' + self.attr_name() + '] > * > a[href=#' + hash_tab_container_id + ']').parent(), hash); 84 | } 85 | } 86 | } else { 87 | // Reference the default tab hashes which were initialized in the init function 88 | for (var ind in self.default_tab_hashes) { 89 | self.toggle_active_tab($('[' + self.attr_name() + '] > * > a[href=' + self.default_tab_hashes[ind] + ']').parent()); 90 | } 91 | } 92 | } 93 | }); 94 | }, 95 | 96 | toggle_active_tab: function (tab, location_hash) { 97 | var S = this.S, 98 | tabs = tab.closest('[' + this.attr_name() + ']'), 99 | anchor = tab.children('a').first(), 100 | target_hash = '#' + anchor.attr('href').split('#')[1], 101 | target = S(target_hash), 102 | siblings = tab.siblings(), 103 | settings = tabs.data(this.attr_name(true) + '-init'); 104 | 105 | // allow usage of data-tab-content attribute instead of href 106 | if (S(this).data(this.data_attr('tab-content'))) { 107 | target_hash = '#' + S(this).data(this.data_attr('tab-content')).split('#')[1]; 108 | target = S(target_hash); 109 | } 110 | 111 | if (settings.deep_linking) { 112 | // Get the scroll Y position prior to moving to the hash ID 113 | var cur_ypos = $('body,html').scrollTop(); 114 | 115 | // Update the location hash to preserve browser history 116 | // Note that the hash does not need to correspond to the 117 | // tab content ID anchor; it can be an ID inside or outside of the tab 118 | // content div. 119 | if (location_hash != undefined) { 120 | window.location.hash = location_hash; 121 | } else { 122 | window.location.hash = target_hash; 123 | } 124 | 125 | if (settings.scroll_to_content) { 126 | // If the user is requesting the content of a tab, then scroll to the 127 | // top of the title area; otherwise, scroll to the element within 128 | // the content area as defined by the hash value. 129 | if (location_hash == undefined || location_hash == target_hash) { 130 | tab.parent()[0].scrollIntoView(); 131 | } else { 132 | S(target_hash)[0].scrollIntoView(); 133 | } 134 | } else { 135 | // Adjust the scrollbar to the Y position prior to setting the hash 136 | // Only do this for the tab content anchor, otherwise there will be 137 | // conflicts with in-tab anchor links nested in the tab-content div 138 | if (location_hash == undefined || location_hash == target_hash) { 139 | $('body,html').scrollTop(cur_ypos); 140 | } 141 | } 142 | } 143 | 144 | // WARNING: The activation and deactivation of the tab content must 145 | // occur after the deep linking in order to properly refresh the browser 146 | // window (notably in Chrome). 147 | tab.addClass(settings.active_class).triggerHandler('opened'); 148 | siblings.removeClass(settings.active_class); 149 | target.siblings().removeClass(settings.active_class).end().addClass(settings.active_class); 150 | settings.callback(tab); 151 | target.triggerHandler('toggled', [tab]); 152 | tabs.triggerHandler('toggled', [target]); 153 | }, 154 | 155 | data_attr: function (str) { 156 | if (this.namespace.length > 0) { 157 | return this.namespace + '-' + str; 158 | } 159 | 160 | return str; 161 | }, 162 | 163 | off : function () {}, 164 | 165 | reflow : function () {} 166 | }; 167 | }(jQuery, window, window.document)); 168 | -------------------------------------------------------------------------------- /website/static/js/vendor/fastclick.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs. 3 | * 4 | * @version 1.0.2 5 | * @codingstandard ftlabs-jsv2 6 | * @copyright The Financial Times Limited [All Rights Reserved] 7 | * @license MIT License (see LICENSE.txt) 8 | */ 9 | function FastClick(a,b){"use strict";function c(a,b){return function(){return a.apply(b,arguments)}}var d;if(b=b||{},this.trackingClick=!1,this.trackingClickStart=0,this.targetElement=null,this.touchStartX=0,this.touchStartY=0,this.lastTouchIdentifier=0,this.touchBoundary=b.touchBoundary||10,this.layer=a,this.tapDelay=b.tapDelay||200,!FastClick.notNeeded(a)){for(var e=["onMouse","onClick","onTouchStart","onTouchMove","onTouchEnd","onTouchCancel"],f=this,g=0,h=e.length;h>g;g++)f[e[g]]=c(f[e[g]],f);deviceIsAndroid&&(a.addEventListener("mouseover",this.onMouse,!0),a.addEventListener("mousedown",this.onMouse,!0),a.addEventListener("mouseup",this.onMouse,!0)),a.addEventListener("click",this.onClick,!0),a.addEventListener("touchstart",this.onTouchStart,!1),a.addEventListener("touchmove",this.onTouchMove,!1),a.addEventListener("touchend",this.onTouchEnd,!1),a.addEventListener("touchcancel",this.onTouchCancel,!1),Event.prototype.stopImmediatePropagation||(a.removeEventListener=function(b,c,d){var e=Node.prototype.removeEventListener;"click"===b?e.call(a,b,c.hijacked||c,d):e.call(a,b,c,d)},a.addEventListener=function(b,c,d){var e=Node.prototype.addEventListener;"click"===b?e.call(a,b,c.hijacked||(c.hijacked=function(a){a.propagationStopped||c(a)}),d):e.call(a,b,c,d)}),"function"==typeof a.onclick&&(d=a.onclick,a.addEventListener("click",function(a){d(a)},!1),a.onclick=null)}}var deviceIsAndroid=navigator.userAgent.indexOf("Android")>0,deviceIsIOS=/iP(ad|hone|od)/.test(navigator.userAgent),deviceIsIOS4=deviceIsIOS&&/OS 4_\d(_\d)?/.test(navigator.userAgent),deviceIsIOSWithBadTarget=deviceIsIOS&&/OS ([6-9]|\d{2})_\d/.test(navigator.userAgent);FastClick.prototype.needsClick=function(a){"use strict";switch(a.nodeName.toLowerCase()){case"button":case"select":case"textarea":if(a.disabled)return!0;break;case"input":if(deviceIsIOS&&"file"===a.type||a.disabled)return!0;break;case"label":case"video":return!0}return/\bneedsclick\b/.test(a.className)},FastClick.prototype.needsFocus=function(a){"use strict";switch(a.nodeName.toLowerCase()){case"textarea":return!0;case"select":return!deviceIsAndroid;case"input":switch(a.type){case"button":case"checkbox":case"file":case"image":case"radio":case"submit":return!1}return!a.disabled&&!a.readOnly;default:return/\bneedsfocus\b/.test(a.className)}},FastClick.prototype.sendClick=function(a,b){"use strict";var c,d;document.activeElement&&document.activeElement!==a&&document.activeElement.blur(),d=b.changedTouches[0],c=document.createEvent("MouseEvents"),c.initMouseEvent(this.determineEventType(a),!0,!0,window,1,d.screenX,d.screenY,d.clientX,d.clientY,!1,!1,!1,!1,0,null),c.forwardedTouchEvent=!0,a.dispatchEvent(c)},FastClick.prototype.determineEventType=function(a){"use strict";return deviceIsAndroid&&"select"===a.tagName.toLowerCase()?"mousedown":"click"},FastClick.prototype.focus=function(a){"use strict";var b;deviceIsIOS&&a.setSelectionRange&&0!==a.type.indexOf("date")&&"time"!==a.type?(b=a.value.length,a.setSelectionRange(b,b)):a.focus()},FastClick.prototype.updateScrollParent=function(a){"use strict";var b,c;if(b=a.fastClickScrollParent,!b||!b.contains(a)){c=a;do{if(c.scrollHeight>c.offsetHeight){b=c,a.fastClickScrollParent=c;break}c=c.parentElement}while(c)}b&&(b.fastClickLastScrollTop=b.scrollTop)},FastClick.prototype.getTargetElementFromEventTarget=function(a){"use strict";return a.nodeType===Node.TEXT_NODE?a.parentNode:a},FastClick.prototype.onTouchStart=function(a){"use strict";var b,c,d;if(a.targetTouches.length>1)return!0;if(b=this.getTargetElementFromEventTarget(a.target),c=a.targetTouches[0],deviceIsIOS){if(d=window.getSelection(),d.rangeCount&&!d.isCollapsed)return!0;if(!deviceIsIOS4){if(c.identifier===this.lastTouchIdentifier)return a.preventDefault(),!1;this.lastTouchIdentifier=c.identifier,this.updateScrollParent(b)}}return this.trackingClick=!0,this.trackingClickStart=a.timeStamp,this.targetElement=b,this.touchStartX=c.pageX,this.touchStartY=c.pageY,a.timeStamp-this.lastClickTimec||Math.abs(b.pageY-this.touchStartY)>c?!0:!1},FastClick.prototype.onTouchMove=function(a){"use strict";return this.trackingClick?((this.targetElement!==this.getTargetElementFromEventTarget(a.target)||this.touchHasMoved(a))&&(this.trackingClick=!1,this.targetElement=null),!0):!0},FastClick.prototype.findControl=function(a){"use strict";return void 0!==a.control?a.control:a.htmlFor?document.getElementById(a.htmlFor):a.querySelector("button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea")},FastClick.prototype.onTouchEnd=function(a){"use strict";var b,c,d,e,f,g=this.targetElement;if(!this.trackingClick)return!0;if(a.timeStamp-this.lastClickTime100||deviceIsIOS&&window.top!==window&&"input"===d?(this.targetElement=null,!1):(this.focus(g),this.sendClick(g,a),deviceIsIOS&&"select"===d||(this.targetElement=null,a.preventDefault()),!1);return deviceIsIOS&&!deviceIsIOS4&&(e=g.fastClickScrollParent,e&&e.fastClickLastScrollTop!==e.scrollTop)?!0:(this.needsClick(g)||(a.preventDefault(),this.sendClick(g,a)),!1)},FastClick.prototype.onTouchCancel=function(){"use strict";this.trackingClick=!1,this.targetElement=null},FastClick.prototype.onMouse=function(a){"use strict";return this.targetElement?a.forwardedTouchEvent?!0:a.cancelable&&(!this.needsClick(this.targetElement)||this.cancelNextClick)?(a.stopImmediatePropagation?a.stopImmediatePropagation():a.propagationStopped=!0,a.stopPropagation(),a.preventDefault(),!1):!0:!0},FastClick.prototype.onClick=function(a){"use strict";var b;return this.trackingClick?(this.targetElement=null,this.trackingClick=!1,!0):"submit"===a.target.type&&0===a.detail?!0:(b=this.onMouse(a),b||(this.targetElement=null),b)},FastClick.prototype.destroy=function(){"use strict";var a=this.layer;deviceIsAndroid&&(a.removeEventListener("mouseover",this.onMouse,!0),a.removeEventListener("mousedown",this.onMouse,!0),a.removeEventListener("mouseup",this.onMouse,!0)),a.removeEventListener("click",this.onClick,!0),a.removeEventListener("touchstart",this.onTouchStart,!1),a.removeEventListener("touchmove",this.onTouchMove,!1),a.removeEventListener("touchend",this.onTouchEnd,!1),a.removeEventListener("touchcancel",this.onTouchCancel,!1)},FastClick.notNeeded=function(a){"use strict";var b,c;if("undefined"==typeof window.ontouchstart)return!0;if(c=+(/Chrome\/([0-9]+)/.exec(navigator.userAgent)||[,0])[1]){if(!deviceIsAndroid)return!0;if(b=document.querySelector("meta[name=viewport]")){if(-1!==b.content.indexOf("user-scalable=no"))return!0;if(c>31&&document.documentElement.scrollWidth<=window.outerWidth)return!0}}return"none"===a.style.msTouchAction?!0:!1},FastClick.attach=function(a,b){"use strict";return new FastClick(a,b)},"function"==typeof define&&"object"==typeof define.amd&&define.amd?define(function(){"use strict";return FastClick}):"undefined"!=typeof module&&module.exports?(module.exports=FastClick.attach,module.exports.FastClick=FastClick):window.FastClick=FastClick; 10 | -------------------------------------------------------------------------------- /website/static/js/vendor/jquery.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin v1.4.1 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2013 Klaus Hartl 6 | * Released under the MIT license 7 | */ 8 | !function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){function b(a){return h.raw?a:encodeURIComponent(a)}function c(a){return h.raw?a:decodeURIComponent(a)}function d(a){return b(h.json?JSON.stringify(a):String(a))}function e(a){0===a.indexOf('"')&&(a=a.slice(1,-1).replace(/\\"/g,'"').replace(/\\\\/g,"\\"));try{return a=decodeURIComponent(a.replace(g," ")),h.json?JSON.parse(a):a}catch(b){}}function f(b,c){var d=h.raw?b:e(b);return a.isFunction(c)?c(d):d}var g=/\+/g,h=a.cookie=function(e,g,i){if(void 0!==g&&!a.isFunction(g)){if(i=a.extend({},h.defaults,i),"number"==typeof i.expires){var j=i.expires,k=i.expires=new Date;k.setTime(+k+864e5*j)}return document.cookie=[b(e),"=",d(g),i.expires?"; expires="+i.expires.toUTCString():"",i.path?"; path="+i.path:"",i.domain?"; domain="+i.domain:"",i.secure?"; secure":""].join("")}for(var l=e?void 0:{},m=document.cookie?document.cookie.split("; "):[],n=0,o=m.length;o>n;n++){var p=m[n].split("="),q=c(p.shift()),r=p.join("=");if(e&&e===q){l=f(r,g);break}e||void 0===(r=f(r))||(l[q]=r)}return l};h.defaults={},a.removeCookie=function(b,c){return void 0===a.cookie(b)?!1:(a.cookie(b,"",a.extend({},c,{expires:-1})),!a.cookie(b))}}); 9 | -------------------------------------------------------------------------------- /website/static/js/vendor/placeholder.js: -------------------------------------------------------------------------------- 1 | /*! http://mths.be/placeholder v2.0.8 by @mathias */ 2 | !function(a,b,c){function d(a){var b={},d=/^jQuery\d+$/;return c.each(a.attributes,function(a,c){c.specified&&!d.test(c.name)&&(b[c.name]=c.value)}),b}function e(a,b){var d=this,e=c(d);if(d.value==e.attr("placeholder")&&e.hasClass("placeholder"))if(e.data("placeholder-password")){if(e=e.hide().next().show().attr("id",e.removeAttr("id").data("placeholder-id")),a===!0)return e[0].value=b;e.focus()}else d.value="",e.removeClass("placeholder"),d==g()&&d.select()}function f(){var a,b=this,f=c(b),g=this.id;if(""==b.value){if("password"==b.type){if(!f.data("placeholder-textinput")){try{a=f.clone().attr({type:"text"})}catch(h){a=c("").attr(c.extend(d(this),{type:"text"}))}a.removeAttr("name").data({"placeholder-password":f,"placeholder-id":g}).bind("focus.placeholder",e),f.data({"placeholder-textinput":a,"placeholder-id":g}).before(a)}f=f.removeAttr("id").hide().prev().attr("id",g).show()}f.addClass("placeholder"),f[0].value=f.attr("placeholder")}else f.removeClass("placeholder")}function g(){try{return b.activeElement}catch(a){}}var h,i,j="[object OperaMini]"==Object.prototype.toString.call(a.operamini),k="placeholder"in b.createElement("input")&&!j,l="placeholder"in b.createElement("textarea")&&!j,m=c.fn,n=c.valHooks,o=c.propHooks;k&&l?(i=m.placeholder=function(){return this},i.input=i.textarea=!0):(i=m.placeholder=function(){var a=this;return a.filter((k?"textarea":":input")+"[placeholder]").not(".placeholder").bind({"focus.placeholder":e,"blur.placeholder":f}).data("placeholder-enabled",!0).trigger("blur.placeholder"),a},i.input=k,i.textarea=l,h={get:function(a){var b=c(a),d=b.data("placeholder-password");return d?d[0].value:b.data("placeholder-enabled")&&b.hasClass("placeholder")?"":a.value},set:function(a,b){var d=c(a),h=d.data("placeholder-password");return h?h[0].value=b:d.data("placeholder-enabled")?(""==b?(a.value=b,a!=g()&&f.call(a)):d.hasClass("placeholder")?e.call(a,!0,b)||(a.value=b):a.value=b,d):a.value=b}},k||(n.input=h,o.value=h),l||(n.textarea=h,o.value=h),c(function(){c(b).delegate("form","submit.placeholder",function(){var a=c(".placeholder",this).each(e);setTimeout(function(){a.each(f)},10)})}),c(a).bind("beforeunload.placeholder",function(){c(".placeholder").each(function(){this.value=""})}))}(this,document,jQuery); 3 | -------------------------------------------------------------------------------- /website/static/js/vpnview.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | $("#showpass").bind('click', function (event) { 3 | event.preventDefault(); 4 | if ($("#password")[0].type == 'password') { 5 | $("#password")[0].type = 'text'; 6 | $("#showpass").html('隐藏密码'); 7 | } else { 8 | $("#password")[0].type = 'password'; 9 | $("#showpass").html('显示密码'); 10 | } 11 | }); 12 | })(); 13 | -------------------------------------------------------------------------------- /website/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}{% endblock title %} | GateWay 9 | 10 | 11 | 12 | 13 | 14 | {% block head %}{% endblock head %} 15 | 16 | 17 | 18 | 19 | 20 | {% block header %} 21 | 22 |
    23 |
    24 |
    25 |

    Flex GateWay

    26 |
    27 |
    28 |
    29 | 30 | {% endblock header %} 31 | 32 | {% block header_bar %} 33 | 34 | 59 | 60 | {% endblock header_bar %} 61 | 62 | {% block trail_all %} 63 | 64 |
    65 |
    66 | 69 |
    70 |
    71 | 72 | {% endblock trail_all %} 73 | 74 | 75 |
    76 | {% block content %}{% endblock content %} 77 |
    78 | 79 | 80 | 81 |
    82 |
    83 | 84 | a vpn, snat web app for ecs. 85 | 86 |
    87 |
    88 | 89 | © 2014-2015 Flex GateWay Project. 90 | 91 |
    92 |
    93 | 94 | 95 | 96 | 97 | 98 | 101 | {% block jsplugin %}{% endblock jsplugin %} 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /website/templates/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %} {% endblock %} 3 | {% block content %} 4 | 5 |
    6 | 11 |
    12 | 13 | 14 |
    15 |
    16 |
    17 |
    18 |
    19 | 20 |
    21 |
    22 | 23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 | 30 |
    31 |
    32 | 33 |
    34 |
    35 |
    36 |
    37 |
    38 |
    39 | 40 |
    41 |
    42 |
    43 |
    44 |
    45 | 46 | {% endblock content %} 47 | 48 | -------------------------------------------------------------------------------- /website/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.views 4 | ~~~~~~~~~~~~~ 5 | 6 | top level views. 7 | """ 8 | 9 | 10 | from flask import g, redirect, url_for 11 | 12 | from flask.ext.login import login_required, current_user 13 | 14 | from website import app 15 | 16 | 17 | @app.before_request 18 | def before_request(): 19 | g.account = None 20 | if not current_user.is_anonymous(): 21 | g.account = current_user.username 22 | 23 | 24 | @app.route('/') 25 | @login_required 26 | def default(): 27 | return redirect(url_for('sts.index')) 28 | -------------------------------------------------------------------------------- /website/vpn/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.vpn 4 | ~~~~~~~~~~~ 5 | 6 | website vpn blueprint. 7 | """ 8 | -------------------------------------------------------------------------------- /website/vpn/dial/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.vpn.dial 4 | ~~~~~~~~~~~~~~~~ 5 | 6 | website vpn dial blueprint. 7 | """ 8 | -------------------------------------------------------------------------------- /website/vpn/dial/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.vpn.dial.forms 4 | ~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | vpn forms: 7 | /vpn/dial/add 8 | /vpn/dial/settings 9 | /vpn/dial//settings 10 | """ 11 | 12 | 13 | from flask_wtf import Form 14 | from wtforms import StringField, SubmitField, TextAreaField, SelectField 15 | from wtforms import ValidationError 16 | from wtforms.validators import DataRequired, Length, Regexp 17 | 18 | 19 | def _ipool(value): 20 | try: 21 | ip = value.split('/')[0] 22 | mask = int(value.split('/')[1]) 23 | except: 24 | return False 25 | if mask < 0 or mask > 32: 26 | return False 27 | parts = ip.split('.') 28 | if len(parts) == 4 and all(x.isdigit() for x in parts): 29 | numbers = list(int(x) for x in parts) 30 | if not all(num >= 0 and num < 256 for num in numbers): 31 | return False 32 | return True 33 | return False 34 | 35 | 36 | def IPool(message=u"无效的地址段"): 37 | def __ipool(form, field): 38 | value = field.data 39 | if not _ipool(value): 40 | raise ValidationError(message) 41 | return __ipool 42 | 43 | 44 | def SubNets(message=u"无效的子网"): 45 | def __subnets(form, field): 46 | value = field.data 47 | parts = [i.strip() for i in value.split(',')] 48 | if not all(_ipool(part) for part in parts): 49 | raise ValidationError(message) 50 | return __subnets 51 | 52 | 53 | class AddForm(Form): 54 | name = StringField(u'账号名', 55 | validators=[DataRequired(message=u'这是一个必选项!'), 56 | Length(max=20, message=u'帐号最长为20个字符!'), 57 | Regexp(r'^[\w]+$', message=u"只可包含如下字符:数字、字母、下划线!")]) 58 | password = StringField(u'密码', 59 | validators=[DataRequired(message=u'这是一个必选项!'), 60 | Length(max=20, message=u'密码最长为20个字符!'), 61 | Regexp(r'^[\w]+$', message=u"只可包含如下字符:数字、字母、下划线!")]) 62 | #: submit button 63 | save = SubmitField(u'保存') 64 | delete = SubmitField(u'删除') 65 | 66 | 67 | class SettingsForm(Form): 68 | ipool = StringField(u'虚拟IP 地址池', 69 | validators=[DataRequired(message=u'这是一个必选项!'), 70 | IPool(message=u"无效的IP 地址池")]) 71 | subnet = TextAreaField(u'子网网段', 72 | validators=[DataRequired(message=u'这是一个必选项!'), 73 | SubNets(message=u"无效的子网")]) 74 | c2c = SelectField(u'允许client 间通信', 75 | choices=[('no', u'否'), ('yes', u'是')]) 76 | duplicate = SelectField(u'允许单个账号同时在线', 77 | choices=[('no', u'否'), ('yes', u'是')]) 78 | proto = SelectField(u'通信协议', 79 | choices=[('udp', u'UDP'), ('tcp', u'TCP')]) 80 | 81 | 82 | class ConsoleForm(Form): 83 | '''web console form''' 84 | #: submit button 85 | stop = SubmitField(u'关闭') 86 | start = SubmitField(u'启动') 87 | re_load = SubmitField(u'下发&重启') 88 | -------------------------------------------------------------------------------- /website/vpn/dial/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.vpn.dial.helpers 4 | ~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | vpn dial helpers api. 7 | """ 8 | 9 | 10 | import sys 11 | 12 | from flask import current_app 13 | 14 | from website.services import exec_command 15 | 16 | 17 | def exchange_maskint(mask_int): 18 | bin_arr = ['0' for i in range(32)] 19 | 20 | for i in xrange(mask_int): 21 | bin_arr[i] = '1' 22 | tmpmask = [''.join(bin_arr[i * 8:i * 8 + 8]) for i in range(4)] 23 | tmpmask = [str(int(tmpstr, 2)) for tmpstr in tmpmask] 24 | return '.'.join(tmpmask) 25 | 26 | 27 | def get_localhost_ip(): 28 | cmd = ['/sbin/ifconfig'] 29 | eth_ip = {} 30 | try: 31 | r = exec_command(cmd) 32 | except: 33 | current_app.logger.error('[Dial Helpers]: exec_command error: %s:%s', cmd, 34 | sys.exc_info()[1]) 35 | return False 36 | if r['return_code'] == 0: 37 | r_data = r['stdout'].split('\n') 38 | for index, line in enumerate(r_data): 39 | if line.startswith('inet addr:'): 40 | eth_ip[r_data[index-1].split()[0]] = line.split().split(':')[1] 41 | else: 42 | current_app.logger.error('[Dial Helpers]: exec_command return: %s:%s:%s', cmd, 43 | r['return_code'], r['stderr']) 44 | return False 45 | return eth_ip 46 | -------------------------------------------------------------------------------- /website/vpn/dial/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.vpn.dial.models 4 | ~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | vpn dial system models. 7 | """ 8 | 9 | 10 | from datetime import datetime 11 | 12 | from website import db 13 | 14 | 15 | class Account(db.Model): 16 | '''dial name.''' 17 | __tablename__ = 'dial_account' 18 | 19 | id = db.Column(db.Integer, primary_key=True) 20 | name = db.Column(db.String(80), unique=True, index=True) 21 | password = db.Column(db.String(80)) 22 | created_at = db.Column(db.DateTime) 23 | 24 | def __init__(self, name, password, created_at=datetime.now()): 25 | self.name = name 26 | self.password = password 27 | self.created_at = created_at 28 | 29 | def __repr__(self): 30 | return '' % (self.name, self.created_at) 31 | 32 | def get_id(self): 33 | return unicode(self.id) 34 | 35 | 36 | class Settings(db.Model): 37 | """settings for dial or common settings.""" 38 | __tablename__ = 'dial_settings' 39 | 40 | id = db.Column(db.Integer, primary_key=True) 41 | ipool = db.Column(db.String(80)) 42 | subnet = db.Column(db.String(80)) 43 | c2c = db.Column(db.Boolean) 44 | duplicate = db.Column(db.Boolean) 45 | proto = db.Column(db.String(80)) 46 | 47 | def __init__(self, ipool, subnet, c2c, duplicate, proto): 48 | self.ipool = ipool 49 | self.subnet = subnet 50 | self.c2c = c2c 51 | self.duplicate = duplicate 52 | self.proto = proto 53 | 54 | def __repr__(self): 55 | return '' % self.id 56 | -------------------------------------------------------------------------------- /website/vpn/dial/static/openvpn-install-2.4.6-I602.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ostaer/FlexGW/58304b17ec71d5f7ffd5c5bf0271174005363b40/website/vpn/dial/static/openvpn-install-2.4.6-I602.exe -------------------------------------------------------------------------------- /website/vpn/dial/templates/dial/add.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}VPN 新增拨号{% endblock title%} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • VPN
  • 7 |
  • Dial
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "dial/sidenav.html" %} 12 | 13 | 14 |
    15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 | {% for category, message in messages %} 18 |
    19 | {{ message }} 20 | × 21 |
    22 | {% endfor %} 23 | {% endif %} 24 | {% endwith %} 25 |
    26 | 注意:「账号名」只可包含如下字符:数字、字母、下划线。 27 |
    28 |
    29 |
    30 |
    31 | {{ form.name.label(class="right inline") }} 32 |
    33 |
    34 | {% if form.name.errors %} 35 | {{ form.name(class="error", value=form.name.value) }} 36 | {{ form.name.errors[0] }} 37 | {% else %} 38 | {{ form.name(placeholder="test_1") }} 39 | {% endif %} 40 |
    41 |
    42 | 43 |
    44 |
    45 | {{ form.password.label(class="right inline") }} 46 |
    47 |
    48 | {% if form.password.errors %} 49 | {{ form.password(class="error", type="password", value=form.password.value) }} 50 | {{ form.password.errors[0] }} 51 | {% else %} 52 | {{ form.password(type="password", placeholder="password") }} 53 | {% endif %} 54 |
    55 |
    56 | 57 |
    58 |
    59 | 60 | 61 |
    62 |
    63 |
    64 |
    65 | 66 | {% endblock content %} 67 | 68 | -------------------------------------------------------------------------------- /website/vpn/dial/templates/dial/client.conf: -------------------------------------------------------------------------------- 1 | client 2 | dev tun 3 | {% if proto == 'tcp' %} 4 | proto tcp 5 | {% else %} 6 | proto udp 7 | {% endif %} 8 | 9 | # please replace EIP to your real eip address. 10 | remote EIP 1194 11 | 12 | resolv-retry infinite 13 | pull 14 | nobind 15 | persist-key 16 | persist-tun 17 | comp-lzo 18 | verb 3 19 | ca ca.crt 20 | cipher AES-128-CBC 21 | auth-user-pass 22 | -------------------------------------------------------------------------------- /website/vpn/dial/templates/dial/console.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}VPN Console{% endblock title%} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • VPN
  • 7 |
  • VPN Console
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "dial/sidenav.html" %} 12 | 13 | 14 |
    15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 | {% for category, message in messages %} 18 |
    19 | {{ message }} 20 | × 21 |
    22 | {% endfor %} 23 | {% endif %} 24 | {% endwith %} 25 |
    26 | {% if status %} 27 | OpenVPN 服务运行正常! 28 | {% else %} 29 | OpenVPN 没有运行! 30 | {% endif %} 31 |
    32 |
    33 |
    34 |
    35 |
      36 |
    • 启动VPN 服务
    • 37 |
    • 启动VPN 服务。不会影响到已经运行的VPN 服务。
    • 38 |
    • 39 | {% if status %} 40 | {{ form.start(class="alert tiny button disabled", disabled="disabled") }} 41 | {% else %} 42 | {{ form.start(class="alert tiny button") }} 43 | {% endif %} 44 |
    • 45 |
    46 |
    47 |
    48 |
      49 |
    • 停止VPN 服务
    • 50 |
    • 停止VPN 服务。服务停止之后,所有已经连接的隧道都将中断。
    • 51 |
    • 52 | {% if status %} 53 | {{ form.stop(class="alert tiny button") }} 54 | {% else %} 55 | {{ form.stop(class="alert tiny button disabled", disabled="disabled") }} 56 | {% endif %} 57 |
    • 58 |
    59 |
    60 |
    61 |
      62 |
    • 配置下发&重启
    • 63 |
    • 下发新的VPN 配置,并使之生效。建议在修改VPN 配置之后再进行使用。
    • 64 |
    • 65 | {% if status %} 66 | {{ form.re_load(class="alert tiny button") }} 67 | {% else %} 68 | {{ form.re_load(class="alert tiny button disabled", disabled="disabled") }} 69 | {% endif %} 70 |
    • 71 |
    72 |
    73 | 74 |
    75 |
    76 |
    77 | 78 | {% endblock content %} 79 | -------------------------------------------------------------------------------- /website/vpn/dial/templates/dial/download.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}VPN Dial{% endblock title%} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • VPN
  • 7 |
  • Dial
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "dial/sidenav.html" %} 12 | 13 | 14 |
    15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 | {% for category, message in messages %} 18 |
    19 | {{ message }} 20 | × 21 |
    22 | {% endfor %} 23 | {% endif %} 24 | {% endwith %} 25 |
    26 | 注意:「配置」文件下载后,请将配置文件中的「remote IP」字段修改为本机所绑定的IP 地址。 27 |
    28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
    说明客户端配置文件
    Installer, Windows 7 and lateropenvpn-install-2.4.6-I602.exewindows-openvpn-client.zip
    Centossudo yum install openvpnlinux-openvpn-client.tar.gz
    Debian/Ubuntusudo apt-get install openvpnlinux-openvpn-client.tar.gz
    54 |
    55 | 56 | {% endblock content %} 57 | 58 | -------------------------------------------------------------------------------- /website/vpn/dial/templates/dial/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}VPN Dial{% endblock title%} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • VPN
  • 7 |
  • Dial
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "dial/sidenav.html" %} 12 | 13 | 14 |
    15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 | {% for category, message in messages %} 18 |
    19 | {{ message }} 20 | × 21 |
    22 | {% endfor %} 23 | {% endif %} 24 | {% endwith %} 25 |
    26 | 注意:「状态」由于VPN 的keepalive 机制,会有1分钟左右的延时。 27 |
    28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {% if accounts %} 43 | {% for account in accounts %} 44 | 45 | 46 | 47 | 48 | 55 | 56 | 57 | 58 | 59 | 60 | {% endfor %} 61 | {% endif %} 62 | 63 |
    账号名真实IP虚拟IP状态Bytes ReceivedBytes Sent连接时间账号创建时间
    {{ account.name }}{{ account.rip }}{{ account.vip }} 49 | {% if account.vip %} 50 | Online 51 | {% else %} 52 | Offline 53 | {% endif %} 54 | {{ account.br }}{{ account.bs }}{{ account.ct }}{{ account.created_at }}
    64 |
    65 | 66 | {% endblock content %} 67 | 68 | -------------------------------------------------------------------------------- /website/vpn/dial/templates/dial/server.conf: -------------------------------------------------------------------------------- 1 | # OpenVPN 2.0 introduces a new mode ("server") 2 | # which implements a multi-client server capability. 3 | mode server 4 | 5 | # Which local IP address should OpenVPN 6 | # listen on? (optional) 7 | local 0.0.0.0 8 | 9 | # Which TCP/UDP port should OpenVPN listen on? 10 | # If you want to run multiple OpenVPN instances 11 | # on the same machine, use a different port 12 | # number for each one. You will need to 13 | # open up this port on your firewall. 14 | port 1194 15 | 16 | # TCP or UDP server? 17 | {% if proto == 'tcp' %} 18 | proto tcp 19 | {% else %} 20 | proto udp 21 | {% endif %} 22 | 23 | # "dev tun" will create a routed IP tunnel, 24 | # "dev tap" will create an ethernet tunnel. 25 | # Use "dev tap0" if you are ethernet bridging 26 | # and have precreated a tap0 virtual interface 27 | # and bridged it with your ethernet interface. 28 | # If you want to control access policies 29 | # over the VPN, you must create firewall 30 | # rules for the the TUN/TAP interface. 31 | # On non-Windows systems, you can give 32 | # an explicit unit number, such as tun0. 33 | # On Windows, use "dev-node" for this. 34 | # On most systems, the VPN will not function 35 | # unless you partially or fully disable 36 | # the firewall for the TUN/TAP interface. 37 | dev tun 38 | 39 | # SSL/TLS root certificate (ca), certificate 40 | # (cert), and private key (key). Each client 41 | # and the server must have their own cert and 42 | # key file. The server and all clients will 43 | # use the same ca file. 44 | # 45 | # See the "easy-rsa" directory for a series 46 | # of scripts for generating RSA certificates 47 | # and private keys. Remember to use 48 | # a unique Common Name for the server 49 | # and each of the client certificates. 50 | # 51 | # Any X509 key management system can be used. 52 | # OpenVPN can also use a PKCS #12 formatted key file 53 | # (see "pkcs12" directive in man page). 54 | ca ca.crt 55 | cert server.crt 56 | key server.key # This file should be kept secret 57 | 58 | # Diffie hellman parameters. 59 | # Generate your own with: 60 | # openssl dhparam -out dh1024.pem 1024 61 | # Substitute 2048 for 1024 if you are using 62 | # 2048 bit keys. 63 | dh dh1024.pem 64 | 65 | # Configure server mode and supply a VPN subnet 66 | # for OpenVPN to draw client addresses from. 67 | # The server will take 10.8.0.1 for itself, 68 | # the rest will be made available to clients. 69 | # Each client will be able to reach the server 70 | # on 10.8.0.1. Comment this line out if you are 71 | # ethernet bridging. See the man page for more info. 72 | {% if ipool %} 73 | server {{ ipool }} 74 | {% endif %} 75 | 76 | # Maintain a record of client <-> virtual IP address 77 | # associations in this file. If OpenVPN goes down or 78 | # is restarted, reconnecting clients can be assigned 79 | # the same virtual IP address from the pool that was 80 | # previously assigned. 81 | ifconfig-pool-persist ipp.txt 82 | 83 | # Push routes to the client to allow it 84 | # to reach other private subnets behind 85 | # the server. Remember that these 86 | # private subnets will also need 87 | # to know to route the OpenVPN client 88 | # address pool (10.8.0.0/255.255.255.0) 89 | # back to the OpenVPN server. 90 | {% if subnets %} 91 | {% for subnet in subnets %} 92 | push "route {{ subnet }}" 93 | {% endfor %} 94 | {% endif %} 95 | 96 | # The keepalive directive causes ping-like 97 | # messages to be sent back and forth over 98 | # the link so that each side knows when 99 | # the other side has gone down. 100 | # Ping every 10 seconds, assume that remote 101 | # peer is down if no ping received during 102 | # a 60 second time period. 103 | keepalive 5 60 104 | 105 | # Select a cryptographic cipher. 106 | # This config item must be copied to 107 | # the client config file as well. 108 | cipher AES-128-CBC # AES 109 | 110 | # Enable compression on the VPN link. 111 | # If you enable it here, you must also 112 | # enable it in the client config file. 113 | comp-lzo 114 | 115 | # The persist options will try to avoid 116 | # accessing certain resources on restart 117 | # that may no longer be accessible because 118 | # of the privilege downgrade. 119 | persist-key 120 | persist-tun 121 | 122 | # Output a short status file showing 123 | # current connections, truncated 124 | # and rewritten every minute. 125 | status openvpn-status.log 1 126 | status-version 2 127 | 128 | # By default, log messages will go to the syslog (or 129 | # on Windows, if running as a service, they will go to 130 | # the "\Program Files\OpenVPN\log" directory). 131 | # Use log or log-append to override this default. 132 | # "log" will truncate the log file on OpenVPN startup, 133 | # while "log-append" will append to it. Use one 134 | # or the other (but not both). 135 | log-append openvpn.log 136 | 137 | # Set the appropriate level of log 138 | # file verbosity. 139 | # 140 | # 0 is silent, except for fatal errors 141 | # 4 is reasonable for general usage 142 | # 5 and 6 can help to debug connection problems 143 | # 9 is extremely verbose 144 | verb 3 145 | 146 | # allow client-to-client 147 | {% if c2c %} 148 | client-to-client 149 | {% endif %} 150 | 151 | # allow duplicate 152 | {% if duplicate %} 153 | duplicate-cn 154 | {% endif %} 155 | 156 | # Username and Password authentication. 157 | client-cert-not-required 158 | username-as-common-name 159 | script-security 3 160 | auth-user-pass-verify /usr/local/flexgw/scripts/openvpn-auth.py via-env 161 | -------------------------------------------------------------------------------- /website/vpn/dial/templates/dial/settings.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}VPN Dial Settings{% endblock title%} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • VPN
  • 7 |
  • Dial
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "dial/sidenav.html" %} 12 | 13 | 14 |
    15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 | {% for category, message in messages %} 18 |
    19 | {{ message }} 20 | × 21 |
    22 | {% endfor %} 23 | {% endif %} 24 | {% endwith %} 25 |
    26 | 虚拟IP 地址池: 为VPN Server 分配给客户端的虚拟IP 地址池。子网网段:为客户端连接之后可以访问的子网网段。可以填写多个子网,用英文「,」分割。

    27 | 注:保存修改后,会重载VPN 服务,所有客户端将会自动断开重连。 28 |
    29 |
    30 |
    31 |
    32 | {{ form.proto.label(class="right inline") }} 33 |
    34 |
    35 | {{ form.proto }} 36 |
    37 |
    38 | 39 |
    40 |
    41 | {{ form.ipool.label(class="right inline") }} 42 |
    43 |
    44 | {% if form.ipool.errors %} 45 | {{ form.ipool(class="error", value=form.ipool.value) }} 46 | {{ form.ipool.errors[0] }} 47 | {% else %} 48 | {{ form.ipool(placeholder="10.8.8.0/24", value=settings.ipool) }} 49 | {% endif %} 50 |
    51 |
    52 | 53 |
    54 |
    55 | {{ form.c2c.label(class="right inline") }} 56 |
    57 |
    58 | {{ form.c2c }} 59 |
    60 |
    61 | 62 |
    63 |
    64 | {{ form.duplicate.label(class="right inline") }} 65 |
    66 |
    67 | {{ form.duplicate }} 68 |
    69 |
    70 | 71 |
    72 |
    73 | {{ form.subnet.label(class="right inline") }} 74 |
    75 |
    76 | {% if form.subnet.errors %} 77 | {{ form.subnet(class="error") }} 78 | {{ form.subnet.errors[0] }} 79 | {% else %} 80 | {{ form.subnet(placeholder="10.1.0.0/24, 10.1.1.0/24") }} 81 | {% endif %} 82 |
    83 |
    84 | 85 |
    86 |
    87 | 88 | 89 |
    90 |
    91 |
    92 |
    93 | 94 | {% endblock content %} 95 | 96 | -------------------------------------------------------------------------------- /website/vpn/dial/templates/dial/sidenav.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 | {% set side_bar = [ 4 | ('dial.index', 'list', '账号列表'), 5 | ('dial.add', 'plus', '新增账号'), 6 | ('dial.settings', 'widget', '设置'), 7 | ('dial.console', 'wrench', 'VPN服务管理'), 8 | ('dial.download', 'download', '客户端下载') 9 | ] -%} 10 |
      11 | {% for endpoint, icon, caption in side_bar %} 12 | {{ caption }} 13 | {% endfor %} 14 |
    15 |
    16 | 17 | -------------------------------------------------------------------------------- /website/vpn/dial/templates/dial/view.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %} VPN 拨号账号修改{% endblock %} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • VPN
  • 7 |
  • Dial
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "dial/sidenav.html" %} 12 | 13 | 14 |
    15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 | {% for category, message in messages %} 18 |
    19 | {{ message }} 20 | × 21 |
    22 | {% endfor %} 23 | {% endif %} 24 | {% endwith %} 25 |
    26 | 注意:删除账号会重载VPN 服务,以确保被删除的账号断开连接。 27 |
    28 |
    29 |
    30 |
    31 | {{ form.name.label(class="right inline") }} 32 |
    33 |
    34 | {% if form.name.errors %} 35 | {{ form.name(class="error", value=form.name.value) }} 36 | {{ form.name.errors[0] }} 37 | {% else %} 38 | {{ form.name(value=account.name) }} 39 | {% endif %} 40 |
    41 |
    42 |
    43 |
    44 | {{ form.password.label(class="right inline") }} 45 |
    46 |
    47 |
    48 |
    49 | {% if form.password.errors %} 50 | {{ form.password(class="error", type="password", value=form.password.value) }} 51 | {{ form.password.errors[0] }} 52 | {% else %} 53 | {{ form.password(type="password", value=account.password) }} 54 | {% endif %} 55 |
    56 |
    57 | 显示密码 58 |
    59 |
    60 |
    61 |
    62 | 63 |
    64 |
    65 | {{ form.save(class="success tiny button") }} 66 |
    67 |
    68 | {{ form.delete(class="alert tiny button") }} 69 |
    70 | 71 |
    72 |
    73 |
    74 | 75 | {% endblock content %} 76 | 77 | {% block jsplugin %} 78 | 79 | {% endblock jsplugin %} 80 | 81 | -------------------------------------------------------------------------------- /website/vpn/dial/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.vpn.dial.views 4 | ~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | vpn dial views: 7 | /vpn/dial/settings 8 | /vpn/dial/add 9 | /vpn/dial/ 10 | /vpn/dial//settings 11 | """ 12 | 13 | from flask import Blueprint, render_template 14 | from flask import url_for, redirect 15 | from flask import flash 16 | 17 | from flask.ext.login import login_required 18 | 19 | from website.vpn.dial.services import get_accounts, account_update, account_del 20 | from website.vpn.dial.services import VpnServer, settings_update 21 | from website.vpn.dial.forms import AddForm, SettingsForm, ConsoleForm 22 | from website.vpn.dial.models import Account, Settings 23 | 24 | 25 | dial = Blueprint('dial', __name__, url_prefix='/vpn/dial', 26 | template_folder='templates', 27 | static_folder='static') 28 | 29 | 30 | @dial.route('/') 31 | @login_required 32 | def index(): 33 | accounts = get_accounts(status=True) 34 | if not accounts: 35 | flash(u'目前没有任何VPN 配置,如有需要请添加。', 'info') 36 | return render_template('dial/index.html', accounts=accounts) 37 | 38 | 39 | @dial.route('/add', methods=['GET', 'POST']) 40 | @login_required 41 | def add(): 42 | settings = Settings.query.get(1) 43 | if not settings: 44 | flash(u'提示:请先进行「设置」再添加VPN 账号。', 'alert') 45 | return redirect(url_for('dial.settings')) 46 | form = AddForm() 47 | if form.validate_on_submit(): 48 | if not Account.query.filter_by(name=form.name.data).first(): 49 | if account_update(form): 50 | message = u'添加VPN 拨号账号成功!' 51 | flash(message, 'success') 52 | return redirect(url_for('dial.index')) 53 | else: 54 | message = u'该账号已经存在:%s' % form.name.data 55 | flash(message, 'alert') 56 | return render_template('dial/add.html', form=form) 57 | 58 | 59 | @dial.route('/settings', methods=['GET', 'POST']) 60 | @login_required 61 | def settings(): 62 | form = SettingsForm() 63 | settings = Settings.query.get(1) 64 | if form.validate_on_submit(): 65 | if settings_update(form): 66 | flash(u'修改配置成功!注:修改「虚拟IP 地址池」之后,需手工调整相应的SNAT 设置!', 'success') 67 | return redirect(url_for('dial.settings')) 68 | if settings: 69 | form.subnet.data = settings.subnet 70 | form.c2c.data = 'yes' if settings.c2c else 'no' 71 | form.duplicate.data = 'yes' if settings.duplicate else 'no' 72 | form.proto.data = settings.proto 73 | return render_template('dial/settings.html', settings=settings, form=form) 74 | 75 | 76 | @dial.route('//settings', methods=['GET', 'POST']) 77 | @login_required 78 | def id_settings(id): 79 | form = AddForm() 80 | account = get_accounts(id) 81 | if form.validate_on_submit(): 82 | if form.delete.data: 83 | if account_del(id): 84 | message = u'删除账号%s :成功!' % account[0]['name'] 85 | flash(message, 'success') 86 | return redirect(url_for('dial.index')) 87 | if form.save.data: 88 | if account_update(form, id): 89 | flash(u'修改账号配置成功!', 'success') 90 | return redirect(url_for('dial.id_settings', id=id)) 91 | return render_template('dial/view.html', account=account[0], form=form) 92 | 93 | 94 | @dial.route('/console', methods=['GET', 'POST']) 95 | @login_required 96 | def console(): 97 | form = ConsoleForm() 98 | vpn = VpnServer() 99 | if form.validate_on_submit(): 100 | if form.stop.data and vpn.stop: 101 | flash(u'VPN 服务停止成功!', 'success') 102 | if form.start.data and vpn.start: 103 | flash(u'VPN 服务启动成功!', 'success') 104 | if form.re_load.data and vpn.reload: 105 | flash(u'VPN 服务配置生效完成!', 'success') 106 | return redirect(url_for('dial.console')) 107 | return render_template('dial/console.html', status=vpn.status, form=form) 108 | 109 | 110 | @dial.route('/download') 111 | @login_required 112 | def download(): 113 | return render_template('dial/download.html') 114 | -------------------------------------------------------------------------------- /website/vpn/sts/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.vpn.sts 4 | ~~~~~~~~~~~~~~~ 5 | 6 | website vpn site-to-site blueprint. 7 | """ 8 | -------------------------------------------------------------------------------- /website/vpn/sts/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.vpn.sts.helpers 4 | ~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | vpn sts helpers api. 7 | """ 8 | 9 | 10 | import sys 11 | 12 | from flask import current_app 13 | 14 | 15 | def ipsec_conf_parser(file): 16 | try: 17 | with open(file, mode='r') as f: 18 | raw_data = [l.strip() for l in f.readlines() if l.strip()] 19 | except: 20 | current_app.logger.error('[Ipsec Helpers]: read ipsec conf file error: %s:%s', 21 | file, sys.exc_info()[1]) 22 | tunnels = {} 23 | tunnel = None 24 | for line in raw_data: 25 | #: continue while read comment string. 26 | if line.startswith('#'): 27 | continue 28 | #: continue while read config string. 29 | if line.startswith('config'): 30 | continue 31 | #: process conn name 32 | if line.startswith('conn'): 33 | if line.split()[1].startswith('%'): 34 | continue 35 | else: 36 | tunnel = line.split()[1] 37 | tunnels[tunnel] = {} 38 | continue 39 | #: process config key=value 40 | if tunnel: 41 | data = line.strip().split('=') 42 | tunnels[tunnel][data[0].strip()] = data[1].strip() 43 | return tunnels 44 | -------------------------------------------------------------------------------- /website/vpn/sts/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.vpn.sts.models 4 | ~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | vpn sts system models. 7 | """ 8 | 9 | 10 | from datetime import datetime 11 | 12 | from website import db 13 | 14 | 15 | class Tunnels(db.Model): 16 | '''tunnels models.''' 17 | __tablename__ = 'sts_tunnels' 18 | 19 | id = db.Column(db.Integer, primary_key=True) 20 | name = db.Column(db.String(80), unique=True, index=True) 21 | rules = db.Column(db.String(500)) 22 | psk = db.Column(db.String(80)) 23 | created_at = db.Column(db.DateTime) 24 | 25 | def __init__(self, name, rules, psk, created_at=datetime.now()): 26 | self.name = name 27 | self.rules = rules 28 | self.psk = psk 29 | self.created_at = created_at 30 | 31 | def __repr__(self): 32 | return '' % (self.name, self.created_at) 33 | -------------------------------------------------------------------------------- /website/vpn/sts/templates/sts/add.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}VPN Site-to-Site 新增隧道{% endblock title%} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • VPN
  • 7 |
  • Site-to-Site
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "sts/sidenav.html" %} 12 | 13 | 14 |
    15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 | {% for category, message in messages %} 18 |
    19 | {{ message }} 20 | × 21 |
    22 | {% endfor %} 23 | {% endif %} 24 | {% endwith %} 25 |
    26 | 隧道ID:互相连接的隧道两端ID 需要保持一致。本端/对端子网:可以填写多个子网,用英文「,」分割。 27 |
    28 |
    29 |
    30 |
    31 | {{ form.tunnel_name.label(class="right inline") }} 32 |
    33 |
    34 | {% if form.tunnel_name.errors %} 35 | {{ form.tunnel_name(class="error", value=form.tunnel_name.value) }} 36 | {{ form.tunnel_name.errors[0] }} 37 | {% else %} 38 | {{ form.tunnel_name(placeholder="home") }} 39 | {% endif %} 40 |
    41 |
    42 |
    43 |
    44 | {{ form.start_type.label(class="right inline") }} 45 |
    46 |
    47 | {{ form.start_type }} 48 |
    49 |
    50 |
    51 |
    52 | {{ form.ike_encryption_algorithm.label(class="right inline") }} 53 |
    54 |
    55 | {{ form.ike_encryption_algorithm }} 56 |
    57 |
    58 |
    59 |
    60 | {{ form.ike_integrity_algorithm.label(class="right inline") }} 61 |
    62 |
    63 | {{ form.ike_integrity_algorithm }} 64 |
    65 |
    66 |
    67 |
    68 | {{ form.ike_dh_algorithm.label(class="right inline") }} 69 |
    70 |
    71 | {{ form.ike_dh_algorithm }} 72 |
    73 |
    74 |
    75 |
    76 | {{ form.esp_encryption_algorithm.label(class="right inline") }} 77 |
    78 |
    79 | {{ form.esp_encryption_algorithm }} 80 |
    81 |
    82 |
    83 |
    84 | {{ form.esp_integrity_algorithm.label(class="right inline") }} 85 |
    86 |
    87 | {{ form.esp_integrity_algorithm }} 88 |
    89 |
    90 |
    91 |
    92 | {{ form.esp_dh_algorithm.label(class="right inline") }} 93 |
    94 |
    95 | {{ form.esp_dh_algorithm }} 96 |
    97 |
    98 |
    99 |
    100 | {{ form.local_subnet.label(class="right inline") }} 101 |
    102 |
    103 | {% if form.local_subnet.errors %} 104 | {{ form.local_subnet(class="error") }} 105 | {{ form.local_subnet.errors[0] }} 106 | {% else %} 107 | {{ form.local_subnet(placeholder="10.1.0.0/24, 10.1.1.0/24") }} 108 | {% endif %} 109 |
    110 |
    111 |
    112 |
    113 | {{ form.remote_ip.label(class="right inline") }} 114 |
    115 |
    116 | {% if form.remote_ip.errors %} 117 | {{ form.remote_ip(class="error", value=form.remote_ip.value) }} 118 | {{ form.remote_ip.errors[0] }} 119 | {% else %} 120 | {{ form.remote_ip(placeholder="100.69.0.119") }} 121 | {% endif %} 122 |
    123 |
    124 |
    125 |
    126 | {{ form.remote_subnet.label(class="right inline") }} 127 |
    128 |
    129 | {% if form.remote_subnet.errors %} 130 | {{ form.remote_subnet(class="error") }} 131 | {{ form.remote_subnet.errors[0] }} 132 | {% else %} 133 | {{ form.remote_subnet(placeholder="10.2.0.0/24, 10.2.1.0/24") }} 134 | {% endif %} 135 |
    136 |
    137 |
    138 |
    139 | {{ form.psk.label(class="right inline") }} 140 |
    141 |
    142 | {% if form.psk.errors %} 143 | {{ form.psk(class="error", value=form.psk.value) }} 144 | {{ form.psk.errors[0] }} 145 | {% else %} 146 | {{ form.psk(placeholder="*DZ4J}[/*I7btVP6&lzXFyBlSV%D3@^]") }} 147 | {% endif %} 148 |
    149 |
    150 | 151 |
    152 |
    153 | 154 | 155 |
    156 |
    157 |
    158 |
    159 | 160 | {% endblock content %} 161 | -------------------------------------------------------------------------------- /website/vpn/sts/templates/sts/console.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}VPN Console{% endblock title%} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • VPN
  • 7 |
  • VPN Console
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "sts/sidenav.html" %} 12 | 13 | 14 |
    15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 | {% for category, message in messages %} 18 |
    19 | {{ message }} 20 | × 21 |
    22 | {% endfor %} 23 | {% endif %} 24 | {% endwith %} 25 |
    26 | {% if status %} 27 | IPSec VPN 服务运行正常! 28 | {% else %} 29 | IPSec VPN 没有运行! 30 | {% endif %} 31 |
    32 |
    33 |
    34 |
    35 |
      36 |
    • 启动VPN 服务
    • 37 |
    • 启动VPN 服务。不会影响到已经运行的VPN 服务。
    • 38 |
    • 39 | {% if status %} 40 | {{ form.start(class="alert tiny button disabled", disabled="disabled") }} 41 | {% else %} 42 | {{ form.start(class="alert tiny button") }} 43 | {% endif %} 44 |
    • 45 |
    46 |
    47 |
    48 |
      49 |
    • 停止VPN 服务
    • 50 |
    • 停止VPN 服务。服务停止之后,所有已经连接的隧道都将中断。
    • 51 |
    • 52 | {% if status %} 53 | {{ form.stop(class="alert tiny button") }} 54 | {% else %} 55 | {{ form.stop(class="alert tiny button disabled", disabled="disabled") }} 56 | {% endif %} 57 |
    • 58 |
    59 |
    60 |
      61 |
    • 配置下发&重载
    • 62 |
    • 下发新的VPN 配置,并使之生效。建议在修改VPN 配置之后再进行使用。
    • 63 |
    • 64 | {% if status %} 65 | {{ form.re_load(class="alert tiny button") }} 66 | {% else %} 67 | {{ form.re_load(class="alert tiny button disabled", disabled="disabled") }} 68 | {% endif %} 69 |
    • 70 |
    71 |
    72 | 73 |
    74 |
    75 |
    76 | 77 | {% endblock content %} 78 | -------------------------------------------------------------------------------- /website/vpn/sts/templates/sts/flow.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}VPN Tunnel Flow{% endblock %} 3 | 4 | {% block head %} 5 | 6 | {% endblock head %} 7 | 8 | {% block trail %} 9 |
  • Home
  • 10 |
  • VPN
  • 11 |
  • 实时流量
  • 12 | {% endblock trail %} 13 | 14 | {% block content %} 15 | 16 |
    17 | 隧道信息 18 |
      19 |
    • 隧道ID:{{ tunnel.name }}
    • 20 |
    • 状态: 21 | {% if tunnel.status %} 22 | Online 23 | {% else %} 24 | Offline 25 | {% endif %} 26 |
    • 27 |
    • 本端子网:{{ tunnel.rules.leftsubnet }}
    • 28 |
    • 对端EIP:{{ tunnel.rules.right }}
    • 29 |
    • 对端子网:{{ tunnel.rules.rightsubnet }}
    • 30 |
    31 |
    32 | 33 | 34 | 35 |
    36 |
    37 |
    38 | 39 | {% endblock content %} 40 | {% block jsplugin %} 41 | 42 | 43 | 44 | 58 | {% endblock jsplugin %} 59 | -------------------------------------------------------------------------------- /website/vpn/sts/templates/sts/index.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}VPN Site-to-Site{% endblock title%} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • VPN
  • 7 |
  • Site-to-Site
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "sts/sidenav.html" %} 12 | 13 | 14 |
    15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 | {% for category, message in messages %} 18 |
    19 | {{ message }} 20 | × 21 |
    22 | {% endfor %} 23 | {% endif %} 24 | {% endwith %} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% if tunnels %} 40 | {% for tunnel in tunnels %} 41 | 42 | 43 | 44 | 45 | 46 | 47 | 54 | 57 | 68 | 69 | {% endfor %} 70 | {% endif %} 71 | 72 |
    隧道ID启动类型本端子网对端EIP对端子网状态查看操作
    {{ tunnel.name }}{% if tunnel.rules.auto == 'add' %}手工连接{% else %}服务启动自动连接{% endif %}{{ tunnel.rules.leftsubnet }}{{ tunnel.rules.right }}{{ tunnel.rules.rightsubnet }} 48 | {% if tunnel.status %} 49 | Online 50 | {% else %} 51 | Offline 52 | {% endif %} 53 | 55 | 流量 56 | 58 |
    59 | {{ form.tunnel_name(value=tunnel.name, type="hidden") }} 60 | 61 | {% if tunnel.status %} 62 | {{ form.down(class="alert bitty button") }} 63 | {% else %} 64 | {{ form.up(class="alert bitty button") }} 65 | {% endif %} 66 |
    67 |
    73 |
    74 | 75 | {% endblock content %} 76 | 77 | -------------------------------------------------------------------------------- /website/vpn/sts/templates/sts/ipsec.conf: -------------------------------------------------------------------------------- 1 | config setup 2 | uniqueids=yes 3 | 4 | conn %default 5 | ikelifetime=60h 6 | keylife=20h 7 | rekeymargin=3h 8 | keyingtries=1 9 | keyexchange=ikev2 10 | mobike=no 11 | type=tunnel 12 | 13 | {%- if tunnels %} 14 | {% for tunnel in tunnels %} 15 | conn {{ tunnel.name }} 16 | leftid={{ tunnel.rules.leftid }} 17 | left={{ tunnel.rules.left }} 18 | leftsubnet={{ tunnel.rules.leftsubnet }} 19 | rightid={{ tunnel.rules.rightid }} 20 | right={{ tunnel.rules.right }} 21 | rightsubnet={{ tunnel.rules.rightsubnet }} 22 | {#- Backward compatible v1.1.0 #} 23 | {%- if tunnel.rules.ike %} 24 | ike={{ tunnel.rules.ike }} 25 | {%- endif %} 26 | esp={{ tunnel.rules.esp }} 27 | authby={{ tunnel.rules.authby }} 28 | auto={{ tunnel.rules.auto }} 29 | {% endfor %} 30 | {%- endif %} 31 | -------------------------------------------------------------------------------- /website/vpn/sts/templates/sts/ipsec.secrets: -------------------------------------------------------------------------------- 1 | {% if tunnels %} 2 | {% for tunnel in tunnels %} 3 | {{ tunnel.leftid }} {{ tunnel.rightid }} : PSK "{{ tunnel.psk }}" 4 | {% endfor %} 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /website/vpn/sts/templates/sts/sidenav.html: -------------------------------------------------------------------------------- 1 | 2 |
    3 | {% set side_bar = [ 4 | ('sts.index', 'list', '隧道列表'), 5 | ('sts.add', 'plus', '新增隧道'), 6 | ('sts.console', 'wrench', 'VPN服务管理') 7 | ] -%} 8 |
      9 | {% for endpoint, icon, caption in side_bar %} 10 | {{ caption }} 11 | {% endfor %} 12 |
    13 |
    14 | 15 | -------------------------------------------------------------------------------- /website/vpn/sts/templates/sts/view.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %} VPN Site-to-Site 修改隧道{% endblock %} 3 | 4 | {% block trail %} 5 |
  • Home
  • 6 |
  • VPN
  • 7 |
  • Site-to-Site
  • 8 | {% endblock trail %} 9 | 10 | {% block content %} 11 | {% include "sts/sidenav.html" %} 12 | 13 | 14 |
    15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 | {% for category, message in messages %} 18 |
    19 | {{ message }} 20 | × 21 |
    22 | {% endfor %} 23 | {% endif %} 24 | {% endwith %} 25 |
    26 |
    27 |
    28 | {{ form.tunnel_name.label(class="right inline") }} 29 |
    30 |
    31 | {% if form.tunnel_name.errors %} 32 | {{ form.tunnel_name(class="error", value=form.tunnel_name.value) }} 33 | {{ form.tunnel_name.errors[0] }} 34 | {% else %} 35 | {{ form.tunnel_name(value=tunnel.name) }} 36 | {% endif %} 37 |
    38 |
    39 |
    40 |
    41 | {{ form.start_type.label(class="right inline") }} 42 |
    43 |
    44 | {{ form.start_type }} 45 |
    46 |
    47 |
    48 |
    49 | {{ form.ike_encryption_algorithm.label(class="right inline") }} 50 |
    51 |
    52 | {{ form.ike_encryption_algorithm }} 53 |
    54 |
    55 |
    56 |
    57 | {{ form.ike_integrity_algorithm.label(class="right inline") }} 58 |
    59 |
    60 | {{ form.ike_integrity_algorithm }} 61 |
    62 |
    63 |
    64 |
    65 | {{ form.ike_dh_algorithm.label(class="right inline") }} 66 |
    67 |
    68 | {{ form.ike_dh_algorithm }} 69 |
    70 |
    71 |
    72 |
    73 | {{ form.esp_encryption_algorithm.label(class="right inline") }} 74 |
    75 |
    76 | {{ form.esp_encryption_algorithm }} 77 |
    78 |
    79 |
    80 |
    81 | {{ form.esp_integrity_algorithm.label(class="right inline") }} 82 |
    83 |
    84 | {{ form.esp_integrity_algorithm }} 85 |
    86 |
    87 |
    88 |
    89 | {{ form.esp_dh_algorithm.label(class="right inline") }} 90 |
    91 |
    92 | {{ form.esp_dh_algorithm }} 93 |
    94 |
    95 |
    96 |
    97 | {{ form.local_subnet.label(class="right inline") }} 98 |
    99 |
    100 | {% if form.local_subnet.errors %} 101 | {{ form.local_subnet(class="error") }} 102 | {{ form.local_subnet.errors[0] }} 103 | {% else %} 104 | {{ form.local_subnet }} 105 | {% endif %} 106 |
    107 |
    108 |
    109 |
    110 | {{ form.remote_ip.label(class="right inline") }} 111 |
    112 |
    113 | {% if form.remote_ip.errors %} 114 | {{ form.remote_ip(class="error", value=form.remote_ip.value) }} 115 | {{ form.remote_ip.errors[0] }} 116 | {% else %} 117 | {{ form.remote_ip(value=tunnel.rules.right) }} 118 | {% endif %} 119 |
    120 |
    121 |
    122 |
    123 | {{ form.remote_subnet.label(class="right inline") }} 124 |
    125 |
    126 | {% if form.remote_subnet.errors %} 127 | {{ form.remote_subnet(class="error") }} 128 | {{ form.remote_subnet.errors[0] }} 129 | {% else %} 130 | {{ form.remote_subnet }} 131 | {% endif %} 132 |
    133 |
    134 |
    135 |
    136 | {{ form.psk.label(class="right inline") }} 137 |
    138 |
    139 | {% if form.psk.errors %} 140 | {{ form.psk(class="error", value=form.psk.value) }} 141 | {{ form.psk.errors[0] }} 142 | {% else %} 143 | {{ form.psk(value=tunnel.psk) }} 144 | {% endif %} 145 |
    146 |
    147 | 148 |
    149 |
    150 | {{ form.save(class="success tiny button") }} 151 |
    152 |
    153 | {{ form.delete(class="alert tiny button") }} 154 |
    155 | 156 |
    157 |
    158 |
    159 | 160 | {% endblock content %} 161 | -------------------------------------------------------------------------------- /website/vpn/sts/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | website.vpn.sts.views 4 | ~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | vpn sts views: 7 | /vpn/sts/add 8 | /vpn/sts/ 9 | /vpn/sts//settings 10 | """ 11 | 12 | from flask import Blueprint, render_template 13 | from flask import url_for, redirect 14 | from flask import flash 15 | 16 | from website.vpn.sts.forms import AddForm 17 | from website.vpn.sts.forms import ConsoleForm, UpDownForm 18 | from website.vpn.sts.services import vpn_settings, vpn_del 19 | from website.vpn.sts.services import get_tunnels, VpnServer 20 | from website.vpn.sts.models import Tunnels 21 | 22 | from flask.ext.login import login_required 23 | 24 | 25 | sts = Blueprint('sts', __name__, url_prefix='/vpn/sts', 26 | template_folder='templates') 27 | 28 | 29 | @sts.route('/') 30 | @login_required 31 | def index(): 32 | form = UpDownForm() 33 | tunnels = get_tunnels(status=True) 34 | if not tunnels: 35 | flash(u'目前没有任何VPN 配置,如有需要请添加。', 'info') 36 | return render_template('sts/index.html', tunnels=tunnels, form=form) 37 | 38 | 39 | @sts.route('/add', methods=['GET', 'POST']) 40 | @login_required 41 | def add(): 42 | form = AddForm() 43 | if form.validate_on_submit(): 44 | if not Tunnels.query.filter_by(name=form.tunnel_name.data).first(): 45 | if vpn_settings(form): 46 | message = u'添加Site-to-Site 隧道成功!' 47 | flash(message, 'success') 48 | return redirect(url_for('sts.index')) 49 | else: 50 | message = u'该隧道已经存在:%s' % form.tunnel_name.data 51 | flash(message, 'alert') 52 | return render_template('sts/add.html', form=form) 53 | 54 | 55 | @sts.route('//settings', methods=['GET', 'POST']) 56 | @login_required 57 | def settings(id): 58 | form = AddForm() 59 | tunnel = get_tunnels(id) 60 | if form.validate_on_submit(): 61 | if form.delete.data: 62 | if vpn_del(id): 63 | message = u'删除隧道%s :成功!' % tunnel[0]['name'] 64 | flash(message, 'success') 65 | return redirect(url_for('sts.index')) 66 | if form.save.data: 67 | if vpn_settings(form, id): 68 | flash(u'修改隧道配置成功!', 'success') 69 | return redirect(url_for('sts.settings', id=id)) 70 | form.local_subnet.data = tunnel[0]['rules']['leftsubnet'] 71 | form.remote_subnet.data = tunnel[0]['rules']['rightsubnet'] 72 | form.start_type.data = tunnel[0]['rules']['auto'] 73 | # Backward compatible v1.1.0 74 | esp_settings = tunnel[0]['rules']['esp'].split('-') 75 | form.esp_encryption_algorithm.data = esp_settings[0] 76 | form.esp_integrity_algorithm.data = esp_settings[1] 77 | form.esp_dh_algorithm.data = esp_settings[2] if len(esp_settings) == 3 else 'null' 78 | ike_settings = tunnel[0]['rules'].get('ike', 'aes128-sha1-modp2048').split('-') 79 | form.ike_encryption_algorithm.data = ike_settings[0] 80 | form.ike_integrity_algorithm.data = ike_settings[1] 81 | form.ike_dh_algorithm.data = ike_settings[2] 82 | return render_template('sts/view.html', tunnel=tunnel[0], form=form) 83 | 84 | 85 | @sts.route('//flow') 86 | @login_required 87 | def flow(id): 88 | tunnel = get_tunnels(id, status=True) 89 | return render_template('sts/flow.html', tunnel=tunnel[0]) 90 | 91 | 92 | @sts.route('/console', methods=['GET', 'POST']) 93 | @login_required 94 | def console(): 95 | form = ConsoleForm() 96 | vpn = VpnServer() 97 | if form.validate_on_submit(): 98 | if form.stop.data and vpn.stop: 99 | flash(u'VPN 服务停止成功!', 'success') 100 | if form.start.data and vpn.start: 101 | flash(u'VPN 服务启动成功!', 'success') 102 | if form.re_load.data and vpn.reload: 103 | flash(u'VPN 服务配置生效完成!', 'success') 104 | return render_template('sts/console.html', status=vpn.status, form=form) 105 | 106 | 107 | @sts.route('/updown', methods=['POST']) 108 | @login_required 109 | def updown(): 110 | form = UpDownForm() 111 | vpn = VpnServer() 112 | if form.validate_on_submit(): 113 | if form.up.data and vpn.tunnel_up(form.tunnel_name.data): 114 | flash(u'隧道连接成功!', 'success') 115 | if form.down.data and vpn.tunnel_down(form.tunnel_name.data): 116 | flash(u'隧道断开成功!', 'success') 117 | return redirect(url_for('sts.index')) 118 | -------------------------------------------------------------------------------- /website_console: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | ######################################################################### 3 | # chkconfig: - 96 48 4 | # description: this is script can start/stop/restart website 5 | ######################################################################### 6 | 7 | 8 | script_home="/usr/local/flexgw" 9 | website_prog="/usr/local/flexgw/python/bin/gunicorn" 10 | website_gunicorn_etc="$script_home/gunicorn.py" 11 | website_app="website:app" 12 | website_pid="$script_home/website.pid" 13 | 14 | status() 15 | { 16 | if [ -f $website_pid ]; then 17 | website_pid=$(cat $website_pid) 18 | else 19 | echo $"not found website pid file, seems not running?" 20 | return 2 21 | fi 22 | 23 | if (kill -0 $website_pid > /dev/null 2>&1); then 24 | echo $"website is running" 25 | return 0 26 | else 27 | echo $"website is not running" 28 | return 1 29 | fi 30 | } 31 | 32 | reload() 33 | { 34 | status 35 | if [ $? -eq 0 ]; then 36 | kill -s SIGHUP $website_pid 37 | if [ $? -eq 0 ]; then 38 | echo $"website reload ok" 39 | else 40 | echo $"website reload failed" 41 | fi 42 | fi 43 | } 44 | 45 | stop() 46 | { 47 | status 48 | if [ $? -eq 0 ]; then 49 | kill -s SIGTERM $website_pid 50 | if [ $? -eq 0 ]; then 51 | echo $"website stop ok" 52 | else 53 | echo $"website stop failed" 54 | fi 55 | sleep 2 56 | status 57 | fi 58 | } 59 | 60 | start() 61 | { 62 | status 63 | if [ $? -ne 0 ]; then 64 | echo $"starting website..." 65 | [ -f /usr/local/flexgw/instance/snat-rules.iptables ] && 66 | iptables-restore --table=nat < /usr/local/flexgw/instance/snat-rules.iptables 67 | $website_prog -D -c $website_gunicorn_etc $website_app --pythonpath $script_home 68 | sleep 2 69 | status 70 | fi 71 | } 72 | 73 | case "$1" in 74 | start) 75 | start 76 | ;; 77 | stop) 78 | stop 79 | ;; 80 | restart) 81 | stop 82 | start 83 | ;; 84 | reload) 85 | reload 86 | ;; 87 | status) 88 | status 89 | ;; 90 | *) 91 | echo $"Usage: $0 {start|stop|restart|reload|status}" 92 | exit 1 93 | esac 94 | exit $? 95 | -------------------------------------------------------------------------------- /websiteconfig.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | websiteconfig 4 | ~~~~~~~~~~~~~ 5 | 6 | default config for website. 7 | """ 8 | 9 | import os 10 | 11 | 12 | class default_settings(object): 13 | DEBUG = True 14 | TESTING = True 15 | 16 | SECRET_KEY = '\x7f\x89q\x87v~\x87~\x86U\xb1\xa8\xb5=v\xaf\xb0\xdcn\xfa\xea\xeb?\x99' 17 | 18 | SQLALCHEMY_ECHO = True 19 | SQLALCHEMY_DATABASE_URI = 'sqlite:///%s/instance/website.db' % os.path.abspath(os.path.dirname(__file__)) 20 | --------------------------------------------------------------------------------