---
title: "OpenWrt 校园网共享"
description: "本篇文章介绍了如何在校园网环境下突破「单账号仅限两台设备」的限制，并实现多账号带宽叠加。作者通过分析锐捷 Web Portal 认证流程，移植登录算法至 OpenWrt，利用 "
date: "2025-09-15"
tags:
  - "网络"
  - "OpenWRT"
canonical: "https://www.kanoverse.com/post/openwrt-campus-network-sharing"
---

# 背景

入学新学校，校园网 49元/月 100M ，只能够同时登录两台设备，于是着手破解。

破解后当普通 Wi-Fi 连接即可，无设备数量限制，同时还可以做到多账号网速叠加。

![Image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F69ee6f76-f8bf-40c0-a95a-469dca45d2f2%2Fff2d7907-ccff-4bef-a26a-546372eb3bde%2Fimage.png?table=block&id=1fbcdc04-0892-837d-a5a2-81a5401936ef&cache=v2&width=1400)

# 思路

1.  逆向校园网 Web 登录算法
2.  将登录算法移植到 OpenWrt 中
3.  使用 Macvlan 和 MultiWAN 插件实现多账号登录

# 登录算法逆向

连接校园网，随便输入一个网址就可以跳转到登录界面（这是通过 DNS劫持 实现的）。

通过观察代码发现，这是锐捷网络的 Web 认证，便在网络上找到了相同认证系统的登录算法实现，所以这一步非常的轻松，基本没有自己进行逆向分析。



