Ap Blog

Cold code, warm soul.

@Aploium3年前

11/20
15:26
网络

Debian(Linux)连接浙大L2TP VPN的脚本

本脚本用于Linux(Debian)连接浙大校园VPN (L2TP) (主要测试于树莓派上)
浙大校园网的特性是ZJUWLAN与VPN的认证互不干扰,并且在连接ZJUWLAN而不登陆的情况下可以通过VPN登陆
于是可以让Linux先连接ZJUWLAN(不登陆),然后经由ZJUWLAN登陆VPN
这样的好处是可以使用别人闲置的VPN账号,而不一定需要用自己的

  • 支持有线与无线
  • 无线时自动连接ZJUWLAN,并用ZJUWLAN承载VPN
  • 使用VPN而不是ZJUWLAN的认证(这样就能用别人闲置的VPN账号了)
  • 自动发送本机IP到自己的接受服务器(附服务端参考代码)
  • 详细的log以帮助debug
  • 半自动断线重连
  • 支持多VPN账号(2015年12月21日更新)
  • DNSPOD域名动态解析(提供PHP代码)

理论上linux通用,目前仅在 Ubuntu 14.04 LTS(VM,桥接,复制物理网络状态)/Kali 2.0(VM,桥接,复制物理网络状态)/raspbian 上实地测试
使用本脚本之前需要先安装xl2tpd

sudo apt-get update
sudo apt-get install xl2tpd

以下为单账号用户的配置步骤

编辑/etc/xl2tpd/xl2tpd.conf文件
加入配置

[global]
port = 1701
access control = no

[lac zjuvpn1] 
lns=10.5.1.9    #Cannot be 10.5.1.7
redial=yes 
redial timeout=5 
max redials=10      
refuse pap=yes 
require chap=yes    #Force chap instead of pap
require authentication=yes 
name=学号
pppoptfile = /etc/ppp/zjuvpn1.options


然后新建 /etc/ppp/zjuvpn1.options
加入

noauth
proxyarp
name 你的VPN账号,一般是学号
password  你的VPN密码

以下为多账号用户的配置步骤

多账号用户与单账户用户类似,但需要配置如 zjuvpn1 zjuvpn2 zjuvpn3 … 多个配置文件
(出于稳定性考虑,暂时不在运行时自动生成)
我写了一个python脚本来生成这些账号(需要python3.x Linux/Win通用)

#!/usr/bin/python3.4
# -*- coding: UTF-8 -*-
import os

# 本程序用于方便地将多个VPN账号转化为L2TP VPN连接脚本可用的profiles

# 请准备一个纯文本输入账号文件,每行一个账号,格式为(不含#):
# 账号{Tab}密码
#eg:
#3140100233 123456
# 运行后会在输出文件夹中得到:
# 输出文件夹
#  |- ppp
#  |   |-profiles
#  |        |-- zjuvpn1.option
#  |        |-- zjuvpn2.option
#  |        |-- zjuvpn3.option
#  |        |-- ....
#  |-- xl2tpd.conf
#
# 其中xl2tpd.conf覆盖到 /etc/xl2tpd/ 文件夹下
# profiles文件夹放到 /etc/ppp/ 中(即两个ppp是同一个)
# 并记得在zjuvpn.sh的设置中把totalVPNAccounts改成你的账号数
# 70个浙大VPN账号实测可用,更多账号理论上可行,未实测
##################################################

# 输入账号文件,每行一个账号,格式为(不含#):
# 账号{Tab}密码
#eg:
#3140100233 123456
accountsListFile = r'C:\Users\Administrator\Desktop\account.txt'

# 输出文件夹
outputDir = r'C:\Users\Administrator\Desktop\vpns'

##################################################
confFile = os.path.join(outputDir, 'xl2tpd.conf')
fp = open(accountsListFile, 'r', encoding='UTF-8')
buff = fp.readline()
no = 1
if not os.path.isdir(os.path.join(outputDir, r'ppp', 'profiles')):
    os.makedirs(os.path.join(outputDir, r'ppp', 'profiles'))

with open(confFile, 'w', encoding='UTF-8') as fpw:  # head of conf file
    fpw.write("[global]\nport = 1701\naccess control = no\n\n")

