广西大学校园网断网自动重连程序

因为广西某211高校校园网有时候会不稳定波动,导致设备掉线。而如果在需要挂机进行一些工作的场合出现这个问题,将会相当蛋疼。再加上在宿舍每天晚上断网后,第二天都要手动重新登录着实有点麻烦,于是乎我就用我入门级中的入门级Go编程能力写了这个程序。

程序实现登录方法查找

因为学校推荐是使用浏览器打开认真网页进行登录,而经过抓包后,我发现相比于大一的时候,校园网的网页传输数据进行了加密。也就是,如果我要通过逆向网页JavaScript脚本获得登录请求发送逻辑,代价有点大。不过好在你西还是关照到了无GUI Linux服务器使用校园网的问题,在官网挂出了登录方法:linux系统宽带客户端-2024.12.30日后使用
这就好办了嘛,只需要构造这个URL请求:

http://172.17.0.2:801/eportal/portal/login?callback=dr1003&login_method=1&user_account=账号@cmcc&user_password=密码&wlan_user_ip=终端IP&wlan_user_ipv6=&wlan_user_mac=000000000000&wlan_ac_ip=&wlan_ac_name=&jsVersion=4.2.1&terminal_type=1&lang=zh-cn&v=5574&lang=zh

发送给认证服务器就可以登录校园网了。
不过我注意到了这个请求URL里面,MAC填写了000000000000,按理来说这部分应该填写设备的MAC,用于保持登录状态和后续无感知登录。这样填写无效的MAC后面大概率会因为校园网波动或者短暂断联掉登录,所以在我的程序逻辑上,这部分应该补上。
现在需要发送的校园网认证信息就理清楚了:

  • 账号
  • 密码
  • 线路类型
  • 设备IP
  • 设备MAC

我只需要写一个程序获取到以上信息,然后持续不断判断网络通断状态,当检测到断网后构造认证的URL并发送,就可以实现校园网断网自动重连了。

用Go实现校园网自动登录

其实之前我就用Python写过一版校园网自动登录程序,但是因为Python的效率和占用确实有点感人了,所以我觉得还是选择编译型语言来进行构建吧。好巧不巧之前学了一些Golang,要不就拿这个试试看?

实现思路

因为Go是编译型语言,登录的配置就不能像Python那样让用户打开源代码写在对应的变量里面了,得写一个配置文件机制。也就是首次运行创建一个文本的配置文件,然后用户填写了相关的信息,再运行程序,程序从配置文件读取信息并且执行校园网认证和断网自动重连工作。
那这样看,实现思路就很清晰了:

  • 程序首次运行,输出配置文件模板,提示用户填写
  • 用户填写好后,读取配置文件并进行基础的校验,确保参数填写无误
  • 实现校园网登录认证的函数,网络通断检测函数,以及上网时段判断函数避免在非上网时段反复尝试重连

对于配置文件的部分,我想采用类似ini配置文件的格式,这是模板:

# 校园网登录脚本信息设置:(注意请不要改变格式)
# 用户名:(填写示例:User=1807210721)
User=
# 密码:(填写示例:Password=www.nekopara.uk)
Password=
# 运营商选择,留空选择校园网,如果需要选择运营商,电信填写telecom,联通填写unicom,移动填写cmcc
Net_Type=
# 是否开启学生上网时段模式?1为开启,0为关闭,开启后周一到周五0:00-6:00将不会尝试重连
Student_Mode=0
# 开启路由器登录模式:
# 如果填写以下两个参数(均非空),则使用指定的路由器IP和MAC进行认证。
# 否则使用本机IP和MAC。
# 示例:
# Router_IP=172.16.6.6
# Router_MAC=36:88:8A:99:A4:CC
Router_IP=
Router_MAC=

考虑到为了便于小白使用,我输出的是.txt文件,可以直接记事本打开。
以及,添加一个命令行启动参数支持,可以让程序作为服务运行和部署,例如:

./GXU_Net_AutoLogin -user 1807210721 -passwd mypassword -nettype telecom -studentmode -ip 172.16.6.6 -mac 36:88:8A:99:A4:CC

现在技术架构想清楚了,开始编程吧!

实现代码

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
    "net/http"
    "net"
    "net/url"
    "time"
    "io"
    "flag"
)

const configFileName = "config.txt"

// Config 结构体保存配置
type Config struct {
    User        string
    Password    string
    NetType     string // 新增字段
    StudentMode bool

    // 路由器模式(当两者都非空时启用)
    RouterIP  string
    RouterMAC string
}

