随着智能手机和平板的普及,现在的孩子几乎人手一部手机或平板,所以常常能看到一些孩子抱着手机玩游戏或是浏览网页,一玩就是一整天,家长们不免担心自己的孩子是不是会浏览不适合他们看的网页?是不是玩的时间太长,导致他们对其他的事情(比如运动、学习和沟通等)丧失兴趣,或者对身体发育造成不良影响。所以,有必要对孩子们用的这些移动智能设备做些上网限制,主要就是上网时间和允许浏览的网站的限制。当然,现在已经有许多应用能够实现这样的功能,但大多都是在应用层实现,要么强制使用自己定制的浏览器,要么强制使用自己的launcher,看起来都不太友好。当我们能够定制一款自己的android设备的时候,貌似有更好的解决方案。下面我们来梳理一下可能的解决方案。

一. 选择方案。

  1. 分家长端和孩子端,通常还会有个门户网站。

    这种方式一般都是在其他应用之上显示内容,即强制使用定制的launcher或浏览器。孩子端一般是android移动设备,如android手机、平板。家长端要么是另一台android设备,要么是跟孩子同一台设备,分两种不同的模式,通过密码切换。家长端也有可能直接是一个门户网站,通过登录此网站,监管孩子端的行为。代表应用有”NQ Family Guardian”、 “中国联通绿色上网管理平台”、 “绿色上网”、”kid launcher”等。

    此种方案的优点是管理全面,容易做网站过滤;缺点是操作复杂,且不友好,还有被强制删除的风险。

  2. 通过代理Proxy服务器。

    在代理服务器端进行网站过滤和上网时间限制,android端无线WLAN设置使用代理上网。具体可参考CCProxy 。

    该方案优点是网站过滤较为灵活,可以过滤网站、站点、内容,禁止下载文件,禁止用webmail收发邮件等;缺点是要维护服务器,且android对代理的支持没有那么成熟。

  3. DNS变换。

    该方式是修改android设备的域名服务器为openDNS FamilyShield的primary DNS(208.67.222.123)或alternative DNS(208.67.220.123),这两个域名服务器会帮我们过滤掉不良网站。

    优点是实现简单,缺点是服务器在国外,使用起来会严重降低网页的响应速度,且需要root设备。

  4. 修改hosts文件。

    hosts文件的作用是将常用的网址域名与其对应的IP地址建立关联。当用户在浏览器中输入一个需要登录的网址时,系统会首先自动从hosts文件中寻找对应的IP地址,一旦找到,系统会立即打开对应网页,如果没有找到,则系统会再将网址提交到DNS域名解析服务器进行IP地址的解析。当我们将要限制的网址域名与回路地址127.0.0.1对应时,那么就相当于禁止了该网站。如,我们打开android设备的/ets/hosts文件,新起一行,输入127.0.0.1 www.baidu.com,之后,我们在浏览器中输入”www.baidu.com”,网页打不开。

    优点是简单快捷,缺点是不灵活,不能限制上网时间,且需要root设备,因为普通的权限不能写hosts文件。

  5. iptables.

    iptables是Linux系统的IP信息包过滤工具,实际就是一个Linux命令,通过这个命令,可以对整个系统发出去的包,接收到的包,以及转发的包进行拦截、修改、拒绝等操作。刚好Android也是基于Linux内核的系统,也集成了iptables,是否可以用它来限制上网行为呢?经过试验,发现是可行的。

    优点:能从底层限制访问某些网站,而不必在浏览器层面阻止,不仅减少了定制或修改浏览器的成本,而且控制灵活,因为能够定制各种策略;缺点:运行iptables需要root权限。

经过综合比较,最后还是选择了用iptables实现上网控制的方案。然而我们不能假想设备一定具有root权限,所以直接在应用层调用iptables命令肯定是不行的。于是想是不是能在android系统层调用iptables命令呢,这样不就轻易解决问题了?如何在android系统层添加服务下篇文章再讨论,先看看在系统层调用iptables命令关闭网络的代码:

二. 系统层具体实现。

String cmd = new String[] {"iptables", "-P", "OUTPUT", "DROP"
};
try {process = Runtime.getRuntime().exec(cmd);
} catch (IOException e1) {e1.printStackTrace();
}

