ocserv指定分配给客户端的IP地址与反向添加路由

天锦 发表于 码农也得有格调 分类,标签: OpenWRTVPNOpenConnectOcserv

遇到的问题

有一个异地组网的需求,也就是说需要搭建VPN进行异地组网,常见的PPTP和L2TP由于某些特殊原因,不得不抛弃不用,重新选择新的协议,思科的AnyConnect基于SSL加密,可以自定义通信的端口号,是个不错的选择,部署了兼容的开源方案ocserv作为服务器,用了一段时间,感觉也很稳定。但是ocserv默认的plan密码只能设置用户名和密码,客户端的IP地址池在ocserv.conf中定义,不能像L2TP和PPTP这些基于PPP拨号获取IP的协议一样指定客户端获取的IP地址,客户端获取的IP地址是随机的,这样一来对于终端用户远程访问很方便,但是对于多站异地组网就很麻烦了,给反向添加路由条目带来了很大的麻烦。

假定有两个站点A(10.1.1.1/24)和B(10.2.2.1/24),A作为VPN服务器,VPN接口网络为192.168.240.1/24,当使用L2TP等基于PPP拨号的协议组网时,由于ppp可以指定用户的ip地址,就可以直接添加静态路由就可以了。如B站点使用用户site_b链接A站点的服务器,指定客户端IP为192.168.240.2,那么在A站点和B站点各加一条静态路由就可以实现A,B互通了:

路由器A:

[root@route-A ~]# route add -net 10.2.2.0 netmask 255.255.255.0 gw 192.168.240.2

路由器B:

[root@route-B ~]# route add -net 10.1.1.0 netmask 255.255.255.0 gw 192.168.240.1

但是ocserv的配置文件不能指定客户端的ip,添加对端路由时就很难受。

基本解决方案

ocserv提供了一个occtl的管理工具可以使用occtl show users 命令查看当前已经连接的用户和客户端

[root@route-A ~]# occtl show users
      id     user    vhost             ip         vpn-ip device   since    dtls-cipher    status
    1399   site_b  default    61.53.53.53  192.168.240.9  vpns0  7m:22s      (no-dtls) connected

像这样的,用occtl命令能得到客户端的ip地址,在用akw命令剥离出ip地址,再进行路由的添加,可以使用cron定时检查当前连接的客户端信息,信息判断是否要添加路由。以下是我用过的路由添加脚本,在这里贴出来供朋友们参考:

#!/bin/bash
setRoute(){
    TARGET_USER=$1
    TARGET_ROUTE=$2
    TARGET_MASK=$3
    echo "--------------------------------------------------------------------"
    echo "|" $(date) "Checking for "$TARGET_USER"@"$TARGET_ROUTE
    ROUTE_TO_TARGET=$(route -n | grep $TARGET_ROUTE)
    if [[ -z "$ROUTE_TO_TARGET" ]];then
        echo "| Route NOT esixt!Checking vpn ststus and add route..."
        OC_USERS=$(occtl show users)
        OC_USERS_COUNT=$(occtl show users | wc -l | awk '{print $1}')
        USER_ONLINE='False'
        for i in $(seq 2 $OC_USERS_COUNT)
        do
            OC_USER=$(occtl show users|sed -n "$i"p)
            USER_NAME=$(echo $OC_USER | awk '{print $2}')
            USER_IP=$(echo $OC_USER | awk '{print $5}')
            USER_DEV=$(echo $OC_USER | awk '{print $6}')
            if [ $USER_NAME = $TARGET_USER ];then
                
                if [ $USER_ONLINE = 'False' ];then
                    USER_ONLINE='True'
                    echo "| add route..."
                    route add -net $TARGET_ROUTE netmask $TARGET_MASK gateway $USER_IP dev $USER_DEV
                    echo "| " $(route -n | grep Destination)
                    echo "| " $(route -n | grep $TARGET_ROUTE)
                    echo "| Done!"
                else
                   echo "| Multiple same users online! not operate!"
                fi
            fi
        done
        if [ $USER_ONLINE = 'False' ];then
            echo "| User NOT Online!"
        fi
    else
        #Route exist,compared then update
        echo "| Route already exists!Checking vpn ststus and update route..."
        PARE_IP=$(echo $ROUTE_TO_TARGET | awk '{print $2}')
        PARE_MASK=$(echo $ROUTE_TO_TARGET | awk '{print $3}')
        PARE_DEV=$(echo $ROUTE_TO_TARGET | awk '{print $8}')
        OC_USERS=$(occtl show users)
        OC_USERS_COUNT=$(occtl show users | wc -l | awk '{print $1}')
        USER_ONLINE='False'
        for i in $(seq 2 $OC_USERS_COUNT)
        do
            OC_USER=$(occtl show users|sed -n "$i"p)
            USER_NAME=$(echo $OC_USER | awk '{print $2}')
            USER_IP=$(echo $OC_USER | awk '{print $5}')
            USER_DEV=$(echo $OC_USER | awk '{print $6}')
            if [ $USER_NAME = $TARGET_USER ];then
                if [ $USER_ONLINE = 'False' ];then
                    USER_ONLINE='True'
                    if [[ $PARE_IP != $USER_IP ]] || [[ $PARE_DEV != $USER_DEV ]];then
                        echo "| Update route..." 
                        route del -net $TARGET_ROUTE netmask $PARE_MASK dev $PARE_DEV
                        route add -net $TARGET_ROUTE netmask $TARGET_MASK gateway $USER_IP dev $USER_DEV
                        echo "| " $(route -n | grep Destination)
                        echo "| " $(route -n | grep $TARGET_ROUTE)
                        
                    else
                        echo "| Local Router Configuration is correct, no update required." 
                    fi
                else
                    echo "| Multiple same users online! not operate!"
                fi
            fi
        done
        if [ $USER_ONLINE = 'False' ];then
            echo "| User NOT Online!"
            echo "| Delete route while user offline!"
            route del -net $TARGET_ROUTE netmask $PARE_MASK dev $PARE_DEV
        fi
    fi
    echo "--------------------------------------------------------------------"
}
setRoute 'site_b' '10.2.2.0' '255.255.255.0'