// loadConfig 加载或创建配置文件
func loadConfig() (*Config, error) {
    // 检查文件是否存在
    if _, err := os.Stat(configFileName); os.IsNotExist(err) {
        // 创建默认模板
        defaultContent := `# 校园网登录脚本信息设置:(注意请不要改变格式)
# 用户名:(填写示例:User=1807210721)
User=
# 密码:(填写示例:Password=www.nekopara.uk)
Password=
# 运营商选择,留空选择校园网,如果需要选择运营商,电信填写telecom,联通填写unicom,移动填写cmcc
Net_Type=
# 是否开启学生上网时段模式?1为开启,0为关闭,开启后周一到周五0:00-6:00将不会尝试重连
Student_Mode=0
# 开启路由器登录模式:
# 如果填写以下两个参数(均非空),则使用指定的路由器IP和MAC进行认证。
# 否则使用本机IP和MAC。
# 示例:
# Router_IP=172.16.6.6
# Router_MAC=36:88:8A:99:A4:CC
Router_IP=
Router_MAC=
`

            err = os.WriteFile(configFileName, []byte(defaultContent), 0644)
            if err != nil {
                return nil, fmt.Errorf("无法创建配置文件: %v", err)
            }
            return nil, fmt.Errorf("未找到配置文件,配置文件 '%s' 已创建,请先填写上网信息后重新运行程序", configFileName)
    }

    // 读取并解析
    content, err := os.ReadFile(configFileName)
    if err != nil {
        return nil, fmt.Errorf("无法读取配置文件: %v", err)
    }

    cfg := &Config{}
    scanner := bufio.NewScanner(strings.NewReader(string(content)))
    for scanner.Scan() {
        line := strings.TrimSpace(scanner.Text())

        // 跳过空行和注释
        if line == "" || strings.HasPrefix(line, "#") {
            continue
        }

        // 按第一个 '=' 分割(避免密码含等号出错)
        parts := strings.SplitN(line, "=", 2)
        if len(parts) != 2 {
            continue // 格式错误,跳过
        }

        key := strings.TrimSpace(parts[0])
        value := strings.TrimSpace(parts[1])

        switch key {
            case "User":
                cfg.User = value
            case "Password":
                cfg.Password = value
            case "Net_Type":
                cfg.NetType = value // 新增这一行
            case "Student_Mode":
                cfg.StudentMode = (value == "1")
            case "Router_IP":
                cfg.RouterIP = value
            case "Router_MAC":
                cfg.RouterMAC = value
        }
    }

    if err := scanner.Err(); err != nil {
        return nil, err
    }

    // 基础校验
    if cfg.User == "" || cfg.Password == "" {
        return nil, fmt.Errorf("请在 '%s' 中填写用户名和密码", configFileName)
    }
    // 在 loadConfig 函数中,解析配置后添加:
    if cfg.NetType != "" {
        // 检查是否是合法的运营商
        valid := false
        switch strings.ToLower(cfg.NetType) {
            case "telecom", "unicom", "cmcc":
                valid = true
        }

        if !valid {
            return nil, fmt.Errorf("错误:运营商类型必须为空、telecom、unicom或cmcc(不区分大小写),当前值: %s", cfg.NetType)
        }
    }

    return cfg, nil
}

func getLocalIP() (string, error) {
    conn, err := net.Dial("udp", "8.8.8.8:80")
    if err != nil {
        return "", err
    }
    defer conn.Close()
    return conn.LocalAddr().(*net.UDPAddr).IP.String(), nil
}

func getMACAddress() (string, error) {
    interfaces, err := net.Interfaces()
    if err != nil {
        return "", err
    }

    for _, iface := range interfaces {
        if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
            continue
        }

        mac := iface.HardwareAddr.String()
        if mac == "" {
            continue
        }

        addrs, _ := iface.Addrs()
        for _, addr := range addrs {
            if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil {
                return mac, nil
            }
        }
    }
    return "", fmt.Errorf("no active network interface with MAC found")
}

func isNetworkOK() bool {
    client := &http.Client{
        Timeout: 1 * time.Second,
    }
    resp, err := client.Get("http://connect.rom.miui.com/generate_204")
    if err != nil {
        return false // 网络不通 / DNS 故障 / 超时
    }
    defer resp.Body.Close()

    return resp.StatusCode == 204
}