while buff:
    id, passwd = buff.split()
    print("No.%d: %s\t%s" % (no, id, passwd))

    with open(os.path.join(outputDir, r'ppp', r'profiles', 'zjuvpn' + str(no) + r'.option'), 'w',
              encoding='UTF-8') as fpw:
        fpw.write('noauth\n')
        fpw.write('proxyarp\n')
        fpw.write('name ' + id + "\n")
        fpw.write('password ' + passwd + "\n")

    with open(confFile, 'a', encoding='UTF-8') as fpw:
        fpw.write(r'[lac zjuvpn' + str(no) + r']' + "\n")
        fpw.write(r'lns=10.5.1.9' + "\n")
        fpw.write(r'redial=yes' + "\n")
        fpw.write(r'redial timeout=5' + "\n")
        fpw.write(r'max redials=10' + "\n")
        fpw.write(r'refuse pap=yes' + "\n")
        fpw.write(r'require chap=yes' + "\n")
        fpw.write(r'name=' + id + "\n")
        fpw.write(r'pppoptfile = /etc/ppp/profiles/zjuvpn' + str(no) + r'.option' + "\n\n")
        fpw.write('')
    no += 1
    buff = fp.readline()

no -= 1
print("total accounts= %d" % no)
fp.close()

以下为脚本主程序
可以直接运行,也可以include到 /etc/rc.local (debian系)中来做到开机自动执行
请修改config中 vpnProfilePrefix , totalVPNAccounts 为你的实际值
其余部分开箱即用

将脚本保存为zjuvpn.sh
chmod +x ./zjuvpn.sh
./zjuvpn.sh

程序日志会保存到 /var/log/zjuvpn.log 中

非树莓派用户(桌面Linux)请将选项中的is_Raspberry设置成0
树莓派用户请修改urlToReceiveIP到你实际的接收IP的URL

#!/bin/bash
#root privilege is required

#version 2015-12-21 11:40:04
#By Aploium
#//z.codes

#------Config Begin-------
#Global log file
#default is '/var/log/zjuvpn.log'
zjulog='/var/log/zjuvpn.log'

#VPN profiles prefix
#You should set your profile(s) here
#default is 'zjuvpn'
vpnProfilePrefix='zjuvpn'

#Total VPN accounts
#If you have several VPN accounts, you can set here
#Program will choose a random accounts first
#If failed, it will tries the next one, 
#recycle if needed
#until reached the connectFailCount set below
#Support at least 70 accounts
########  IMPORTANT  ########
###### those profiles MUST be set like
###### zjuvpn1  zjuvpn2  zjuvpn3  zjuvpn4  (and so on,NO zjuvpn0)
###### the prefix was defined in the former 'vpnProfilePrefix'
#default is 1 (only one account eg: zjuvpn1)
totalVPNAccounts=1

#Try how many times to connect ZJUWLAN and 10.5.1.9
#Infinity tries IS VERY NOT recommended because it may cause
#you computer jammed at the boot stage
#although you can set -1 to FORCE an infinity tries
#default is 5
connectFailCount=5

#is used in raspberry?
#if set to 1, script will report its IP to remote server
#if script runs in your desktop, please disable this option
#default is 1  (IP address report ENABLED)
is_Raspberry=1

#the url to receive the IP
#This option ONLY effects when is_Raspberry was set to 1
#note: maybe more than one IP will be sent
#eg. //z.codes/example.php?ip=
#no default value
urlToReceiveIP='http://你用来接收URL的网站/ras.php?ip='

#------Config End-------
function CheckWAN(){
    #-----Check if we can reach taobao.com-----
    curl -I taobao.com | grep Tengine >/dev/null && result=0 || result=1
    if [ $result == 0 ] #get and check taobao.com
        then return 0 #do not log correct because this check may do many times
        else
             echo `date` CAN NOT REACH WAN _$connectFailCount | tee -a $zjulog
             return 1
    fi
    return 2
}

