/**
* Keep track of wifi/3G/tethering status and LAN IP ranges.
*
* Copyright (C) 2013 Kevin Cernekee
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @author Kevin Cernekee
* @version 1.0
*/
package dev.ukanth.ufirewall;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.DhcpInfo;
import android.net.NetworkInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.support.v4.app.NotificationCompat;
import java.lang.reflect.Method;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Locale;
import dev.ukanth.ufirewall.service.RootShell.RootCommand;
import dev.ukanth.ufirewall.log.Log;
import dev.ukanth.ufirewall.util.G;
public final class InterfaceTracker {
public static final String TAG = "AFWall";
public static final String ITFS_WIFI[] = { "eth+", "wlan+", "tiwlan+", "ra+", "bnep+" };
public static final String ITFS_3G[] = { "rmnet+", "pdp+", "uwbr+","wimax+", "vsnet+",
"rmnet_sdio+", "ccmni+", "qmi+", "svnet0+", "ccemni+","rmnet_usb+",
"wwan+", "cdma_rmnet+", "usb+", "rmnet_usb+","clat4+", "cc2mni+", "bond1+", "rmnet_smux+" };
public static final String ITFS_VPN[] = { "tun+", "ppp+", "tap+" };
public static final String BOOT_COMPLETED = "BOOT_COMPLETED";
public static final String CONNECTIVITY_CHANGE = "CONNECTIVITY_CHANGE";
public static final int ERROR_NOTIFICATION_ID = 1;
private static InterfaceDetails currentCfg = null;
private static class OldInterfaceScanner {
private static String intToDottedQuad(int ip) {
return String.format(Locale.US, "%d.%d.%d.%d",
(ip >>> 0) & 0xff,
(ip >>> 8) & 0xff,
(ip >>> 16) & 0xff,
(ip >>> 24) & 0xff);
}
public static void populateLanMasks(Context context, String[] names, InterfaceDetails ret) {
WifiManager wifi = (WifiManager)context.getSystemService(Context.WIFI_SERVICE);
DhcpInfo dhcp = wifi.getDhcpInfo();
if (dhcp != null) {
ret.lanMaskV4 = intToDottedQuad(dhcp.ipAddress) + "/" +
intToDottedQuad(dhcp.netmask);
ret.wifiName = "UNKNOWN";
}
}
}
private static class NewInterfaceScanner {
private static String truncAfter(String in, String regexp) {
return in.split(regexp)[0];
}
@TargetApi(Build.VERSION_CODES.GINGERBREAD)
public static void populateLanMasks(Context context, String[] names, InterfaceDetails ret) {
try {
Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
while (en.hasMoreElements()) {
NetworkInterface intf = en.nextElement();
boolean match = false;
if (!intf.isUp() || intf.isLoopback()) {
continue;
}
for (String pattern : ITFS_WIFI) {
if (intf.getName().startsWith(truncAfter(pattern, "\\+"))) {
match = true;
break;
}
}
if (!match)
continue;
ret.wifiName = intf.getName();
Iterator<InterfaceAddress> addrList = intf.getInterfaceAddresses().iterator();
while (addrList.hasNext()) {
InterfaceAddress addr = addrList.next();
InetAddress ip = addr.getAddress();
String mask = truncAfter(ip.getHostAddress(), "%") + "/" +
addr.getNetworkPrefixLength();
if (ip instanceof Inet4Address) {
ret.lanMaskV4 = mask;
} else if (ip instanceof Inet6Address) {
ret.lanMaskV6 = mask;
}
}
}
} catch (SocketException e) {
Log.e(TAG, "Error fetching network interface list");
} catch (Exception e) {
Log.e(TAG, "Error fetching network interface list");
}
}
}
private static void getTetherStatus(Context context, InterfaceDetails d) {
WifiManager wifi = (WifiManager)context.getSystemService(Context.WIFI_SERVICE);
Method[] wmMethods = wifi.getClass().getDeclaredMethods();
d.isTethered = false;
d.tetherStatusKnown = false;
for(Method method: wmMethods) {
if(method.getName().equals("isWifiApEnabled")) {
try {
d.isTethered = ((Boolean)method.invoke(wifi)).booleanValue();
d.tetherStatusKnown = true;
Log.d(TAG, "isWifiApEnabled is " + d.isTethered);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
private static InterfaceDetails getInterfaceDetails(Context context) {
InterfaceDetails ret = new InterfaceDetails();
ConnectivityManager cm = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = cm.getActiveNetworkInfo();
if (info == null || info.isConnected() == false) {
return ret;
}
switch (info.getType()) {
case ConnectivityManager.TYPE_MOBILE:
case ConnectivityManager.TYPE_MOBILE_DUN:
case ConnectivityManager.TYPE_MOBILE_HIPRI:
case ConnectivityManager.TYPE_MOBILE_MMS:
case ConnectivityManager.TYPE_MOBILE_SUPL:
case ConnectivityManager.TYPE_WIMAX:
ret.isRoaming = info.isRoaming();
ret.netType = ConnectivityManager.TYPE_MOBILE;
ret.netEnabled = true;
break;
case ConnectivityManager.TYPE_WIFI:
case ConnectivityManager.TYPE_BLUETOOTH:
case ConnectivityManager.TYPE_ETHERNET:
ret.netType = ConnectivityManager.TYPE_WIFI;
ret.netEnabled = true;
break;
}
getTetherStatus(context, ret);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
OldInterfaceScanner.populateLanMasks(context, ITFS_WIFI, ret);
} else {
NewInterfaceScanner.populateLanMasks(context, ITFS_WIFI, ret);
}
return ret;
}
public static boolean isNetworkUp(Context context){
return getInterfaceDetails(context).netEnabled;
}
public static boolean checkForNewCfg(Context context) {
InterfaceDetails newCfg = getInterfaceDetails(context);
if (currentCfg != null && currentCfg.equals(newCfg)) {
return false;
}
currentCfg = newCfg;
if (!newCfg.netEnabled) {
Log.i(TAG, "Now assuming NO connection (all interfaces down)");
} else {
if (newCfg.netType == ConnectivityManager.TYPE_WIFI) {
Log.i(TAG, "Now assuming wifi connection");
} else if (newCfg.netType == ConnectivityManager.TYPE_MOBILE) {
Log.i(TAG, "Now assuming 3G connection (" +
(newCfg.isRoaming ? "roaming, " : "") +
(newCfg.isTethered ? "tethered" : "non-tethered") + ")");
}
if (!newCfg.lanMaskV4.equals("")) {
Log.i(TAG, "IPv4 LAN netmask on " + newCfg.wifiName + ": " + newCfg.lanMaskV4);
}
if (!newCfg.lanMaskV6.equals("")) {
Log.i(TAG, "IPv6 LAN netmask on " + newCfg.wifiName + ": " + newCfg.lanMaskV6);
}
}
return true;
}
public static InterfaceDetails getCurrentCfg(Context context) {
if (currentCfg == null) {
currentCfg = getInterfaceDetails(context);
}
return currentCfg;
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private static void errorNotification(Context ctx) {
NotificationManager mNotificationManager =
(NotificationManager)ctx.getSystemService(Context.NOTIFICATION_SERVICE);
// Artificial stack so that navigating backward leads back to the Home screen
TaskStackBuilder stackBuilder = TaskStackBuilder.create(ctx)
.addParentStack(MainActivity.class)
.addNextIntent(new Intent(ctx, MainActivity.class));
Notification notification = new NotificationCompat.Builder(ctx)
.setContentTitle(ctx.getString(R.string.error_notification_title))
.setContentText(ctx.getString(R.string.error_notification_text))
.setTicker(ctx.getString(R.string.error_notification_ticker))
.setSmallIcon(R.drawable.notification_warn)
.setAutoCancel(true)
.setContentIntent(stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT))
.build();
mNotificationManager.notify(ERROR_NOTIFICATION_ID, notification);
}
public static void applyRulesOnChange(Context context, final String reason) {
final Context ctx = context.getApplicationContext();
if (!checkForNewCfg(ctx)) {
Log.d(TAG, reason + ": interface state has not changed, ignoring");
return;
} else if (!Api.isEnabled(ctx)) {
Log.d(TAG, reason + ": firewall is disabled, ignoring");
return;
}
// update Api.PREFS_NAME so we pick up the right profile
// REVISIT: this can be removed once we're confident that G is in sync with profile changes
G.reloadPrefs();
boolean ret = Api.fastApply(ctx, new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
Log.i(TAG, reason + ": applied rules");
} else {
// error details are already in logcat
//but lets try to run the full rules once
Api.applySavedIptablesRules(ctx, false,new RootCommand()
.setFailureToast(R.string.error_apply)
.setCallback(new RootCommand.Callback() {
@Override
public void cbFunc(RootCommand state) {
if (state.exitCode == 0) {
Log.i(TAG, reason + ": applied rules");
} else {
Api.setEnabled(ctx, false, false);
errorNotification(ctx);
}
}
}));
}
}
}));
if (!ret) {
Log.e(TAG, reason + ": applySavedIptablesRules() returned an error");
errorNotification(ctx);
}
}
public static String matchName(String[] patterns, String name) {
for (String p : patterns) {
int minLen = Math.min(p.length(), name.length());
for (int i = 0; ; i++) {
if (i == minLen) {
if (name.length() == p.length()) {
// exact match
return p;
}
break;
}
if (name.charAt(i) != p.charAt(i)) {
if (p.charAt(i) == '+') {
// wildcard match
return p;
}
break;
}
}
}
return null;
}
}