func login(cfg *Config, ip, mac string) {
    // 格式化 MAC:去掉冒号,转小写(适配你 bash 脚本的行为)
    cleanMAC := strings.ReplaceAll(strings.ToLower(mac), ":", "")

    userAccount := cfg.User
    if cfg.NetType != "" {
        userAccount = cfg.User + "@" + cfg.NetType
    }

    params := url.Values{
        "callback":       {"dr1003"},
        "login_method":   {"1"},
        "user_account":   {userAccount},
        "user_password":  {cfg.Password},
        "wlan_user_ip":   {ip},
        "wlan_user_mac":  {cleanMAC},
        "wlan_user_ipv6": {""},
        "wlan_ac_ip":     {""},
        "wlan_ac_name":   {""},
        "jsVersion":      {"4.2.1"},
        "terminal_type":  {"1"},
        "lang":           {"zh-cn"},
        "v":              {"5574"},
    }

    loginURL := "http://172.17.0.2:801/eportal/portal/login?" + params.Encode()

    resp, err := http.Get(loginURL)
    if err != nil {
        fmt.Printf("❌ 登录请求失败: %v\n", err)
        return
    }
    defer resp.Body.Close()

    // 读取并打印响应体
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("❌ 读取响应体失败: %v\n", err)
        return
    }
    bodyStr := string(body)

    fmt.Printf("✅ 已发送登录请求(HTTP状态码: %d)\n", resp.StatusCode)
    fmt.Printf("响应内容: %s\n", bodyStr)
}

func shouldSkipLogin(cfg *Config) bool {
    if !cfg.StudentMode {
        return false
    }

    now := time.Now()
    weekday := now.Weekday() // Sunday = 0, Monday = 1, ..., Friday = 5
    hour := now.Hour()

    // 周一到周五(1~5),且 0:00 ~ 5:59
    if weekday >= time.Monday && weekday <= time.Friday && hour >= 0 && hour < 6 {
        fmt.Println("🌙 学生模式:当前为禁网时段,暂停重连")
        return true
    }

    return false
}

func getLoginInfo(cfg *Config) (ip, mac string, err error) {
    // 如果启用了路由器模式(两个字段都非空)
    if cfg.RouterIP != "" && cfg.RouterMAC != "" {
        fmt.Println("🌐 使用路由器模式进行认证")
        return cfg.RouterIP, cfg.RouterMAC, nil
    }

    // 否则使用本机信息
    fmt.Println("💻 使用本机模式进行认证")
    ip, err = getLocalIP()
    if err != nil {
        return "", "", fmt.Errorf("获取本机IP失败: %w", err)
    }
    mac, err = getMACAddress()
    if err != nil {
        return "", "", fmt.Errorf("获取本机MAC失败: %w", err)
    }
    return ip, mac, nil
}

func printHelp() {
    fmt.Println(`广西大学校园网自动登录程序参数说明:
必须参数:
-user      用户名(必须提供)
-passwd    密码(必须提供)

可选参数:
-nettype   运营商类型(telecom, unicom, cmcc),不加参数则使用校园网
-studentmode  启用学生模式(不带值)
-ip        路由器IP(必须与-mac一起使用)
-mac       路由器MAC(必须与-ip一起使用)
-help      显示此帮助信息

示例(Linux):
./GXU_Net_AutoLogin -user 1807210721 -passwd mypassword
/opt/GXU_Net_AutoLogin/GXU_Net_AutoLogin -user 1807210721 -passwd mypassword -nettype telecom -studentmode
./GXU_Net_AutoLogin -user 1807210721 -passwd mypassword -ip 172.16.6.6 -mac 36:88:8A:99:A4:CC

示例(Windows):
GXU_Net_AutoLogin.exe -user 1807210721 -passwd mypassword
C:\\Program Files\\GXU_Net_AutoLogin\\GXU_Net_AutoLogin.exe -user 1807210721 -passwd mypassword -nettype telecom -studentmode
C:\\Program Files\\GXU_Net_AutoLogin\\GXU_Net_AutoLogin.exe -user 1807210721 -passwd mypassword -ip 172.16.6.6 -mac 36:88:8A:99:A4:CC
`)
}

