使用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
根据我目前有限的理解和认识,要实现这个功能,我可能需要捕获所有的请求,例如使用 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)
}
}
使用说明
-
配置PC热点:
- 设置热点IP为固定地址(如192.168.137.1)
- 在热点设置中指定自定义DNS服务器地址为PC的IP
-
设备连接流程:
- 设备连接Wi-Fi热点
- DNS查询被重定向到你的服务器
- 设备尝试访问任意HTTP网站时被重定向到登录页
- 用户点击连接后获得完整访问权限
-
检测端点:
/generate_204- Android设备检测/hotspot-detect.html- Apple设备检测- 这些端点确保设备能正确识别门户
注意事项
- 这种方法不需要修改iptables,但需要配置热点的DNS设置
- 确保防火墙允许80端口和53端口的访问
- 对于生产环境,建议添加更完善的认证和会话管理
这个实现通过应用层控制模拟了Captive Portal的基本行为,同时保持了你的现有HTTP/WebSocket服务不变。