function ConnectZJUWLAN(){
    #-----detect and connect ZJUWLAN-----
    #Err 0:everything is OK
    #Err 1:wlan0 does not exists
    #Err 2:wlan0 DOES NOT EXIST
    #Err 3:FAILED TO CONNECT ZJUWLAN
    ifconfig|grep wlan0 >/dev/null && result=0 || result=1
    if [ $result == 1 ] #check if wlan0 exists
        then
            echo `date` Err1 wlan0 DOES NOT EXIST | tee -a $zjulog
            return 1
    fi
    ifconfig wlan0 up #wake up wlan0
    iwlist wlan0 scan|grep ZJUWLAN >/dev/null && result=0 || result=1
    if [ $result == 1 ] #check if ZJUWLAN exists
        then
            echo `date` Err2 we didnt find a ZJUWLAN available | tee -a $zjulog
            return 2
    fi
    iwconfig wlan0 essid "ZJUWLAN"  #connect ZJUWLAN
    sleep 4
    dhclient wlan0  #fetch IP from DHCP
    iwconfig | grep ZJUWLAN >/dev/null && result=0 || result=1
    if [ $result == 0 ]
        then
            echo `date` ZJUWLAN connected | tee -a $zjulog
            return 0 #normal return
        else
            echo `date` Err3 FAILED TO CONNECT ZJUWLAN | tee -a $zjulog
            return 3
    fi
    sleep 1
    return 4
}

function CheckWire(){
    #-----detect wire connection-----
    ifconfig eth0 | grep "inet addr:[0-9]*.\.[0-9]*.\.[0-9]*.\.[0-9]*" >/dev/null && result=0 || result=1
    if [ $result == 0 ] #if eth0 has ip
        then
             echo `date` eth0 has an IP} | tee -a $zjulog
             CheckLns
             return $?
        else return 2
    fi
    return 3
}

function CheckLns(){
    #-----detect wire connection to 10.5.1.9-----
    ping -c1 -W1 10.5.1.9 && result=0 || result=1
    if [ $result == 0 ] #if can connect to 10.5.1.9
        then
             echo `date` we can connect to lns | tee -a $zjulog
             return 0
        else return 1
        fi
}

function ConnectVPN(){
    #-----connectZJUVPN-----
    service xl2tpd restart  #restart xl2tpd to reduce bugs
    vpnconf=${1:-"zjuvpn1"} #if para not given,use zjuvpn1 for default
    sleep 2 #this sleep could not be deleted
    echo 'c '$vpnconf > /var/run/xl2tpd/l2tp-control
    sleep 7
    ifconfig | grep ppp0 >/dev/null && result=0 || result=1
    if [ $result == 0 ] #check if ppp0 established
        then
            echo `date` let us wait 20 sec to ensure the network is OK
            sleep 20 #this sleep could not be deleted
            route add default ppp0 #add ppp0 route to default
            route add default ppp0 #add ppp0 route to default
            route | grep default | grep ppp0 >/dev/null && result=0 || result=1
            if [ $result == 0 ]
              then echo `date` route set succeed | tee -a $zjulog
                   echo `date` VPN established successfully | tee -a $zjulog
              else echo `date` FAILED TO SET ROUTE | tee -a $zjulog
                   return 2
            fi
            return 0
        else
            echo `date` Err ppp0 DOES NOT EXIST | tee -a $zjulog
            return 1
    fi
}

function ReportMyIP(){
    myIP=`hostname -I|sed 's\ \%20\g'`  #get ip(s) and encode the space
    curl --insecure --user-agent "Chrome/23.3.3.3" "${urlToReceiveIP}${myIP}"
}

#--------main---------
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
#Try to connect taobao.com for 3 times
CheckWAN && result=0 || CheckWAN && result=0 || CheckWAN && result=0 || result=1
#Randomly choose an account
currentAccountNo=`expr $RANDOM % $totalVPNAccounts + 1`

until [ $result == 0 ] || [ $connectFailCount == 0 ];
    do
        CheckWire && result=0 || result=1
        if [ $result == 1 ]  #is wire connection to 10.5.1.9 available
            then ConnectZJUWLAN   #if not, connect ZJUWLAN
        fi
        sleep 5
        CheckLns && result=0 || result=1
        if [ $result == 0 ]  #only do this when we have a lns connection
            then
            echo `date` current account no $currentAccountNo | tee -a $zjulog
            ConnectVPN ${vpnProfilePrefix}${currentAccountNo}
            sleep 7 #this sleep could not be deleted
            is_Raspberry == 1 && ReportMyIP
        fi
        connectFailCount=`expr $connectFailCount - 1`
        currentAccountNo=`expr $currentAccountNo % $totalVPNAccounts + 1`
        CheckWAN && result=0 || CheckWAN && result=0 || result=1
    done
is_Raspberry == 1 && ReportMyIP
#End

注:本脚本中出于安全考虑,默认将所有内网流量都走VPN
如果需要设置其他内网网关,请自行添加route命令
如果同时使用SS的话,请把SS的开机运行放在本程序的后面,否则会导致SS无法使用