func main() {
    fmt.Printf("🚀广西大学校园网自动登录程序 By:GTX690战术核显卡导弹(www.nekopara.uk)\n")
    // 定义命令行参数
    var (
        user        string
        passwd      string
        nettype     string
        studentMode bool
        ip          string
        mac         string
        help        bool
    )

    flag.StringVar(&user, "user", "", "用户名")
    flag.StringVar(&passwd, "passwd", "", "密码")
    flag.StringVar(&nettype, "nettype", "", "运营商类型(telecom, unicom, cmcc)")
    flag.BoolVar(&studentMode, "studentmode", false, "启用学生模式")
    flag.StringVar(&ip, "ip", "", "路由器IP(必须与-mac一起使用)")
    flag.StringVar(&mac, "mac", "", "路由器MAC(必须与-ip一起使用)")
    flag.BoolVar(&help, "help", false, "显示帮助信息")
    flag.Parse()

    // 显示帮助信息
    if help {
        printHelp()
        os.Exit(0)
    }

    // 检查必须参数
    if (user == "" && passwd == "") {
        // 从配置文件加载
        cfg, err := loadConfig()
        if err != nil {
            fmt.Println("❌ 错误:", err)
            fmt.Println("💡 请编辑 config.txt 后重新运行本程序。")
            os.Exit(1)
        }
        fmt.Printf("✅ 配置加载成功!\n")
        fmt.Printf("用户: %s\n", cfg.User)
        fmt.Printf("密码: %s\n", cfg.Password)
        fmt.Printf("运营商: %s\n", cfg.NetType)
        fmt.Printf("学生模式: %t\n", cfg.StudentMode)
        if cfg.RouterIP != "" && cfg.RouterMAC != "" {
            fmt.Printf("路由器模式: IP=%s, MAC=%s\n", cfg.RouterIP, cfg.RouterMAC)
        }
        // 使用配置文件中的配置
        cfg.User = cfg.User
        cfg.Password = cfg.Password
        cfg.NetType = nettype // 优先使用命令行参数,如果命令行没提供则保持配置文件中的值
        cfg.StudentMode = studentMode
        cfg.RouterIP = ip
        cfg.RouterMAC = mac
    } else if user != "" && passwd != "" {
        // 从命令行参数加载
        cfg := &Config{
            User:        user,
            Password:    passwd,
            NetType:     nettype,
            StudentMode: studentMode,
            RouterIP:    ip,
            RouterMAC:   mac,
        }

        // 校验运营商类型
        if nettype != "" {
            valid := false
            switch strings.ToLower(nettype) {
                case "telecom", "unicom", "cmcc":
                    valid = true
            }
            if !valid {
                fmt.Printf("❌ 错误:运营商类型必须为telecom, unicom, cmcc(不区分大小写),当前值: %s\n", nettype)
                os.Exit(1)
            }
        }

        // 校验路由器IP/MAC
        if (ip != "" && mac == "") || (ip == "" && mac != "") {
            fmt.Println("❌ 错误:必须同时提供ip和mac参数,两者缺一不可")
            os.Exit(1)
        }

        // 显示配置
        fmt.Printf("✅ 命令行参数加载成功!\n")
        fmt.Printf("用户: %s\n", cfg.User)
        fmt.Printf("密码: %s\n", cfg.Password)
        fmt.Printf("运营商: %s\n", cfg.NetType)
        fmt.Printf("学生模式: %t\n", cfg.StudentMode)
        if cfg.RouterIP != "" && cfg.RouterMAC != "" {
            fmt.Printf("路由器模式: IP=%s, MAC=%s\n", cfg.RouterIP, cfg.RouterMAC)
        }

        // 使用命令行参数配置
        cfg.User = user
        cfg.Password = passwd
        cfg.NetType = nettype
        cfg.StudentMode = studentMode
        cfg.RouterIP = ip
        cfg.RouterMAC = mac
    } else {
        // 只提供了其中一个参数
        fmt.Println("❌ 错误:必须同时提供user和passwd参数,或者都不提供(通过配置文件)")
        fmt.Println("💡 请使用 -help 查看参数说明")
        os.Exit(1)
    }

    // 获取用于登录的 IP 和 MAC(自动判断模式)
    ipAddr, macAddr, err := getLoginInfo(&Config{
        RouterIP:  ip,
        RouterMAC: mac,
    })
    if err != nil {
        fmt.Printf("❌ %v\n", err)
        os.Exit(1)
    }

    fmt.Printf("✅ 守护进程启动:认证IP=%s | 认证MAC=%s\n", ipAddr, macAddr)

    // 主循环
    for {
        if shouldSkipLogin(&Config{
            StudentMode: studentMode,
        }) {
            time.Sleep(1 * time.Second)
            continue
        }

        if !isNetworkOK() {
            fmt.Println("⚠️ 检测到断网,正在重新登录...")
            login(&Config{
                User:     user,
                Password: passwd,
                NetType:  nettype,
            }, ipAddr, macAddr)
        }

        time.Sleep(1 * time.Second)
    }
}

代码详细解释

下面我将详细解释代码中每个函数的作用,帮助你理解这个校园网自动重连程序的工作原理。这些函数都是程序的核心逻辑,我将按照从配置加载到网络检测再到认证的流程进行说明。

1. loadConfig()