运行之后,网络并没有关闭,连上设备,进入shell,输入

iptables -L -n

发现OUTPUT链是这样的:

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
mark       all  --  0.0.0.0/0            0.0.0.0/0
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0
oem_out    all  --  0.0.0.0/0            0.0.0.0/0
fw_OUTPUT  all  --  0.0.0.0/0            0.0.0.0/0
bw_OUTPUT  all  --  0.0.0.0/0            0.0.0.0/0
st_filter_OUTPUT  all  --  0.0.0.0/0            0.0.0.0/0

说明OUTPUT链的规则并没有改变,也就是说命令没有执行或是执行了但是没效果。于是,尝试打印process的错误流,像这样:

InputStream stderr = process.getErrorStream();
...
while (true) {...if (stderr.available() > 0) {read = stderr.read(buf);if (result != null) {result.append(new String(buf,   0, read));}Slog.d(TAG, "stderr: " + result.toString());}
}
Slog.d(TAG, "exec command over");

查看log,发现了这些信息:

02-18 13:55:29.720 546-7708/system_process D/GreenNetworkService: stderr: [-] set uid permission denied: Operation not permitted
02-18 13:55:29.720 546-7708/system_process D/GreenNetworkService: exec command over

提示没有权限, 说明不能在系统service进程直接执行iptables命令,那我们就换一个思路。好在我们在开机那段时间抓到了这一段log:

V/NatController(  122): runCmd(/system/bin/iptables -F natctrl_FORWARD) res=0
V/NatController(  122): runCmd(/system/bin/iptables -A natctrl_FORWARD -j DROP) res=0
D/NvOsDebugPrintf(  127):  modeIndex =2,xres =960,yres =720
V/NatController(  122): runCmd(/system/bin/iptables -t nat -F natctrl_nat_POSTROUTING) res=0
D/NvOsDebugPrintf(  127):  modeIndex =1,xres =1280,yres =720
I/AudioPolicyManagerBase(  127): loadAudioPolicyConfig() loaded /system/etc/audio_policy.conf
V/NatController(  122): runCmd(/system/bin/ip rule flush) res=0
V/NatController(  122): runCmd(/system/bin/ip -6 rule flush) res=0
V/NatController(  122): runCmd(/system/bin/ip rule add from all lookup default prio 32767) res=0
V/NatController(  122): runCmd(/system/bin/ip rule add from all lookup main prio 32766) res=0
V/NatController(  122): runCmd(/system/bin/ip -6 rule add from all lookup default prio 32767) res=0
V/NatController(  122): runCmd(/system/bin/ip -6 rule add from all lookup main prio 32766) res=0
V/NatController(  122): runCmd(/system/bin/ip route flush cache) res=0
V/NatController(  122): runCmd(/system/bin/iptables -F natctrl_tether_counters) res=1
V/NatController(  122): runCmd(/system/bin/iptables -X natctrl_tether_counters) res=1
V/NatController(  122): runCmd(/system/bin/iptables -N natctrl_tether_counters) res=0

为什么会特意找这一段,原因就是看看系统是不是调用了iptables.果不其然,被我们发现了,就是上面这一段,只要搜索关键字”iptables”就好。从上面的log看来,有在执行iptables的命令,因为有个”runCmd”关键字,再顺着此关键字找到打印此log的类NatController.cpp. 顺着这条线索,我们发现了NatController存在于Netd进程,而Netd又是作为Android Linux Kernel与Framework之间通信的桥梁,这就不难理解为什么系统Framework层调用Kernel层的iptables命令是通过Netd进程的NatController。