使用cron定时执行这个脚本就能实现定时检查链路状态并添加和维护到对端站点的路由了,但是由于cron是定时执行,频率太快影响性能,太慢又更新不及时,多少有些不完美。

提高效率

后来在调别的相关业务的时候偶然得知ocserv有一个连接建立后执行脚本和断开连接后执行脚本的配置选项,在ocserv.conf配置文件中,配置完之后会在新连接建立后和连接断开后执行,并且会将相应的用户名,IP地址等信息以变量$IP_REMOTE、$REASON、$USERNAME、$DEVICE提供,所以就对基于cron的脚本进行了改造:

addRoute() {
    USERNAME=$1
    ROUTE_NEXT=$2
    ROUTE_DEVICE=$3
    case "$USERNAME" in
        'site_b')
            route add -net 10.2.2.0 netmask 255.255.255.0 gateway $ROUTE_NEXT dev $ROUTE_DEVICE
        *) ;;
    esac
}
delRoute() {
    USERNAME=$1
    ROUTE_NEXT=$2
    ROUTE_DEVICE=$3
    case "$USERNAME" in
        'site_b')
            route del -net 10.2.2.0 netmask 255.255.255.0 gateway $ROUTE_NEXT dev $ROUTE_DEVICE
        *);;
    esac
}
case "$REASON" in
    connect)
        addRoute $USERNAME $IP_REMOTE $DEVICE ;;
    disconnect)
        delRoute $USERNAME $IP_REMOTE $DEVICE ;;
    *) ;;
esac

在/etc/ocserv/ocserv.conf中配置:

connect-script = /usr/bin/ocserv-script
disconnect-script = /usr/bin/ocserv-script

一定要指定IP

如果说其他原因一定要指定ocserv给openconnect客户端指定ip地址的话,也不是不可行,也是在调别的相关业务的时候找到了这个的解决门路。(和前面提到的是一个业务,是OSPF动态路由相关的,没办法,组网越来越大,静态路由已经很难搞了,详细的见下一篇博文会具体介绍)ocserv默认的plan认证方式不支持,但是ocserv支持radius认证,radius可以指定ip地址,也可以根据不同的用户名,匹配下发不同的路由表,这个就很绝了!参考https://github.com/openconnect/ocserv/blob/master/doc/README-radius.md

其中指出了使用Framed-IP-Address指定客户端IP地址,使用Framed-Route可以指定下发给客户端的路由,并且可以指定多条路由,如:

site_b        Cleartext-Password := "password@user"
        Framed-IP-Address := "192.168.194.2",
        Framed-Route := "192.168.192.0/18",
        Framed-Route := "10.0.0.0/8"

注意:本方法仅限常见的CentOS,Ubuntu的Linux 服务器系统可以实现,OpenWRT系统中的ocserv包不支持radius(目前我没有研究出来),另外ocserv作者推荐使用radcli库进行radius客户端的连接,其他radius客户端ocserv作者并未测试。

更新:OpenWRT下的ocserv使用radius进行认证的方法已在文章《OpenWRT系统中ocserv不支持使用Radius指定客户端IP的解决方法》中更新。

部署的方法为安装freeradius和radcli

[root@ocserv ~] yum install freeradius radcli -y

编辑/etc/radcli/radiusclient.conf 和/etc/radcli/server中authserver的地址和密码。

修改ocserv.conf使其使用radius进行认证

auth = "radius[config=/etc/radcli/radiusclient.conf,groupconfig=true]"

在/etc/raddb/user中加入用户信息和指定的IP地址、路由信息等。具体搜索 "Openconnect + Radius" 即可。

动态路由

慢慢的,组网的站点从两个发展到了四个了,每次有新的网段加入后,路由调整能把人折磨死,手工的添加管理静态路由已经无法满足现有的网络环境了,所以我尝试了OSPF动态路由协议去让这些站点动态的学习和维护路由,这样一个节点崩掉后其他节点也能自动更换路由绕开这个节点实现网络的自愈,这是手动静态路由所不能达到的。在研究OSPF的过程中,不得不说一句RouterOS真心好用!但是由于我的各个节点多是使用的OpenWRT设备做路由,如果全部节点更换到RouterOS又要购买新的设备,又是一笔不小的开支,只能硬着头皮去折腾和尝试在OpenWRT上跑OSPF协议了,而且是over ocserv的,过程相当蛋疼,欢迎观摩我的下一篇博文《在OpenWrt上部署OSPF进行异地组网的动态路由生成》。