使用Golang和Sockets实现Captive Portal应用

使用Golang和Sockets实现Captive Portal应用 大家好, 我有一个现有的应用程序,它使用 http.NewServeMux() 来提供 HTTP 和 WebSocket 服务。

目前运行正常,远程设备可以通过共享的 Wi-Fi 或使用 PC 共享的热点来访问我的服务器。

我希望提供一个 Wi-Fi 门户,就像咖啡馆里的 Wi-Fi 接入点门户一样,在首次连接时自动提供一个登录页面(重定向到之前的服务器);我认为强制网络门户(captive portal)或许可以实现这一点。

我尝试使用了“GitHub - hsanjuan/go-captive: The world’s simplest captive portal”。我其实不太清楚自己在做什么 :frowning:,并且创建了以下代码,它与 HTTP 服务器分开运行。

我想知道是否可以在不重新配置主机 PC(即不更改 iptables 等)的情况下实现这一点。

预期的用途是用于数字交互,用户可以用他们的手机连接到 Wi-Fi(PC 热点),并自动打开一个页面,使他们能够与运行在 PC 上的 Web 应用程序进行交互。

谢谢 - Andy

	proxy := &captive.Portal{
		LoginPath:           "/editor",
		PortalDomain:        "quando.local",
		AllowedBypassPortal: false,
		// WebPath:             "staticContentFolder",
		// LoginHandler: func(){return true},
	}
	go func() {
		http.HandleFunc("/editor", func(w http.ResponseWriter, r *http.Request) {
			w.WriteHeader(http.StatusAccepted)
		})
	}()
	go func() {
		err := proxy.Run()
		if err != nil {
			fmt.Println(err)
			os.Exit(1)
		}
	}()

更多关于使用Golang和Sockets实现Captive Portal应用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

2 回复

根据我目前有限的理解和认识,要实现这个功能,我可能需要捕获所有的请求,例如使用 iptables。所以,目前我打算尝试其他方法……

更多关于使用Golang和Sockets实现Captive Portal应用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


要实现强制网络门户(Captive Portal)功能,通常需要网络层面的重定向配置,但如果你希望在不修改主机PC网络设置(如iptables)的情况下实现,可以考虑以下基于DNS重定向和应用层处理的方案。

核心思路

使用DNS劫持和HTTP重定向来模拟Captive Portal行为。当设备连接Wi-Fi时,通过DNS将特定域名解析到你的服务器,然后服务器检测未认证的请求并重定向到登录页面。

示例实现

package main

import (
    "context"
    "fmt"
    "log"
    "net"
    "net/http"
    "strings"
    "sync"
    "time"
)

// CaptivePortal 管理门户状态
type CaptivePortal struct {
    serverAddr     string
    dnsAddr        string
    authenticated  map[string]bool
    mu             sync.RWMutex
    loginPath      string
    portalDomain   string
}

func NewCaptivePortal(serverAddr, dnsAddr string) *CaptivePortal {
    return &CaptivePortal{
        serverAddr:    serverAddr,
        dnsAddr:       dnsAddr,
        authenticated: make(map[string]bool),
        loginPath:     "/login",
        portalDomain:  "captive.portal",
    }
}

// DNS服务器实现
func (cp *CaptivePortal) startDNSServer() error {
    pc, err := net.ListenPacket("udp", cp.dnsAddr)
    if err != nil {
        return err
    }
    defer pc.Close()

    log.Printf("DNS服务器监听在 %s", cp.dnsAddr)

    for {
        buf := make([]byte, 512)
        n, addr, err := pc.ReadFrom(buf)
        if err != nil {
            continue
        }

        go cp.handleDNSQuery(pc, addr, buf[:n])
    }
}

func (cp *CaptivePortal) handleDNSQuery(pc net.PacketConn, addr net.Addr, query []byte) {
    // 简单的DNS响应,将所有查询解析到服务器IP
    response := make([]byte, len(query)+16)
    copy(response, query)
    
    // 设置响应标志
    response[2] |= 0x80 // QR = 1
    response[3] |= 0x80 // RA = 1
    
    // 设置回答数
    response[6] = 0
    response[7] = 1
    
    // 添加回答记录
    offset := len(query)
    copy(response[offset:], query[12:])
    offset += len(query) - 12
    
    // 类型A记录
    response[offset] = 0x00
    response[offset+1] = 0x01
    offset += 2
    
    // 类IN
    response[offset] = 0x00
    response[offset+1] = 0x01
    offset += 2
    
    // TTL
    response[offset] = 0x00
    response[offset+1] = 0x00
    response[offset+2] = 0x0e
    response[offset+3] = 0x10
    offset += 4
    
    // 数据长度
    response[offset] = 0x00
    response[offset+1] = 0x04
    offset += 2
    
    // IP地址(解析到服务器)
    ip := net.ParseIP(strings.Split(cp.serverAddr, ":")[0])
    copy(response[offset:], ip.To4())
    
    pc.WriteTo(response, addr)
}