什么是Netd呢?Netd是Network Daemon 的缩写,表示Network守护进程。Netd负责跟一些涉及网络的配置,操作,管理,查询等相关的功能实现,比如,例如带宽控制(Bandwidth),流量统计,带宽控制,网络地址转换(NAT),个人局域网(pan),PPP链接,soft-ap,共享上网(Tether),配置路由表,interface配置管理,等等。(摘自博客http://blog.csdn.net/xiaokeweng/article/details/8130218)

Framework部分通常会有一个对应的service与本地进程(如Netd)通信,并且提供API供应用层调用,那么Android系统中哪个service负责网络这一块呢?带着这个问题,我们轻易地找到了NetworkManagementService,在源码中位于/frameworks/base/services/java/com/android/server/NetworkManagementService.java 
查看NetworkManagementService的接口,看到了这个

@Overridepublic void setFirewallEnabled(boolean enabled) {enforceSystemUid();try {mConnector.execute("firewall", enabled ? "enable" : "disable");mFirewallEnabled = enabled;} catch (NativeDaemonConnectorException e) {throw e.rethrowAsParcelableException();}}

找到核心语句,mConnector是NativeDaemonConnector的对象 ,位于源码中的 /frameworks/base/services/java/com/android/server/NativeDaemonConnector.java,通过socket发送字符串命令给Netd,并接收Netd返回的结果。 
到此,思路就清晰了。我们可以尝试在NetworkManagementService中提供网络开关和允许上某个网站的API,应用层调用这些API,就可以实现不必root, 也可以控制上网的功能了。

回头再看刚才setFirewallEnabled那一段代码,像是开启/关闭防火墙的,我们在应用程序中调用看看。

@Overridepublic void setFirewallEnable(boolean enable) {GreenNetworkManager networkManagement =   (GreenNetworkManager)mContext.getSystemService(com.test.greennetwork.util.CommonUtil.GREEN_NETWORK_SERVICE);if(enable) {networkManagement.disableAll();}else {networkManagement.enableAll();}CommonUtil.setEnabled(mContext, enable);}

结果表明,的确是可以达到开关网络的目的,且不需要root权限,证明此思路可行。那么上网白名单怎么实现呢?显然NetworkManagementService里没有直接提供,需要我们自己添加。

三. 修改系统,增加“上网过滤”功能。 
所谓“网络白名单”就是一串能访问的网址,除这串名单之外的网址不允许访问。这里最基本的功能就是只允许访问某一个站点,我们就从这儿着手。先看看如果调用iptables命令如何实现。

iptables -A Filter -p udp --dport 53 -j ACCEPT
iptables -A Filter -p tcp --dport 53 -j ACCEPT
iptables -A Filter -d www.163.com -j ACCEPT
iptables -A Filter -j DROP

上面这一串命令的作用就是只允许访问”www.163.com”站点。那么在安卓里面怎么实现呢?一种想法是先打开DNS解析的端口,加入上面第三条命令,再禁止所有,即第四条命令。另一种思路是先禁止所有站点,再打开DNS端口,再允许访问某一个站点。第一种思路是策略型的,需要自己定义默认策略(链表chain默认DROP),而我们复用的fw_OUTPUT、fw_INPUT的规则是非策略型的,如下

int FirewallController::enableFirewall(void) {int res = 0;// flush any existing rulesdisableFirewall();// create default rule to drop all trafficres |= execIptables(V4V6, "-A", LOCAL_INPUT, "-j", "DROP", NULL);res |= execIptables(V4V6, "-A", LOCAL_OUTPUT, "-j", "REJECT", NULL);res |= execIptables(V4V6, "-A", LOCAL_FORWARD, "-j", "REJECT", NULL);return res;
}

如果用这种思路就要修改这条链,或者自己再建一条链,比较麻烦,反而不如将“关闭所有网络”独立出来更方便。为了更灵活,我们选第二种思路。

  1. 关闭网络 
    在应用层调用NetworkManagementService的setFirewallEnabled(true)就好。
  2. 打开DNS端口 
    仿照setFirewallEnabled方法,在它实现的类FirewallController.cpp添加
int FirewallController::enableDNSPort(int protocol, int port) {char protocolStr[16];sprintf(protocolStr, "%d", protocol);char portStr[16];sprintf(portStr, "%d", port);int res = 0;res |= execIptables(V4, "-I", LOCAL_INPUT, "-p", protocolStr, "--sport", portStr, "-j", "ACCEPT", NULL);res |= execIptables(V4, "-I", LOCAL_OUTPUT, "-p", protocolStr, "--dport", portStr, "-j", "ACCEPT", NULL);return res;
}

别忘了在对应的头文件中声明这个函数。 
接着在维护接收framework传来的字符串命令的socket线程的CommandListener.cpp里处理上层发送来的命令:

int CommandListener::FirewallCmd::runCommand(SocketClient *cli, int argc,char **argv) {...// 第二个参数对应发送命令端的第二个参数if(!strcmp(argv[1], "enable_dns_port")) {// 3对应发送端的参数个数if (argc < 3) {cli->sendMsg(ResponseCode::CommandSyntaxError, "Missing argument", false);return 0;}// port为发送端的第三个参数int port = atoi(argv[2]);int res = 0;res |= sFirewallCtrl->enableDNSPort(PROTOCOL_UDP, port);res |= sFirewallCtrl->enableDNSPort(PROTOCOL_TCP, port);return sendGenericOkFail(cli, res);}cli->sendMsg(ResponseCode::CommandSyntaxError, "Unknown command", false);return 0;
}

接下来是发命令的地方NetworkManagementService,也就是命令的发送端:

mConnector.execute("firewall", "enable_dns_port", 53);

3.允许访问某一个站点 
同打开DNS端口的流程一样,先在FirewallController.cpp中增加如下:

int FirewallController::enableUrl(const char* addr) {int res = 0;res |= execIptables(V4, "-I", LOCAL_INPUT, "-s", addr, "-j", "ACCEPT", NULL);res |= execIptables(V4, "-I", LOCAL_OUTPUT, "-d", addr, "-j", "ACCEPT", NULL);return res;
}

同样别忘了声明。接着在CommandListener.cpp添加

 if(!strcmp(argv[1], "enable_url")) {if (argc < 3) {cli->sendMsg(ResponseCode::CommandSyntaxError,"Missing argument",false);return 0;}const char* addr = argv[2];int res = sFirewallCtrl->enableUrl(addr);return sendGenericOkFail(cli, res);}

发送命令处:

@Overridepublic void enableUrl(String url) {Slog.d(TAG, "enable url is: " + url);Preconditions.checkState(mFirewallEnabled);if(mFirewallEnabled) {try {mConnector.execute("firewall", "enable_dns_port", 53);Slog.d(TAG, "Firewall is enabled. Open dns port is: " + 53);} catch (NativeDaemonConnectorException e) {throw e.rethrowAsParcelableException();}}InetAddress[] ipArray;try {ipArray = InetAddress.getAllByName(url);if(ipArray != null) {for(int i = 0; i < ipArray.length; i++) {Slog.d(TAG, "ipArray[" + i +"].getHostAddress() = " + ipArray[i].getHostAddress());mConnector.execute("firewall", "enable_url", ipArray[i].getHostAddress());}}} catch (UnknownHostException e) {Slog.e(TAG, "Gets all IP addresses associated with the given host \"" + url +"\" faild. Because the host name can not be resolved.");e.printStackTrace();} catch (GaiException e) {Slog.e(TAG, "Get host address to string faild.");e.printStackTrace();} catch (NativeDaemonConnectorException e) {e.rethrowAsParcelableException();}}

有没有体会到分开控制的方便,只有在防火墙是开启的状态才打开DNS端口 ,这段方法其实已经做了DNS解析,如果解析失败,可以方便让应用程序知道。如果在底层让iptables自动解析也是可以的,就像上面提到的命令那样,但是不好返回解析的结果。 
这里需要注意的有三点:一是这里DNS的端口是53,但是别的平台可能不一样,最好不要写成静态的;二是可以在执行此方法之前先判断一下网络的状态,如果网络OK,再执行(添加规则到iptables chain)。三是这里修改的iptables链表的规则都是临时的,重启之后就失效,需要保存。 
到这里,是不是就做完了呢?当然不是。如果这样子,即使修改了系统,应用程序也没法编译通过,因为这段功能并没有在原先的API里面啊。这就涉及到增加系统API的方法,这会在下篇文章详细介绍,不过可以先来看看这个功能的最后一点实现,自定义一个类,封装我们的API:

public class NetworkManagement {private final INetworkManagementService mService;public NetworkManagement(INetworkManagementService mService) {this.mService = mService;}public void setFirewallEnabled(boolean enabled) {try {mService.setFirewallEnabled(enabled);} catch (RemoteException ex) {ex.printStackTrace();}}public void enableUrl(String url) {try {mService.enableUrl(url);} catch (RemoteException ex) {ex.printStackTrace();}}
}

将编译出来的jar引入到应用程序,就可以直接调用新生成的API啦。

四. 应用程序实现上网时间限制。 
上网时间的限制最基本的两种情形:一种是玩一段时间,提醒该休息了,并限制网络或锁屏;另一种是每天的一段时间内限制网络,其余时间不限制,或是一段时间不限制,其余时间限制。无论是哪一种都会涉及到采用哪种重复方式,就像闹钟里设置的重复方式一样,要么是一次性的,要么每天,要么每周的某几天…怎么做到定时重复呢,而且是全局的精确定时?当然iptables命令有个匹配参数-m time可以做到,然而可惜的是android并没有很好的支持,且上层不容易控制,那么还是在应用层做吧。既然谈到这个定时跟闹钟很像,我们不妨采用闹钟的定时方式,看起来完全可行呢!下面我们以第二种时间限制方式为例。 
查阅资料不难发现闹钟的定时功能用的是AlarmManager,一个系统service, 获得这个service很容易:

AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Activity.ALARM_SERVICE);

