随着wifi的普及,移动运营商的热点也越来越多了,如中国移动的CMCC、中国电信的ChinaNet、中国联通的ChinaUnicom等,一般来说,连上此类的热点,打开浏览器上网时都会自动跳转到一个验证页面,最近有个项目也有类似的需求,Android手机自建热点,别的手机wifi连接此热点,打开浏览器,输入任意内容,自动跳转到一个下载列表页面,点击相应的链接即可下载相应的文件。
考虑如下几种情况:
因此,需要解决如下问题:
Android系统本身是Linux内核,1024以下端口都名花有主,如http是80,https是443,dns是53,对于这些1024以下端口的绑定需要root权限,但一般的App是没有root权限的,除非在 AndroidManifest.xml 文件中声明 android:sharedUserId="android.uid.system",并使用密钥文件进行签名:
java -jar signapk.jar platform.x509.pem platform.pk8 your.apk your_signed.apk
但问题是密钥文件属于手机厂商,显然不可能拿到这个密钥文件,当然,如果在模拟器里测试倒是可以的,从android源代码 build/target/product/security 里找到密钥文件,platform.pk8 和 platform.x509.pem,签名工具 signapk.jar 在 build/tools/signapk 下。
基于以上原因,一般Web服务器都绑定8080端口,手机浏览器如果输入IP地址,会访问Web服务器的80端口,这样就需要进行端口报文转发,对应dns报文拦截,无法监听53端口,同样需要端口转发,此外,浏览器的搜索引擎如果是google的话,使用https,同样也有这个问题。
iptables是个很好的防火墙管理工具,这里需要做如下配置:
iptables -t nat -A PREROUTING -d 0.0.0.0/0 -p tcp --dport 80 -j DNAT --to 192.168.43.1:8080
iptables -t nat -A PREROUTING -d 0.0.0.0/0 -p tcp --dport 443 -j DNAT --to 192.168.43.1:8443
iptables -t nat -A PREROUTING -d 0.0.0.0/0 -p udp --dport 53 -j DNAT --to 192.168.43.1:53530
说明:-t nat:指定nat表,-A:添加,PREROUTING:路由前处理,-d 0.0.0.0/0:任意目的地IP,-p tcp:协议,--dport 80:端口,-j DNAT:地址映射跳转,--to 192.168.43.1:8080:转发目的地,总的意思就是,到达防火墙的报文,不管去往那个IP地址,只要是发往80端口的tcp包,都转发到192.168.43.1的8080端口。剩下两条意思类似。
需要注意的是:
1、如果是App中的java代码调用,需要root权限,一般这么写:
String shell = "su -c iptables -t nat -A PREROUTING -d 0.0.0.0/0 -p tcp --dport 80 -j DNAT --to 192.168.43.1:8080";
Runtime.getRuntime().exec(shell);
2、Android手机设置为热点模式时,IP地址一般都会固定成192.168.43.1,这是由手机的dhcpcd服务指定的,一般不会去改dhcpcd服务的源代码然后重新编译,但这种写死的做法显然是不太合适的,通用的做法是自动取Ap的IP地址:
public static String getNetworkIpAddress(String name) {
try {
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
NetworkInterface networkInterface = (NetworkInterface) interfaces.nextElement();
Enumeration<InetAddress> enumeration = networkInterface.getInetAddresses();
while (enumeration.hasMoreElements()) {
InetAddress inetAddress = (InetAddress) enumeration.nextElement();
if (!inetAddress.isLoopbackAddress()
&& inetAddress instanceof Inet4Address
&& TextUtils.equals(name, networkInterface.getDisplayName())){
return inetAddress.getHostAddress().toString();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
public static String getApName(Context context) {
try {
ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
Method method = connectivityManager.getClass().getMethod("getTetheredIfaces");
String[] names = (String[]) method.invoke(connectivityManager);
return names[0];
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
看着挺复杂的,因为热点模式和连接到别的热点是完全不同的,取Ap名字时,用到了一个隐藏的方法,需要用反射的方式调用。
DNS意思是域名解析协议,用户打开浏览器浏览网页时,不会记IP地址,而是记某些有含义的网址,DNS就是解决网址到IP地址的对应问题。DNS报文格式参考RFC1035文档,微软的网站上也有介绍:http://technet.microsoft.com/en-us/library/dd197470(v=ws.10).aspx ,这里主要介绍DNS报文的格式。
DNS报文一般由如下部分组成:
套用《TCPIP详解卷》中的一张图
总共占12字节,结构如下:
-- 标识:报文的标识,占2字节,查询的报文里生成,响应的报文里复制此内容,用来标识是对相应查询的响应
-- 标记:报文的标记位,占2字节,也就是16位,如下:
--问题资源数:占2字节
--回答资源数:占2字节
--授权资源数:占2字节
--附加资源数:占2字节
不定长,结构如下:
不定长,结构如下:
同上。
同上。
经过以上的分析,来看个例子,打开wireshark抓包工具,监听网卡数据包,打开控制台,输入:host baidu.com,并抓取DNS报文。
上图为DNS查询:
d50100000100000000000005626169647503636f6d0000010001
上图为DNS应答:
d58180000100030000000005626169647503636f6d0000010001c00c000100010000017c00047b7d7290c00c000100010000017c0004dcb56f55c00c000100010000017c0004dcb56f56
从上面的DNS应答报文看,关注7b7d7290、dcb56f55、dcb56f56即可,分别对应三个IP地址:123.125.114.144、220.181.111.85、220.181.111.86
了解了DNS报文的内容,下面需要做的就是,监听DNS端口,构造自己的报文返回即可,由于权限问题,一般Android的App是无法监听53端口的,这里可以监听53530端口,再通过iptables设置防火墙,将53端口的报文转发到53530端口即可,注意DNS是UDP包,代码参考如下
byte[] requestBuffer = new byte[256];
byte[] responseBuffer = new byte[256];
byte[] ipBuffer = { (byte) 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00,
x00, 0x01, 0x7c, 0x00, 0x04, 0x01, 0x01, 0x01, 0x01 };
try {
datagramSocket = new DatagramSocket(53530);
DatagramPacket requestPacket = new DatagramPacket(requestBuffer,requestBuffer.length);
while (!Thread.currentThread().isInterrupted()) {
datagramSocket.receive(requestPacket);
int requestLength = requestPacket.getLength();
System.arraycopy(requestBuffer, 0, responseBuffer, 0, requestLength);
System.arraycopy(ipBuffer, 0, responseBuffer, requestLength, ipBuffer.length);
// 标志位
responseBuffer[2] = (byte) 0x81;
responseBuffer[3] = (byte) 0x80;
// 响应数
responseBuffer[6] = (byte) 0x00;
responseBuffer[7] = (byte) 0x01;
DatagramPacket response = new DatagramPacket(responseBuffer, requestLength + ipBuffer.length, requestPacket.getAddress(), requestPacket.getPort());
datagramSocket.send(response);
}
} catch (Exception e) {
e.printStackTrace();
}
这样,所有的DNS解析请求都被转到1.1.1.1这个IP地址了。
这个一般由Web服务器决定,Android有款ijetty,是开源的 http://code.google.com/p/i-jetty/, 可看看其中的源代码,修改其中的Handler就搞定了。
转自:http://ju.outofmemory.cn/entry/94780