// HTTP中间件检查认证状态
func (cp *CaptivePortal) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 获取客户端IP
        clientIP := strings.Split(r.RemoteAddr, ":")[0]
        
        // 检查是否已认证
        cp.mu.RLock()
        authenticated := cp.authenticated[clientIP]
        cp.mu.RUnlock()
        
        // 允许访问登录页面和静态资源
        if r.URL.Path == cp.loginPath || 
           strings.HasPrefix(r.URL.Path, "/static/") ||
           strings.HasPrefix(r.URL.Path, "/auth/") {
            next(w, r)
            return
        }
        
        // 未认证用户重定向到登录页
        if !authenticated {
            http.Redirect(w, r, cp.loginPath, http.StatusTemporaryRedirect)
            return
        }
        
        next(w, r)
    }
}

// 处理登录
func (cp *CaptivePortal) handleLogin(w http.ResponseWriter, r *http.Request) {
    if r.Method == "POST" {
        // 验证登录凭据(这里简化处理)
        clientIP := strings.Split(r.RemoteAddr, ":")[0]
        
        cp.mu.Lock()
        cp.authenticated[clientIP] = true
        cp.mu.Unlock()
        
        // 重定向到主应用
        http.Redirect(w, r, "/", http.StatusSeeOther)
        return
    }
    
    // 显示登录页面
    fmt.Fprintf(w, `
    <!DOCTYPE html>
    <html>
    <head>
        <title>Wi-Fi Portal</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <body>
        <h2>欢迎使用Wi-Fi</h2>
        <form method="POST">
            <button type="submit">免费连接</button>
        </form>
    </body>
    </html>
    `)
}

// 处理认证检查(用于设备探测)
func (cp *CaptivePortal) handleCaptiveDetection(w http.ResponseWriter, r *http.Request) {
    // 返回成功响应,表示门户已就绪
    w.Header().Set("Content-Type", "text/html")
    fmt.Fprintf(w, `
    <!DOCTYPE html>
    <html>
    <head>
        <meta http-equiv="refresh" content="0;url=%s">
    </head>
    <body>
        <p>正在重定向到门户...</p>
    </body>
    </html>
    `, cp.loginPath)
}

// 启动HTTP服务器
func (cp *CaptivePortal) startHTTPServer() error {
    mux := http.NewServeMux()
    
    // 注册处理器
    mux.HandleFunc("/", cp.authMiddleware(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "欢迎使用应用程序!")
    }))
    
    mux.HandleFunc(cp.loginPath, cp.handleLogin)
    mux.HandleFunc("/generate_204", cp.handleCaptiveDetection)  // Android检测
    mux.HandleFunc("/hotspot-detect.html", cp.handleCaptiveDetection)  // Apple检测
    
    // 你的现有WebSocket和HTTP处理器
    mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
        // 你的WebSocket处理器
    })
    
    log.Printf("HTTP服务器监听在 %s", cp.serverAddr)
    return http.ListenAndServe(cp.serverAddr, mux)
}

// 启动门户
func (cp *CaptivePortal) Run(ctx context.Context) error {
    var wg sync.WaitGroup
    errChan := make(chan error, 2)
    
    // 启动DNS服务器
    wg.Add(1)
    go func() {
        defer wg.Done()
        if err := cp.startDNSServer(); err != nil {
            errChan <- fmt.Errorf("DNS服务器错误: %v", err)
        }
    }()
    
    // 启动HTTP服务器
    wg.Add(1)
    go func() {
        defer wg.Done()
        if err := cp.startHTTPServer(); err != nil {
            errChan <- fmt.Errorf("HTTP服务器错误: %v", err)
        }
    }()
    
    // 清理过期的认证
    go cp.cleanupExpiredAuth(ctx)
    
    select {
    case err := <-errChan:
        return err
    case <-ctx.Done():
        return ctx.Err()
    }
}

// 清理过期认证
func (cp *CaptivePortal) cleanupExpiredAuth(ctx context.Context) {
    ticker := time.NewTicker(5 * time.Minute)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            cp.mu.Lock()
            // 这里可以添加基于时间的清理逻辑
            cp.mu.Unlock()
        case <-ctx.Done():
            return
        }
    }
}

func main() {
    // 配置服务器地址
    serverAddr := "192.168.137.1:80"  // PC热点的IP和端口
    dnsAddr := ":53"                  // DNS监听端口
    
    portal := NewCaptivePortal(serverAddr, dnsAddr)
    
    ctx := context.Background()
    if err := portal.Run(ctx); err != nil {
        log.Fatal(err)
    }
}

使用说明

  1. 配置PC热点

    • 设置热点IP为固定地址(如192.168.137.1)
    • 在热点设置中指定自定义DNS服务器地址为PC的IP
  2. 设备连接流程

    • 设备连接Wi-Fi热点
    • DNS查询被重定向到你的服务器
    • 设备尝试访问任意HTTP网站时被重定向到登录页
    • 用户点击连接后获得完整访问权限
  3. 检测端点

    • /generate_204 - Android设备检测
    • /hotspot-detect.html - Apple设备检测
    • 这些端点确保设备能正确识别门户

注意事项

  • 这种方法不需要修改iptables,但需要配置热点的DNS设置
  • 确保防火墙允许80端口和53端口的访问
  • 对于生产环境,建议添加更完善的认证和会话管理

这个实现通过应用层控制模拟了Captive Portal的基本行为,同时保持了你的现有HTTP/WebSocket服务不变。

回到顶部