一种最典型的用法:

alarmManager.setRepeating(AlarmManager.RTC, timeStart.getTimeInMillis(), AlarmManager.INTERVAL_DAY, start_pendingIntent);

参数一表示定时时间的类型是系统绝对时间,休眠时不提醒; 
参数二是定时任务首次触发的时间,毫秒数表示; 
参数三是重复的间隔时间,也是毫秒数,这里是一天; 
参数四是定时任务,即时间到了,该执行什么操作。 
整个方法是设置了一个每日重复的定时任务。 
看看某一个定时任务:

ArrayList<String> list = new ArrayList(acceptSet);Intent start_intent = new Intent(mContext, TimerBroadcastReceiver.class);start_intent.putExtra("repeat_type", repeatType);start_intent.putExtra("isWhiteListMode", isWhiteListMode);
...
PendingIntent start_pendingIntent = PendingIntent.getBroadcast(mContext, 0, start_intent, PendingIntent.FLAG_UPDATE_CURRENT);

定时时间到,发送广播。传一些基本的数值,具体的操作在接收广播的地方处理。 
这里需要注意的主要有两点:一是因为是时间段,所以有起始时间和结束时间,两个时间点要做的任务不同,所以应该分开设置定时任务;二是Android在API 19之后,所有的重复闹钟(如setRepeating)都不精确了 , 据说是为了优化电源性能,不再实时响应每一个定时器,而是每隔5分钟一起响应这5分钟内的所有定时器,当然5分钟只是打个比方。那么在API 19之后怎样做到精确的重复定时呢?办法在API文档中已经提示了,就是使用一次性的精确定时,然后每一次时间到的时候重新设置。