设置断线重连
本脚本支持直接用作断线检测与重连脚本
只需要在crontab中设置定时执行即可
首先给脚本设置执行权限(假设脚本在此位置)

chmod +x /etc/zjuvpn.sh

执行

crontab -e

在配置文件中添加

*/4 * * * * /etc/zjuvpn.sh #exec per 6 mins

让脚本每4分钟执行一次来检测网络连接情况(并断线重连)


服务端代码参考
附一个用于接收树莓派所发送IP简易的服务端代码参考(PHP)
接收到的IP会被写入到同目录的raspi.log中,缺点是需要每次自己从raspi.log中读取IP

<?php 
$timefomat = "c";
if(htmlspecialchars($_GET['ip'])){
	file_put_contents("./raspi.log",
					date( $timefomat ) . "\t"
					. htmlspecialchars($_GET['ip']). "\n"
					,FILE_APPEND
					);
	echo "Thank you\n";
}
else{
	header( $_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden' );
	header( 'Status: 403 Forbidden' );
	echo 'Access Denied';
}
?>

DNSPOD域名动态解析 代码参考
这个PHP包含前面面那个PHP的所有功能,ip记录会在本地的./logs/raspi.log里存一份
并且在ip修改时同时利用DNSPOD的API动态修改一条subdomain记录
这样在ssh里就只需要把地址设置成那个subdomain就可以了,不需要每次打开log去看ip
也不用每次点一下那个证书信任的东西
注:本脚本会自动处理DNSPOD的[相同记录不能频繁重复递交]的限制,跟上面那个脚本无缝配合
/*写得相当混乱..但是能用..别理我(逃*/

<?php
/*You Should Modify These Values*/
/*Please see DNSPOD's documents for yours*/
/*And you can contact me to get an subdomain if you are my friend*/
$yourDnspodLoginToken = '12345,4110e3351e11c42949d73f9fa360574f';
$yourDnspodDomainID = '26543210';
$yourDnspodRecordID = '128765432';
$yourDnspodSubdomainName = 'myraspisubdomain';
$isDebug = false;

/*-----------Main Program---------------*/
$timefomat = "c";
$str = htmlspecialchars($_GET['ip']);
if($str){
	file_put_contents("./logs/raspi.log",
					date( $timefomat ) . "\t"
					. $str . "\n"
					,FILE_APPEND
					);
}
else{
	header( $_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden' );
	header( 'Status: 403 Forbidden' );
	echo 'Im Not evil';
	die(0);
}

$str = explode(" ",trim($str));
$ip = $str[count($str) - 1];
$ip=filter_var($ip,FILTER_VALIDATE_IP);

if(!$ip){
	echo "Thank you";
	die(1);
}

$commonToken = array('login_token'=>$yourDnspodLoginToken
				,'format'=>'json'
				,'error_on_empty' => 'yes'
				,'domain_id' => $yourDnspodDomainID
				,'record_id' => $yourDnspodRecordID
				);
/*get current record info*/
$ch = curl_init("https://dnsapi.cn/Record.Info");
curl_setopt($ch,CURLOPT_POST,1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSLVERSION, 1);
curl_setopt($ch,CURLOPT_USERAGENT,"aploiumDDNS/1.0 (i@z.codes)");
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($commonToken));
$output = curl_exec($ch);
curl_close($ch);

if($isDebug) echo $output . "<hr>";
/*make sure ip has been changed*/
if(strpos($output,$ip) != false){
	echo "thank you\n";
	die(0);
}


if($isDebug) echo "<hr>";
/*submit ip change*/
$newIPdata = array('record_line' => '默认'
				,'sub_domain' => $yourDnspodSubdomainName
				,'value' => $ip);
$newIPdata = array_merge($commonToken,$newIPdata);
$ch = curl_init("https://dnsapi.cn/Record.Ddns");
curl_setopt($ch,CURLOPT_POST,1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSLVERSION, 1);
curl_setopt($ch,CURLOPT_USERAGENT,"aploiumDDNS/1.0 (i@z.codes)");
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($newIPdata));
$output = curl_exec($ch);
curl_close($ch);
if($isDebug) echo $output . "<hr>";
echo "succeed\n";


后续准备添加:

  • 多VPN账号支持(已完成)
  • DNSPOD动态解析支持(已完成)

Debian(Linux)连接浙大L2TP VPN的脚本

来一发吐槽叭O(∩_∩)O    仅首次吐槽时需要审核