简单实现小米共享WiFi,利用空闲网络获取收益

1,642 阅读5分钟

背景

打开手机连接WiFi的时候,发现有个叫小米共享WiFi无密码登录,试着连接一下,发现他会跳转一个验证页面,验证页面有【点击如下内容,开始3分钟网络体验】和【红包上网】,一个是看广告获取上网时长,一个是花钱上网。 还可以这样利用空闲网络获取收益,打算简单实现一下。

目标

尝试实现一个路由WiFi无密码连接,连接后跳转登录网页,登录后才可以上网,10分钟后需要重新登录。先把基本流程打通,加入广告需要第三接口接入。

环境

  • Redmi路由器AC2100(OpenWrt 21.02.7)
  • 红米手机

Captive Portal机制

sequenceDiagram
    participant Android设备
    participant 网络连接
    participant Captive Portal检测服务器

    Android设备->>网络连接: 尝试连接网络
    网络连接-->>Android设备: 返回网络连接成功
    
    Android设备->>Captive Portal检测服务器: 发送HTTP请求(通常为固定URL[generate_204])
    Captive Portal检测服务器-->>Android设备: 返回HTTP状态码/响应
    
    alt 响应为204(No Content)
        Android设备->>Android设备: 认为网络正常
    else 响应为301(或其他3xx状态码)
        Android设备->>Android设备: 认为存在Captive Portal
        Android设备->>用户: 提示用户登录Captive Portal
    else 响应为非3xx且非204
        Android设备->>Android设备: 认为网络异常
    end

    用户->>Captive Portal: 提供认证信息
    Captive Portal-->>用户: 验证通过,开放网络访问
    Android设备->>Captive Portal检测服务器: 再次发送HTTP请求
    Captive Portal检测服务器-->>Android设备: 返回204(No Content)
    Android设备->>Android设备: 认为网络正常

实现

了解一下Captive Portal机制,我们需要拦截验证请求(带有/generate_204),如果登录了就返回204,没有登录就返回302并且携带跳转页面,等待用户的验证。

如何找到这个验证请求呢?可以通过Fiddler抓手机的数据包得到。

我这里拿到http://connect.rom.miui.com/generate_204,因为我的是红米手机。

如何拦截到generate_204请求,路由器是有dns服务的,可以在dns服务上添加域名解析到我的web应用上,修改/etc/config/dhcp,末尾添加一行list address '/connect.rom.miui.com/192.168.1.195',如下

config dnsmasq
        option domainneeded '1'
        option boguspriv '1'
        option filterwin2k '0'
        option localise_queries '1'
        option rebind_protection '1'
        option rebind_localhost '1'
        option local '/lan/'
        option domain 'lan'
        option expandhosts '1'
        option nonegcache '0'
        option authoritative '1'
        option readethers '1'
        option leasefile '/tmp/dhcp.leases'
        option resolvfile '/tmp/resolv.conf.d/resolv.conf.auto'
        option nonwildcard '1'
        option localservice '1'
        option ednspacket_max '1232'
        list address '/connect.rom.miui.com/192.168.1.195'

192.168.1.195是我的PC的ip连接路由器的lan1,登录的web服务我这里使用nodejs,下面是后台代码,页面代码在完整项目里

const express = require('express');
const bodyParser = require('body-parser');
const session = require('express-session');  
const path = require('path');
const fs = require('fs').;

const app = express();
//存ip
const map = new Map();

// 使用body-parser中间件来解析JSON数据
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));


// 提供静态文件
app.use(express.static(path.join(__dirname, 'public')));

function isLogin(ip){
	if(map.get(ip)){
		const data = map.get(ip);
		console.log("data",data)
		//不超过10分钟
		if(new Date().getTime() - data.time< 10 * 60 * 1000){
			return true;
		}
	}
	return false
}

// 登录路由
app.post('/login', (req, res) => {
	const reqIp = req.ip;
    const { username, password } = req.body;

    // 假设这是一个简单的用户名密码验证
    if (username === 'admin' && password === 'admin') {
        // 登录成功,存储登录信息到map
        map.set(reqIp,{username:username,time:new Date().getTime()})
		
        res.json({ success: true });
    } else {
        res.json({ success: false, message: '用户名或密码错误' });
    }
});

// 主页路由(登录成功后跳转)
app.get('/', async (req, res) => {
	const reqIp = req.ip;
	
    if (!isLogin(reqIp)) {
        // 如果没有登录,重定向到登录页
		res.writeHead(302, { 'Location': '/login' });
		res.end();
		return
    }
	
	try {
        // 读取文件内容
        const filePath = path.join(__dirname, 'public', 'auth.html');
        const fileContent = await fs.readFile(filePath, 'utf8');

        // 替换内容
        const username = map.get(reqIp).username;
        const modifiedContent = fileContent.replace(/{{username}}/g, username);

        // 发送修改后的内容
        res.send(modifiedContent);
    } catch (err) {
        console.error('读取文件时发生错误:', err);
        res.status(500).send('内部服务器错误');
    }
	
 
});

app.get('/generate_204', (req, res) => {
	const reqIp = req.ip;
	console.log(reqIp)
	
    if (!isLogin(reqIp)) {
        // 如果没有登录,重定向到登录页
		res.writeHead(302, { 'Location': '/login' });
		res.end();
		return
    }
    res.status(204).end();
});
//登录
app.get('/login', (req, res) => {
    res.sendFile(path.join(__dirname, 'public', 'login.html'));
});
//注销
app.get('/logout', (req, res) => {
    const reqIp = req.ip;
	map.set(reqIp,null);
    res.json({ success: true });
});