alarmManager.setExact(AlarmManager.RTC, timeStart.getTimeInMillis(), start_pendingIntent);
...
nextStartTime.setTimeInMillis(times[2] + AlarmManager.INTERVAL_DAY);
setAlarm(startDate, stopDate, nextStartTime, stopTime, set, true, repeatType);

上面这个例子就是每收到定时时间到的广播就设置下一天同一时刻的定时器。另外还要注意保存设置的信息,等到下次开机的时候还能拿到之前的数据,默默地重新设置。 
到此,Android定制实现上网限制的基本功能就写完了,算是一个完整的节点,以后有机会再扩展。

查询
adb shell iptables -L OUTPUT -nv  --line-number 新增(2条,顺序不能颠倒)
adb shell iptables -A OUTPUT -m limit  --limit 10/s -j ACCEPT
adb shell iptables -A OUTPUT -j DROP删除
adb shell iptables -D OUTPUT -m limit  --limit 10/s -j ACCEPT
adb shell iptables -D OUTPUT -j DROP

Android定制实现上网限制iptables相关推荐

  1. Android定制争夺战 三大主流ROM横评

    随着MIUI在广大"机油"们心目中位置的逐渐攀升,越来越多的厂商也相继推出了属于自己的定制Android ROM,想以此来抢占这一新兴市场,像点心OS.腾讯的Tita以及近期比较热 ...

  2. Android定制--------系统内置文件,用到linux服务(视频文件、音频文件等)

    在Android定制的过程中,有客户要求将一些视频.音频等文件内置在系统中.将过程记录下来,方便以后查询. 内置过程大致分为以下几个过程: 1.将需要内置的我文件,放置指定文件夹. 2.用mk文件将需 ...

  3. Android手机GPRS上网和彩信设置教程

    Android手机刚在内地市场流通的时候,如何设置手机接收彩信简直和各大巴赫猜想一样难以捉摸,论坛上流传着各种彩信的设置参数,而每种设置办法都只有一部分用户可以正常使用,甚至有一些沮丧的用户开始认为A ...

  4. Android 手机GPRS 上网和彩信设置教程

    Android手机刚在内地市场流通的时候,如何设置手机接收彩信简直和各大巴赫猜想一样难以捉摸,论坛上流传着各种彩信的设置参数,而每种设置办法都只有一部分用户可以正常使用,甚至有一些沮丧的用户开始认为A ...

  5. Android 定制Google SetupWizard

    ##Android 定制Google SetupWizard #####google setupwizard(开机向导) 是可以定制,我们可以替换掉里面的页面,也可以新添加页面,也可以修改原有页面的部 ...

  6. Android定制:修改开机启动画面

    转自:https://blog.csdn.net/godiors_163/article/details/72529210 引言 Android系统在按下开机键之后就会进入启动流程,这个过程本身需要一 ...

  7. Android—— 定制界面风格

    统一的用户界面是可以使得应用程序更友好.要做到用户界面的统一,我们就必须用到风格(style)和主题(theme).OPhone系统提供了很多系统默认的风格和主题,但是很多情况下,这些不能满足我们的需 ...

  8. Android模拟器无法上网问题

    方法一 首先,Windows下,配置Adroid环境变量(Win7为例) 1.桌面右键-->我的电脑-->高级系统设置 2.高级-->环境变量-->系统变量-->Path ...

  9. 【Android】Android模拟器无法上网问题

    方法一 首先,Windows下,配置Adroid环境变量(Win7为例) 1.桌面右键-->我的电脑-->高级系统设置 2.高级-->环境变量-->系统变量-->Path ...