[

![](https://img2023.cnblogs.com/blog/2433451/202503/2433451-20250311220850468-60945062.png)

校园网认证流程分析及自动认证脚本 - Mars-Luke - 博客园

校园网认证流程分析及自动认证脚本 很久之前在处理Linux终端环境下校园网认证登录时所做的工作，理论上适用于所有基于锐捷网络的认证系统 分析过程 未进行校园网认证时curl -v任意网站得到 可见会重定向到认证网址并且附带一些参数 浏览器访问该网址并打开网络日志，输入自己的账号密码测试登录 找到登录

cnblogs.com



](https://www.cnblogs.com/0x000001/p/18766279)

# OpenWrt

## 硬路由的选择

因为要使用 OpenWRT 系统，所以路由器需要选择能够刷机的，且自带 Wi-Fi 的硬路由。

这里选择的是 **H3C NX30 Pro**，二手售价约100元，配置如下：

<table class="notion-table"><tbody><tr><th>型号</th><td>H3C NX30 Pro</td></tr><tr><th>CPU</th><td>MT7981B</td></tr><tr><th>闪存</th><td>128M</td></tr><tr><th>内存</th><td>DDR3 256M</td></tr></tbody></table>

## 刷机教程

网络上有很多刷机的教程，这里不多赘述。

使用的 uboot 仓库如下：

[https://github.com/hanwckf/bl-mt798x](https://github.com/hanwckf/bl-mt798x)

## 固件的选择

固件选择了自编译 ImmortalWrt 23.05.6，且使用最精简固件的思路，仅保留了 `mwan`、`[turboacc](https://github.com/chenmozhijin/turboacc)`、`fileassistant`、`upnp` 插件。



**编译产物的 config.buildinfo 如下**

```plain
CONFIG_TARGET_mediatek=y
CONFIG_TARGET_mediatek_filogic=y
CONFIG_TARGET_mediatek_filogic_DEVICE_h3c_magic-nx30-pro-nmbm=y
CONFIG_LIBCURL_COOKIES=y
CONFIG_LIBCURL_CRYPTO_AUTH=y
CONFIG_LIBCURL_FILE=y
CONFIG_LIBCURL_FTP=y
CONFIG_LIBCURL_HTTP=y
CONFIG_LIBCURL_NGHTTP2=y
CONFIG_LIBCURL_OPENSSL=y
CONFIG_LIBCURL_PROXY=y
CONFIG_LIBCURL_TFTP=y
CONFIG_LIBCURL_THREADED_RESOLVER=y
CONFIG_LIBCURL_TLS_SRP=y
CONFIG_LIBCURL_UNIX_SOCKETS=y
CONFIG_PACKAGE_bash=y
CONFIG_PACKAGE_curl=y
CONFIG_PACKAGE_htop=y
CONFIG_PACKAGE_ip-tiny=y
CONFIG_PACKAGE_ip6tables-nft=y
CONFIG_PACKAGE_ipset=y
CONFIG_PACKAGE_iptables-mod-conntrack-extra=y
CONFIG_PACKAGE_iptables-mod-ipopt=y
CONFIG_PACKAGE_iptables-nft=y
CONFIG_PACKAGE_iputils-arping=y
CONFIG_PACKAGE_jq=y
CONFIG_PACKAGE_kmod-ip6tables=y
CONFIG_PACKAGE_kmod-ipt-conntrack=y
CONFIG_PACKAGE_kmod-ipt-conntrack-extra=y
CONFIG_PACKAGE_kmod-ipt-core=y
CONFIG_PACKAGE_kmod-ipt-ipopt=y
CONFIG_PACKAGE_kmod-ipt-ipset=y
CONFIG_PACKAGE_kmod-nf-conncount=y
CONFIG_PACKAGE_kmod-nf-ipt=y
CONFIG_PACKAGE_kmod-nf-ipt6=y
CONFIG_PACKAGE_kmod-nft-compat=y
CONFIG_PACKAGE_kmod-nft-fullcone=y
CONFIG_PACKAGE_kmod-tcp-bbr=y
CONFIG_PACKAGE_libblkid=y
CONFIG_PACKAGE_libcap=y
CONFIG_PACKAGE_libcap-ng=y
CONFIG_PACKAGE_libcurl=y
CONFIG_PACKAGE_libipset=y
CONFIG_PACKAGE_libiptext=y
CONFIG_PACKAGE_libiptext-nft=y
CONFIG_PACKAGE_libiptext6=y
CONFIG_PACKAGE_libmount=y
CONFIG_PACKAGE_libncurses=y
CONFIG_PACKAGE_libnghttp2=y
CONFIG_PACKAGE_libreadline=y
CONFIG_PACKAGE_librt=y
CONFIG_PACKAGE_libsmartcols=y
CONFIG_PACKAGE_libstdcpp=y
CONFIG_PACKAGE_libuuid=y
CONFIG_PACKAGE_libuv=y
CONFIG_PACKAGE_libwebsockets-full=y
CONFIG_PACKAGE_libxtables=y
CONFIG_PACKAGE_lsblk=y
CONFIG_PACKAGE_luci-app-fileassistant=y
CONFIG_PACKAGE_luci-app-mwan3=y
CONFIG_PACKAGE_luci-app-turboacc=y
CONFIG_PACKAGE_luci-app-upnp=y
CONFIG_PACKAGE_luci-i18n-mwan3-zh-cn=y
CONFIG_PACKAGE_luci-i18n-turboacc-zh-cn=y
CONFIG_PACKAGE_luci-i18n-upnp-zh-cn=y
CONFIG_PACKAGE_miniupnpd-nftables=y
CONFIG_PACKAGE_mwan3=y
CONFIG_PACKAGE_openssh-client=y
CONFIG_PACKAGE_openssh-keygen=y
CONFIG_PACKAGE_pdnsd-alt=y
CONFIG_PACKAGE_smartmontools=y
CONFIG_PACKAGE_smartmontools-drivedb=y
CONFIG_PACKAGE_terminfo=y
CONFIG_PACKAGE_xtables-nft=y
CONFIG_PACKAGE_zlib=y
```

* * *



通过上面的 `buildinfo` 编译出来的固件仅仅 15.4MB。

## 登录算法移植

这里使用 OpenWrt 的 `init.d` 与 `procd` 守护校园网登录状态，如果检测到离线则自动尝试登录。

代码如下：

```shell
#!/bin/sh /etc/rc.common
# campus portal login as an init.d service for OpenWrt (with procd monitor)

START=95
STOP=10
USE_PROCD=1

#  配置区 
USER_ID="" # 用户名
PASSWORD="" # 密码
SERVICE="DianXin" # Service 字段，没有可以留空

INTERFACE="eth1" # 网络端口，用于指定 Macvlan 多账号登录

PING_URL="http://baidu.com"
PORTAL_HOST="10.30.252.71" # 记得修改这个为自己学校的登录网址 IP
INDEX_URL="http://$PORTAL_HOST/eportal/index.jsp"
LOGIN_API="http://$PORTAL_HOST/eportal/InterFace.do?method=login"

CURL_UA="Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.84 Safari/537.36"
CURL_OPTS="-fsS --connect-timeout 5 --max-time 10 --interface $INTERFACE"

# 监控间隔（秒）
MONITOR_INTERVAL=60
# 断线判定失败后重试次数/间隔
RETRY_MAX=3
RETRY_SLEEP=2

EXTRA_COMMANDS="login sstatus"
EXTRA_HELP="        login   Run a one-shot portal login now
        sstatus  Print current connectivity/portal status"

url_encode() {
    local s="${1}" i c out=""
    for i in $(seq 1 ${#s}); do
        c="$(printf '%s' "$s" | cut -c $i)"
        case "$c" in
            [a-zA-Z0-9.~_-]) out="${out}${c}" ;;
            *) out="${out}$(printf '%%%02X' "'$c")" ;;
        esac
    done
    printf '%s' "$out"
}

is_online() {
    local resp
    resp="$(curl $CURL_OPTS "$PING_URL" -A "$CURL_UA" || true)"
    echo "$resp" | grep -q "http://www\.baidu\.com/" && return 0 || return 1
}

extract_query_string() {
    local resp="$1"
    echo "$resp" \
    | grep -o "top\.self\.location\.href='http://$PORTAL_HOST/eportal/index\.jsp?[^']\+'" \
    | sed "s#.*index.jsp?##; s/'\$//"
}

do_login_once() {
    echo "[campus_portal_eth1] Probing connectivity..."
    if is_online; then
        echo "[campus_portal_eth1] Already online."
        return 0
    fi

    echo "[campus_portal_eth1] Fetching portal redirect page..."
    local resp qs enc1 enc2 post_data login_json
    resp="$(curl $CURL_OPTS "$PING_URL" -A "$CURL_UA" || true)"
    qs="$(extract_query_string "$resp")"
    if [ -z "$qs" ]; then
        echo "[campus_portal_eth1] ERROR: failed to get queryString."
        return 1
    fi
    echo "[campus_portal_eth1] Got queryString (len=${#qs})."

    enc1="$(url_encode "$qs")"
    enc2="$(url_encode "$enc1")"
    post_data="userId=$(url_encode "$USER_ID")&password=$(url_encode "$PASSWORD")&service=$(url_encode "$SERVICE")&queryString=$enc2&operatorPwd=&operatorUserId=&validcode=&passwordEncrypt=false"

    echo "[campus_portal_eth1] Logging in..."
    login_json="$(curl $CURL_OPTS -X POST "$LOGIN_API" \
        -H "User-Agent: $CURL_UA" \
        -H "Referer: $INDEX_URL" \
        -H "Accept: */*" \
        -H "Accept-Encoding: gzip, deflate" \
        -H "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8" \
        -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
        --data "$post_data" || true)"

    echo "$login_json" | grep -q '"result":"success"' && {
        echo "[campus_portal_eth1] Login success."
        return 0
    }
    echo "[campus_portal_eth1] Login failed."
    echo "$login_json" | sed -n 's/.*"message":"\([^"]*\)".*/[campus_portal_eth1] reason: \1/p'
    return 2
}

# procd 管理的常驻监控
_monitor_loop() {
    # 注意：这里不要用 'EOF'，让变量在这里就展开
    cat <<EOF
MONITOR_INTERVAL=${MONITOR_INTERVAL}
RETRY_MAX=${RETRY_MAX}
RETRY_SLEEP=${RETRY_SLEEP}
PING_URL="${PING_URL}"
CURL_UA="${CURL_UA}"

is_online() {
    resp="\$(curl -fsS --connect-timeout 5 --max-time 10 --interface $INTERFACE "\$PING_URL" -A "$CURL_UA" || true)"
    echo "\$resp" | grep -q "http://www\\.baidu\\.com/"
}

while true; do
    if is_online; then
        sleep "\$MONITOR_INTERVAL"
        continue
    fi

    echo "[campus_portal_eth1] offline detected, trying to login..."
    tries=1
    rc=1
    while [ "\$tries" -le "\$RETRY_MAX" ]; do
        # 请注意，一定要保证这里的文件名和你的文件名一致。
        /etc/init.d/campus_portal_eth1 login && { rc=0; break; }
        sleep "\$RETRY_SLEEP"
        tries=\$((tries+1))
    done
    if [ "\$rc" -ne 0 ]; then
        echo "[campus_portal_eth1] auto-login failed after \$RETRY_MAX tries."
    fi
    sleep "\$MONITOR_INTERVAL"
done
EOF
}

start_service() {
    procd_open_instance
    # 用 /bin/sh -c 启一个 loop，当作守护进程
    procd_set_param command /bin/sh -c "$(_monitor_loop)"
    # 异常退出自动重启（首次延迟 5s，之后指数回退 60s 封顶）
    procd_set_param respawn 5 1 60
    procd_set_param stdout 1
    procd_set_param stderr 1
    # 可按需添加：当网络配置变更时重启该实例
    procd_close_instance
}

stop_service() {
    # procd 会杀掉守护进程
    return 0
}

reload_service() { stop; start; }

service_triggers() {
    # 当 network / wireless 配置被 commit 时触发
    procd_add_reload_trigger "network" "wireless"
}

# rc.common 默认 boot() 会调用 start()
# 我们保留一次性手动命令
login() { do_login_once; }
sstatus() { if is_online; then echo "[campus_portal_eth1] Online"; else echo "[campus_portal_eth1] Offline"; fi; }
```



修改完配置后将代码写到 `/etc/init.d/campus_portal_eth1`（注意，文件名要在代码里面一并修改），启动后可以在**系统日志**中看到日志信息。

```shell
# 开机启动
/etc/init.d/campus_portal_eth1 enable
# 启动
/etc/init.d/campus_portal_eth1 start
```

## 多账号

配置详细教程如下：**[基于openwrt的MWAN3实现多运营商负载均衡的一种方法](https://zhuanlan.zhihu.com/p/352098418)**



配置完成端口后将上面的 `init.d` 代码全局替换 `eth1` 为你创建的接口后，复制一份到 `/etc/init.d/` 中即可，每个账号一个服务这样可以保证对每个校园网账号的登录控制。

### Macvlan

在 网络→接口→设备 中可以添加 Macvlan，选择正确的 基础设备 后点击保存即可。

添加 Macvlan 完成后在 网络→接口 中添加对应的 DHCP客户端，同时设置防火墙区域为 `wan` 即可。

![Image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F69ee6f76-f8bf-40c0-a95a-469dca45d2f2%2F25057bb1-38d6-456b-af2e-c00ad7ee580d%2Fimage.png?table=block&id=eeacdc04-0892-820d-8e7e-810865591911&cache=v2&width=1400)

![Image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F69ee6f76-f8bf-40c0-a95a-469dca45d2f2%2F6a9d7707-b0e1-4c26-9100-516f072d9639%2Fimage.png?table=block&id=664cdc04-0892-83d9-a442-81857bbafb9e&cache=v2&width=1400)

### **MultiWAN** 负载均衡

![Image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F69ee6f76-f8bf-40c0-a95a-469dca45d2f2%2Fc2b9f3a9-bba5-4fb7-849b-2dbe04018be4%2Fimage.png?table=block&id=8c9cdc04-0892-838d-80fb-81b5cf99bbd4&cache=v2&width=1400)

![Image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F69ee6f76-f8bf-40c0-a95a-469dca45d2f2%2Fcd4be05f-7aa0-4f96-84e2-0e4b8de80c88%2Fimage.png?table=block&id=1c9cdc04-0892-827d-8fad-81020463ee3d&cache=v2&width=1400)

![Image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F69ee6f76-f8bf-40c0-a95a-469dca45d2f2%2F2195e259-0397-44a1-975d-4dfcd96c281f%2Fimage.png?table=block&id=01fcdc04-0892-8243-bf27-81bf9e4baa68&cache=v2&width=1400)

![Image](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F69ee6f76-f8bf-40c0-a95a-469dca45d2f2%2F8cf88a03-372a-46b3-b427-81ac3cc2f905%2Fimage.png?table=block&id=bc0cdc04-0892-820c-b0b2-01665b1cc35b&cache=v2&width=1400)

`mwan` 的配置教程这里就不过多赘述了，我使用个两个接口（也就是两个账号）配置如上图。`mwan` 是通过 `iptables` + `fwmark` 路由规则来实现的负载均衡，所以在单线程情况下，无法速度叠加。

# 附图

![OpenWrt 后台界面](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F69ee6f76-f8bf-40c0-a95a-469dca45d2f2%2F0083796e-972d-4fbd-9421-f176d77e5ba0%2Fimage.png?table=block&id=625cdc04-0892-8310-8fe1-01a87ad9d9c6&cache=v2&width=1400)
*OpenWrt 后台界面*

![100M + 50M 账号 mwan 测速](https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F69ee6f76-f8bf-40c0-a95a-469dca45d2f2%2Fccf38c0e-091b-4650-a5bb-ff01ad69b215%2Fimage.png?table=block&id=62ecdc04-0892-8326-9ec8-8130a1a1b02a&cache=v2&width=1400)
*100M + 50M 账号 mwan 测速*