// 启动服务器
const port = 80;
app.listen(port, () => {
    console.log(`Server running on http://localhost:${port}`);
});

发现域名是connect.rom.miui.com的,解析到我的web服务ip上,请求就可以走到web服务,web服务再判断是否登录,有登录就返回响应码204,没有登录就返回301和登录的url,登录的url也是指向web服务,登录成功后。下次的generate_204请求就会返回204,下面大致流程

sequenceDiagram
    participant Android设备
    participant DNS服务
    participant Web服务
    participant 登录页面

    Android设备->>DNS服务: 请求域名解析
    DNS服务-->>Android设备: 解析 `generate_204` 到 Web 服务IP
    
    Android设备->>Web服务: 发送 `generate_204` 请求
    Web服务->>Web服务: 判断用户是否已登录
    
    alt 已登录
        Web服务-->>Android设备: 返回 HTTP 204(No Content)
        Android设备->>Android设备: 网络连接正常
    else 未登录
        Web服务-->>Android设备: 返回 HTTP 301 重定向到登录页面
        Android设备->>登录页面: 打开登录页面
        用户->>登录页面: 提交登录信息
        登录页面-->>Web服务: 验证用户登录
        Web服务-->>Android设备: 登录成功
        Android设备->>Web服务: 再次发送 `generate_204` 请求
        Web服务-->>Android设备: 返回 HTTP 204(No Content)
        Android设备->>Android设备: 网络连接正常
    end

上面只是把表面的效果实现了,实际上手机上的应用还是可以上网的,因为我们只是针对connect.rom.miui.com域名,我们还得通过iptables拦截网络访问。

我的wifi使用路由器的wlan0口,桥接到br-lan,如下可以查到两个wifi,三个有线,我需要拦截从wlan0口的数据包不允许通过。

root@OpenWrt:~# brctl show
bridge name	bridge id		STP enabled	interfaces
br-lan		7fff.28d1271c4329	no		lan2
							wlan0
							lan3
							wlan1
							lan1

然后通过iptables是拦截不到wlan1br-lan数据包的,怎么办呢?

可以通过安装ebtables,在ebtables中标记wlan0流量,iptables也是可以拿到这个标记的,然后限制通过。

# 安装ebtalbes
opkg update
opkg install ebtales
# 标记流量
ebtables -A INPUT -i wlan0 -j mark --set-mark 1
# 拒绝标记流量
iptables -I FORWARD -m mark --mark 1 -j DROP
# 允许80端口标记流量 主要给验证请求通过(generate_204)
iptables -I FORWARD -p tcp --dport 80 -m mark --mark 1 -j ACCEPT

下面可以查看添加的规则:

ebtables -L
iptables  -L FORWARD -v -n

上面已经将wifi来的数据包都拦截,现在添加动态允许通过规则,我们可以通过tcpdump抓包去generate_204的响应,如果是204添加iptables允许通过规则,否则就删除iptables允许通过规则,openwrt下可以通过lua脚本允许

安装lua

opkg update
opkg install lua

创建rule.lua,内容如下:

-- 动态修改 iptables 规则
function add_rule(ip)
    -- 检查规则是否已经存在
    local check_command = string.format("iptables -C FORWARD -p tcp -s %s -j ACCEPT", ip)
    local exists = os.execute(check_command) == 0
    
    if not exists then
        -- 如果规则不存在,则添加规则
        local command = string.format("iptables -I FORWARD -p tcp -s %s -j ACCEPT", ip)
        os.execute(command)
        print("Added rule for IP: " .. ip)
    else
        print("Rule for IP: " .. ip .. " already exists.")
    end
end

-- 删除 iptables 规则
function delete_rule(ip)
    -- 检查规则是否已经存在
    local check_command = string.format("iptables -C FORWARD -p tcp -s %s -j ACCEPT", ip)
    local exists = os.execute(check_command) == 0
    
    if exists then
        -- 如果规则存在,则删除规则
        local command = string.format("iptables -D FORWARD -p tcp -s %s -j ACCEPT", ip)
        os.execute(command)
        print("Deleted rule for IP: " .. ip)
    else
        print("Rule for IP: " .. ip .. " does not exist.")
    end
end

-- 解析 tcpdump 输出
function process_packet(packet)
    -- 提取源 IP 和响应码
    local src_ip, dest_ip, response_code = string.match(packet, "IP (%d+%.%d+%.%d+%.%d+)%.%d+ > (%d+%.%d+%.%d+%.%d+)%.%d+:.-HTTP/1.%d (%d+)")

    -- 检查是否匹配
    if dest_ip and response_code then
        if response_code == "302" then
            -- 响应码 302,删除允许通过
			delete_rule(dest_ip)
        elseif response_code == "204" then
            -- 响应码 204,允许通过规则
            add_rule(dest_ip)
        end
    end
end

-- 启动 tcpdump 命令并实时处理
function start_tcpdump()
    local command = "tcpdump -i br-lan -nn tcp src port 80 and src host 192.168.1.195 -l"
    local file = io.popen(command)  -- 打开tcpdump输出
    while true do
        local packet = file:read("*l")  -- 持续读取每一行数据包
        if packet then
            process_packet(packet)  -- 处理每个数据包
        else
            break
        end
    end
end

-- 启动 tcpdump 监听
start_tcpdump()

执行脚本

lua rule.lua

运行效果

output.gif

完整代码

login-web · 断续/route-wifi - 码云 - 开源中国