最新文章

  1. Logback 配置文件这样优化,TPS提高 10 倍
  2. python中列表用某个数字出现的次数_Python实现统计给定列表中指定数字出现次数的方法...
  3. PMcaff-干货| App品牌初期搭建必须要注意的几个点
  4. 随e行安全层在与远程计算机初始化,g3随e行怎么用_g3随e行怎么安装_随e行wlan无法登陆...
  5. Java 并发时的 互斥锁机制
  6. Gradle 配置jetty启动项目
  7. 迅为工业级iMX6Q开发板全新升级兼容PLUS版本|四核商业级|工业级|双核商业级...
  8. tomcat安装成功页面翻译
  9. 好用又被遗忘的Char,String 方法
  10. 程序员的进阶课-架构师之路(8)-二叉树
  11. 推荐系统学习(三)SVD奇异值分解做推荐与python代码
  12. IoC 容器和 Dependency Injection 模式[转]
  13. mujoco_py中文文档
  14. 谷歌浏览器无法翻译此网页的解决办法
  15. LoopClosing中为什么要使用剥离尺度的sim3计算投影匹配
  16. 终端服务器超出了最大允许连接数解决办法
  17. 巧用 arp 命令 防p2p终结者的方法
  18. 精伦身份证阅读器php_华视CVR-100身份证阅读器BS开发包
  19. (31)2021-01-20(JSON字符串和本地存储)
  20. 学习笔记19—dpabi错误集

热门文章

  1. Python自然语言处理 | 加工原料文本
  2. 干货分享)五金模具设计的资料!
  3. 【读书笔记】彼得德鲁克管理理念摘写
  4. 论文检测系统是怎么检测呢?
  5. php app微信支付demo下载,HTML5微信支付DEMO
  6. qq服务器 udp协议,基于UDP傳输协议的实时通信系统的开发(仿qq聊天程序).doc
  7. labview与stm32通信
  8. mac上超受欢迎的ppt软件:PowerPoint 2021中文版
  9. ESXI 网卡等PCI设备硬件直通配置
  10. buuuuu流量分析刷题