func loadConfig() (*Config, error) {
    // 检查文件是否存在
    if _, err := os.Stat(configFileName); os.IsNotExist(err) {
        // 创建默认模板
        defaultContent := `# 校园网登录脚本信息设置:(注意请不要改变格式)
# 用户名:(填写示例:User=1807210721)
User=
# 密码:(填写示例:Password=www.nekopara.uk)
Password=
# 运营商选择,留空选择校园网,如果需要选择运营商,电信填写telecom,联通填写unicom,移动填写cmcc
Net_Type=
# 是否开启学生上网时段模式?1为开启,0为关闭,开启后周一到周五0:00-6:00将不会尝试重连
Student_Mode=0
# 开启路由器登录模式:
# 如果填写以下两个参数(均非空),则使用指定的路由器IP和MAC进行认证。
# 否则使用本机IP和MAC。
# 示例:
# Router_IP=172.16.6.6
# Router_MAC=36:88:8A:99:A4:CC
Router_IP=
Router_MAC=
`

        err = os.WriteFile(configFileName, []byte(defaultContent), 0644)
        if err != nil {
            return nil, fmt.Errorf("无法创建配置文件: %v", err)
        }
        return nil, fmt.Errorf("未找到配置文件,配置文件 '%s' 已创建,请先填写上网信息后重新运行程序", configFileName)
    }

    // 读取并解析
    content, err := os.ReadFile(configFileName)
    if err != nil {
        return nil, fmt.Errorf("无法读取配置文件: %v", err)
    }

    cfg := &Config{}
    scanner := bufio.NewScanner(strings.NewReader(string(content)))
    for scanner.Scan() {
        line := strings.TrimSpace(scanner.Text())

        // 跳过空行和注释
        if line == "" || strings.HasPrefix(line, "#") {
            continue
        }

        // 按第一个 '=' 分割(避免密码含等号出错)
        parts := strings.SplitN(line, "=", 2)
        if len(parts) != 2 {
            continue // 格式错误,跳过
        }

        key := strings.TrimSpace(parts[0])
        value := strings.TrimSpace(parts[1])

        switch key {
            case "User":
                cfg.User = value
            case "Password":
                cfg.Password = value
            case "Net_Type":
                cfg.NetType = value // 新增这一行
            case "Student_Mode":
                cfg.StudentMode = (value == "1")
            case "Router_IP":
                cfg.RouterIP = value
            case "Router_MAC":
                cfg.RouterMAC = value
        }
    }

    if err := scanner.Err(); err != nil {
        return nil, err
    }

    // 基础校验
    if cfg.User == "" || cfg.Password == "" {
        return nil, fmt.Errorf("请在 '%s' 中填写用户名和密码", configFileName)
    }
    // 在 loadConfig 函数中,解析配置后添加:
    if cfg.NetType != "" {
        // 检查是否是合法的运营商
        valid := false
        switch strings.ToLower(cfg.NetType) {
            case "telecom", "unicom", "cmcc":
                valid = true
        }

        if !valid {
            return nil, fmt.Errorf("错误:运营商类型必须为空、telecom、unicom或cmcc(不区分大小写),当前值: %s", cfg.NetType)
        }
    }

    return cfg, nil
}

函数解释loadConfig() 是程序的配置加载核心函数。它的作用是检查是否存在配置文件,如果不存在则创建一个默认配置文件模板,让用户填写必要信息;如果存在则解析配置文件内容并返回配置结构体。
关键点:

  • 首次运行时会创建config.txt文件,包含详细的填写说明
  • 使用bufio.Scanner逐行解析配置文件,跳过注释和空行
  • 对配置项进行基础校验,确保用户名和密码必填
  • 对运营商类型进行合法性检查(必须是telecomunicomcmcc
  • 如果配置不合法,会返回错误信息提示用户

这个函数是程序与用户交互的第一步,也是保证后续程序能正确运行的基础。

2. getLocalIP()

func getLocalIP() (string, error) {
    conn, err := net.Dial("udp", "8.8.8.8:80")
    if err != nil {
        return "", err
    }
    defer conn.Close()
    return conn.LocalAddr().(*net.UDPAddr).IP.String(), nil
}

函数解释getLocalIP() 用于获取当前设备的本地IP地址。这个函数通过与公共DNS服务器(8.8.8.8)建立UDP连接,然后获取连接的本地地址。
关键点:

  • 使用net.Dial建立UDP连接,连接到Google DNS服务器(8.8.8.8:80)
  • 通过conn.LocalAddr()获取连接的本地IP地址
  • 这是获取本机IP的常用方法,避免了遍历所有网络接口的复杂性
  • 返回的IP地址是字符串格式,例如"192.168.1.100"

这个函数在获取本机IP时非常实用,避免了手动遍历网络接口的麻烦。

3. getMACAddress()

func getMACAddress() (string, error) {
    interfaces, err := net.Interfaces()
    if err != nil {
        return "", err
    }

    for _, iface := range interfaces {
        if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 {
            continue
        }

        mac := iface.HardwareAddr.String()
        if mac == "" {
            continue
        }

        addrs, _ := iface.Addrs()
        for _, addr := range addrs {
            if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil {
                return mac, nil
            }
        }
    }
    return "", fmt.Errorf("no active network interface with MAC found")
}

函数解释getMACAddress() 用于获取当前设备的MAC地址。它会遍历所有网络接口,找到第一个非回环、启用的接口,并返回其MAC地址。
关键点:

  • 使用net.Interfaces()获取所有网络接口信息
  • 过滤掉回环接口(FlagLoopback)和未启用的接口(FlagUp
  • 从接口的硬件地址(HardwareAddr)获取MAC地址
  • 遍历接口的IP地址,确保返回的是有效网络接口的MAC
  • 返回的MAC格式如"36:88:8A:99:A4:CC"

这个函数在获取本机MAC地址时非常关键,因为校园网认证需要MAC地址来保持登录状态。

4. isNetworkOK()

func isNetworkOK() bool {
    client := &http.Client{
        Timeout: 1 * time.Second,
    }
    resp, err := client.Get("http://connect.rom.miui.com/generate_204")
    if err != nil {
        return false // 网络不通 / DNS 故障 / 超时
    }
    defer resp.Body.Close()

    return resp.StatusCode == 204
}

函数解释isNetworkOK() 用于检测当前网络连接状态。它通过HTTP请求访问一个特殊URL(http://connect.rom.miui.com/generate_204),根据响应状态码判断网络是否通畅。
关键点:

  • 使用http.Client发送GET请求,设置1秒超时
  • 请求的URL是MIUI系统用于检测网络连接的特殊地址
  • 如果响应状态码为204,表示网络通畅;否则表示断网
  • 这个检测方法非常轻量,不会产生太多网络流量
  • 通过defer resp.Body.Close()确保资源正确释放

这个函数是程序检测断网的核心,决定了是否需要触发重连逻辑。

5. login()

func login(cfg *Config, ip, mac string) {
    // 格式化 MAC:去掉冒号,转小写(适配你 bash 脚本的行为)
    cleanMAC := strings.ReplaceAll(strings.ToLower(mac), ":", "")

    userAccount := cfg.User
    if cfg.NetType != "" {
        userAccount = cfg.User + "@" + cfg.NetType
    }

    params := url.Values{
        "callback":       {"dr1003"},
        "login_method":   {"1"},
        "user_account":   {userAccount},
        "user_password":  {cfg.Password},
        "wlan_user_ip":   {ip},
        "wlan_user_mac":  {cleanMAC},
        "wlan_user_ipv6": {""},
        "wlan_ac_ip":     {""},
        "wlan_ac_name":   {""},
        "jsVersion":      {"4.2.1"},
        "terminal_type":  {"1"},
        "lang":           {"zh-cn"},
        "v":              {"5574"},
    }

    loginURL := "http://172.17.0.2:801/eportal/portal/login?" + params.Encode()

    resp, err := http.Get(loginURL)
    if err != nil {
        fmt.Printf("❌ 登录请求失败: %v\n", err)
        return
    }
    defer resp.Body.Close()

    // 读取并打印响应体
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("❌ 读取响应体失败: %v\n", err)
        return
    }
    bodyStr := string(body)

    fmt.Printf("✅ 已发送登录请求(HTTP状态码: %d)\n", resp.StatusCode)
    fmt.Printf("响应内容: %s\n", bodyStr)
}

函数解释login() 是校园网认证的核心函数,负责构造认证请求并发送给认证服务器。
关键点:

  • 对MAC地址进行格式化:去掉冒号,转为小写
  • 根据运营商类型(NetType)构造用户名(例如1807210721@cmcc
  • 使用url.Values构建查询参数,确保参数编码正确
  • 拼接完整的认证URL(http://172.17.0.2:801/...
  • 发送HTTP GET请求到认证服务器
  • 打印请求结果和响应内容,便于调试
  • 通过params.Encode()正确编码URL参数

这个函数实现了校园网认证的关键逻辑,是整个程序的核心功能。

6. shouldSkipLogin()

func shouldSkipLogin(cfg *Config) bool {
    if !cfg.StudentMode {
        return false
    }

    now := time.Now()
    weekday := now.Weekday() // Sunday = 0, Monday = 1, ..., Friday = 5
    hour := now.Hour()

    // 周一到周五(1~5),且 0:00 ~ 5:59
    if weekday >= time.Monday && weekday <= time.Friday && hour >= 0 && hour < 6 {
        fmt.Println("🌙 学生模式:当前为禁网时段,暂停重连")
        return true
    }

    return false
}

函数解释shouldSkipLogin() 用于判断在特定时段是否应该跳过登录尝试。这个函数实现了"学生模式",即在周一到周五的0:00-6:00期间不尝试重连。
关键点:

  • 仅在StudentModetrue时生效
  • 获取当前时间的星期几和小时
  • 判断是否为周一至周五的凌晨时段(0:00-5:59)
  • 如果是禁网时段,打印提示并返回true,表示应该跳过登录
  • 否则返回false,表示可以尝试登录

这个函数是为了解决学生账号在凌晨时段断网后反复尝试自动重连的问题,避免持续不断发起无意义的请求。

7. getLoginInfo()

func getLoginInfo(cfg *Config) (ip, mac string, err error) {
    // 如果启用了路由器模式(两个字段都非空)
    if cfg.RouterIP != "" && cfg.RouterMAC != "" {
        fmt.Println("🌐 使用路由器模式进行认证")
        return cfg.RouterIP, cfg.RouterMAC, nil
    }

    // 否则使用本机信息
    fmt.Println("💻 使用本机模式进行认证")
    ip, err = getLocalIP()
    if err != nil {
        return "", "", fmt.Errorf("获取本机IP失败: %w", err)
    }
    mac, err = getMACAddress()
    if err != nil {
        return "", "", fmt.Errorf("获取本机MAC失败: %w", err)
    }
    return ip, mac, nil
}

函数解释getLoginInfo() 用于确定认证时使用的IP和MAC地址。它会根据配置决定是使用路由器的IP/MAC还是本机的IP/MAC。
关键点:

  • 如果配置了Router_IPRouter_MAC,则使用路由器的IP和MAC
  • 否则,使用本机的IP和MAC(通过调用getLocalIP()getMACAddress()
  • 返回的IP和MAC格式符合校园网认证要求
  • 在使用路由器模式时,会打印提示信息

这个函数是程序灵活应对不同网络环境和部署需求的关键,既适配在教室本机连接校园网时的认证,又适配在宿舍使用路由器共享上网的场景。

8. printHelp()

func printHelp() {
    fmt.Println(`广西大学校园网自动登录程序参数说明:
必须参数:
-user      用户名(必须提供)
-passwd    密码(必须提供)

可选参数:
-nettype   运营商类型(telecom, unicom, cmcc),不加参数则使用校园网
-studentmode  启用学生模式(不带值)
-ip        路由器IP(必须与-mac一起使用)
-mac       路由器MAC(必须与-ip一起使用)
-help      显示此帮助信息

示例(Linux):
./GXU_Net_AutoLogin -user 1807210721 -passwd mypassword
/opt/GXU_Net_AutoLogin/GXU_Net_AutoLogin -user 1807210721 -passwd mypassword -nettype telecom -studentmode
./GXU_Net_AutoLogin -user 1807210721 -passwd mypassword -ip 172.16.6.6 -mac 36:88:8A:99:A4:CC

示例(Windows):
GXU_Net_AutoLogin.exe -user 1807210721 -passwd mypassword
C:\\Program Files\\GXU_Net_AutoLogin\\GXU_Net_AutoLogin.exe -user 1807210721 -passwd mypassword -nettype telecom -studentmode
C:\\Program Files\\GXU_Net_AutoLogin\\GXU_Net_AutoLogin.exe -user 1807210721 -passwd mypassword -ip 172.16.6.6 -mac 36:88:8A:99:A4:CC
`)
}

函数解释printHelp() 用于打印程序的使用帮助信息,包括参数说明和使用示例。
关键点:

  • 提供了详细的参数说明,包括必须参数和可选参数
  • 列举了Linux和Windows平台的使用示例
  • 清晰地说明了各参数的作用
  • 便于用户快速了解如何使用程序

这个函数在用户输入-help参数时调用,是程序的文档部分,帮助用户正确使用程序。

9. main()

func main() {
    fmt.Printf("🚀广西大学校园网自动登录程序 By:GTX690战术核显卡导弹(www.nekopara.uk)\n")
    // 定义命令行参数
    var (
        user        string
        pass        string
        nettype     string
        studentMode bool
        ip          string
        mac         string
        help        bool
    )

    flag.StringVar(&user, "user", "", "用户名")
    flag.StringVar(&pass, "passwd", "", "密码")
    flag.StringVar(&nettype, "nettype", "", "运营商类型(telecom, unicom, cmcc)")
    flag.BoolVar(&studentMode, "studentmode", false, "启用学生模式")
    flag.StringVar(&ip, "ip", "", "路由器IP(必须与-mac一起使用)")
    flag.StringVar(&mac, "mac", "", "路由器MAC(必须与-ip一起使用)")
    flag.BoolVar(&help, "help", false, "显示帮助信息")
    flag.Parse()

    // 显示帮助信息
    if help {
        printHelp()
        os.Exit(0)
    }

    // 检查必须参数
    if (user == "" && pass == "") {
        // 从配置文件加载
        cfg, err := loadConfig()
        if err != nil {
            fmt.Println("❌ 错误:", err)
            fmt.Println("💡 请编辑 config.txt 后重新运行本程序。")
            os.Exit(1)
        }
        fmt.Printf("✅ 配置加载成功!\n")
        fmt.Printf("用户: %s\n", cfg.User)
        fmt.Printf("密码: %s\n", cfg.Password)
        fmt.Printf("运营商: %s\n", cfg.NetType)
        fmt.Printf("学生模式: %t\n", cfg.StudentMode)
        if cfg.RouterIP != "" && cfg.RouterMAC != "" {
            fmt.Printf("路由器模式: IP=%s, MAC=%s\n", cfg.RouterIP, cfg.RouterMAC)
        }
        // 使用配置文件中的配置
        cfg.User = cfg.User
        cfg.Password = cfg.Password
        cfg.NetType = nettype // 优先使用命令行参数,如果命令行没提供则保持配置文件中的值
        cfg.StudentMode = studentMode
        cfg.RouterIP = ip
        cfg.RouterMAC = mac
    } else if user != "" && pass != "" {
        // 从命令行参数加载
        cfg := &Config{
            User:        user,
            Password:    pass,
            NetType:     nettype,
            StudentMode: studentMode,
            RouterIP:    ip,
            RouterMAC:   mac,
        }

        // 校验运营商类型
        if nettype != "" {
            valid := false
            switch strings.ToLower(nettype) {
                case "telecom", "unicom", "cmcc":
                    valid = true
            }
            if !valid {
                fmt.Printf("❌ 错误:运营商类型必须为telecom, unicom, cmcc(不区分大小写),当前值: %s\n", nettype)
                os.Exit(1)
            }
        }

        // 校验路由器IP/MAC
        if (ip != "" && mac == "") || (ip == "" && mac != "") {
            fmt.Println("❌ 错误:必须同时提供ip和mac参数,两者缺一不可")
            os.Exit(1)
        }

        // 显示配置
        fmt.Printf("✅ 命令行参数加载成功!\n")
        fmt.Printf("用户: %s\n", cfg.User)
        fmt.Printf("密码: %s\n", cfg.Password)
        fmt.Printf("运营商: %s\n", cfg.NetType)
        fmt.Printf("学生模式: %t\n", cfg.StudentMode)
        if cfg.RouterIP != "" && cfg.RouterMAC != "" {
            fmt.Printf("路由器模式: IP=%s, MAC=%s\n", cfg.RouterIP, cfg.RouterMAC)
        }

        // 使用命令行参数配置
        cfg.User = user
        cfg.Password = pass
        cfg.NetType = nettype
        cfg.StudentMode = studentMode
        cfg.RouterIP = ip
        cfg.RouterMAC = mac
    } else {
        // 只提供了其中一个参数
        fmt.Println("❌ 错误:必须同时提供user和passwd参数,或者都不提供(通过配置文件)")
        fmt.Println("💡 请使用 -help 查看参数说明")
        os.Exit(1)
    }

    // 获取用于登录的 IP 和 MAC(自动判断模式)
    ipAddr, macAddr, err := getLoginInfo(&Config{
        RouterIP:  ip,
        RouterMAC: mac,
    })
    if err != nil {
        fmt.Printf("❌ %v\n", err)
        os.Exit(1)
    }

    fmt.Printf("✅ 守护进程启动:认证IP=%s | 认证MAC=%s\n", ipAddr, macAddr)

    // 主循环
    for {
        if shouldSkipLogin(&Config{
            StudentMode: studentMode,
        }) {
            time.Sleep(1 * time.Second)
            continue
        }

        if !isNetworkOK() {
            fmt.Println("⚠️ 检测到断网,正在重新登录...")
            login(&Config{
                User:     user,
                Password: pass,
                NetType:  nettype,
            }, ipAddr, macAddr)
        }

        time.Sleep(1 * time.Second)
    }
}

函数解释main() 是程序的主入口函数,负责处理命令行参数、加载配置、启动守护进程并持续监控网络状态。
关键点:

  • 使用flag包解析命令行参数
  • 支持两种配置方式:通过配置文件或命令行参数
  • 对配置进行校验,确保必要参数存在且格式正确
  • 根据配置决定使用路由器模式还是本机模式
  • 启动主循环,每秒检查网络状态
  • 当检测到断网时,调用login()函数进行重连
  • 在学生模式下,避免在特定时段重连

这个函数是整个程序的"大脑",协调所有其他函数的工作,实现断网自动重连的核心功能。

使用方法

你既可以从上面的源代码编译使用,也可以下载我编译好的程序。
如果不放心我分发的二进制文件,要自己编译来用的话,把上面那段完整的代码保存为main.go,安装好Go语言环境,然后进行编译:

go build -ldflags="-s -w" -o GXU_Net_AutoLogin main.go 

或者直接运行也行:

go run main.go

二进制文件的话,我就直接只编译了win32的程序:GXU_Net_AutoLogin.7z
对于Linux的话,我相信使用Linux的你应该知道怎么配置Go环境,源代码放在这了,你就自己编译去吧(其实真不难)。
或许可以用wine运行?(bushi
相关源代码已经放到Github仓库:github.com/Chocola-X/GXU-Net-AutoLogin