代码编织梦想

要拦截 DNS 解析,首先得找到系统哪个类去解析 Host 的,找到对应的 hook 点去实现。

一,去了解一些常用的联网类:

Volley 默认使用的是 HttpURLConnection。
而 HttpURLConnection 底层 DNS 解析用的是:

InetAddress.getAllByName();

Okhttp 是可以自定义 DNS 的:

new OkHttpClient().newBuilder().dns(new Dns() {
	@Override
	public List<InetAddress> lookup(String hostname) throws UnknownHostException {
		.....................
	}
});

而 Okhttp 默认 DNS 是 Dns.SYSTEM,实际也是 InetAddress.getAllByName();

二,分析 InetAddress 源码,找到 Hook 点:

InetAddress.getAllByName(); 在 Android 7.0 之后有修改。
这里先分析 Android 7.0 之后的源码:

public class InetAddress implements java.io.Serializable {
	static final InetAddressImpl impl = new Inet6AddressImpl();
    public static InetAddress[] getAllByName(String host) throws UnknownHostException {
        return impl.lookupAllHostAddr(host, NETID_UNSET).clone();
    }
}
// 发现 Inet6AddressImpl 是个 接口实现类
class Inet6AddressImpl implements InetAddressImpl {
	.............
}

那么通过反射和动态代理,重新赋值 impl 变量,就可以拦截 DNS 解析了。

再来分析 Android 7.0 之前的源码:

public class InetAddress implements java.io.Serializable {
	private static final AddressCache addressCache = new AddressCache();
    public static InetAddress[] getAllByName(String host) throws UnknownHostException {
        return getAllByNameImpl(host, NETID_UNSET).clone();
    }

    private static InetAddress[] getAllByNameImpl(String host, int netId) throws UnknownHostException {
        if (host == null || host.isEmpty()) {
            return loopbackAddresses();
        }

        // Is it a numeric address?
        InetAddress result = parseNumericAddressNoThrow(host);
        if (result != null) {
            result = disallowDeprecatedFormats(host, result);
            if (result == null) {
                throw new UnknownHostException("Deprecated IPv4 address format: " + host);
            }
            return new InetAddress[] { result };
        }

        return lookupHostByName(host, netId).clone();
    }


    private static InetAddress[] lookupHostByName(String host, int netId) throws UnknownHostException {
    	// 这里会从 缓存中取
        Object cachedResult = addressCache.get(host, netId);
        if (cachedResult != null) {
            if (cachedResult instanceof InetAddress[]) {
                return (InetAddress[]) cachedResult;
            }
        }
        .................
    }
}
class AddressCache {
    private static final int MAX_ENTRIES = 16;
    // cache just 2s.
    private static final long TTL_NANOS = 2 * 1000000000L;
    
    static class AddressCacheEntry {
        final Object value;
        final long expiryNanos;
        AddressCacheEntry(Object value) {
            this.value = value;
            this.expiryNanos = System.nanoTime() + TTL_NANOS;
        }
    }

	// 这里 get 的时候,缓存有有效时间
	// 而从 put 方法,AddressCacheEntry 构造方法里,知道有效时间是 TTL_NANOS(2s)
    public Object get(String hostname, int netId) {
        AddressCacheEntry entry = cache.get(new AddressCacheKey(hostname, netId));
        if (entry != null && entry.expiryNanos >= System.nanoTime()) {
            return entry.value;
        }
        return null;
    }

    public void put(String hostname, int netId, InetAddress[] addresses) {
        cache.put(new AddressCacheKey(hostname, netId), new AddressCacheEntry(addresses));
    }
}

从源码我们了解到可以利用 addressCache 缓存,提前处理好 DNS解析,但是这个缓存又只有 2s 有效时间。

三,具体实现代码:

public class DnsUtils {
    private final static String TAG = "DnsUtils";
    private static final Pattern IPV4_PATTERN = Pattern.compile("^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$");

    public static void hook() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            hookN();
        }
    }

    public static boolean isIpv4(String address) {
        return IPV4_PATTERN.matcher(address).matches();
    }

    /**
     * 7.0 之后 InetAddress 才有 impl
     */
    private static void hookN() {
        try {
            if (LogUtils.isDebug()) {
                LogUtils.d(TAG, "invoke 111:ipv6FirstN");
            }
            //获取InetAddress中的impl
            Field impl = InetAddress.class.getDeclaredField("impl");
            impl.setAccessible(true);
            //获取accessFlags
            Field modifiersField = Field.class.getDeclaredField("accessFlags");
            modifiersField.setAccessible(true);
            //去final
            modifiersField.setInt(impl, impl.getModifiers() & ~java.lang.reflect.Modifier.FINAL);
            //获取原始InetAddressImpl对象
            final Object originalImpl = impl.get(null);
            //构建动态代理InetAddressImpl对象
            Object dynamicImpl = Proxy.newProxyInstance(originalImpl.getClass().getClassLoader(), originalImpl.getClass().getInterfaces(), new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    //如果函数名为lookupAllHostAddr,并且参数长度为2,第一个参数是host,第二个参数是netId
                    Object originalResult = method.invoke(originalImpl, args);
                    if (method.getName().equals("lookupAllHostAddr") && args != null && args.length == 2 && originalResult != null) {
                        InetAddress[] originalAddresses = (InetAddress[]) originalResult;
                        return myDnsLogic(originalAddresses);
                    }
                    return originalResult;
                }
            });
            //替换impl为动态代理对象
            impl.set(null, dynamicImpl);
            //还原final
            modifiersField.setInt(impl, impl.getModifiers() & java.lang.reflect.Modifier.FINAL);
        } catch (Throwable e) {
            if (LogUtils.isDebug()) {
                LogUtils.e(TAG, "invoke 555:" + e);
            }
        }
    }

    private static InetAddress[] myDnsLogic(InetAddress[] originalAddresses) {
        // 做自己的逻辑;
        return result;
    }

    /**
     * 7.0之前,不能动态代理,只能 addressCache 缓存来弄。
     * 而缓存只有 2S 有效时间
     */
    private static void hookM(final String hostName) {
        BackgroundExecutors.getGlobalExecutor().post(new Runnable() {
            @Override
            public void run() {
                try {
                    if (LogUtils.isDebug()) {
                        LogUtils.d(TAG, "invoke 111:ipv6FirstM");
                    }
                    InetAddress[] originalAddresses = InetAddress.getAllByName(hostName);
                    InetAddress[] result = myDnsLogic(originalAddresses);
                    Class inetAddressClass = null;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                        inetAddressClass = Class.forName("java.net.Inet6AddressImpl");
                    } else {
                        inetAddressClass = InetAddress.class;
                    }
                    Field field = inetAddressClass.getDeclaredField("addressCache");
                    field.setAccessible(true);
                    Object object = field.get(inetAddressClass);
                    LogUtils.d(TAG, "test 11:" + object);
                    Class cacheClass = object.getClass();
                    Method putMethod;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                        //put方法在api21及以上为put(String host, int netId, InetAddress[] address)
                        putMethod = cacheClass.getDeclaredMethod("put", String.class, int.class, InetAddress[].class);
                        putMethod.setAccessible(true);
                        putMethod.invoke(object, hostName, 0, result);
                    } else {
                        //put方法在api20及以下为put(String host, InetAddress[] address)
                        putMethod = cacheClass.getDeclaredMethod("put", String.class, InetAddress[].class);
                        putMethod.setAccessible(true);
                        putMethod.invoke(object, hostName, result);
                    }
                    InetAddress[] test = InetAddress.getAllByName(hostName);
                    if (LogUtils.isDebug()) {
                        for (InetAddress inetAddress : test) {
                            LogUtils.d(TAG, "test 777:" + inetAddress);
                        }
                    }
                } catch (Throwable e) {
                    if (LogUtils.isDebug()) {
                        LogUtils.e(TAG, "invoke 666:" + e);
                    }
                }
            }
        });
    }
}

四,总结:

1,如果项目中只使用了 Okhttp,那么可以自定义 DNS 解析,很简单。
2,如果项目中用的其他库,所以可以参考上面的第三点的代码。
3,利用反射拦截的方式,对于7.0以下的手机,不方便,只因为缓存只有2秒,所以必须得在每次请求前调用一次 hookM 方法。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn.net/miLLlulei/article/details/110090287

Android 重命名、删除文件后,文件仍存在系统数据库问题,(本地文件已改变)-爱代码爱编程

解决方案:通知系统扫描更新数据库: Intent mediaScanIntent = new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);//通知系统更新数据库 Uri contentUri = Uri.fromFile

Mac下Android的adb环境配置-爱代码爱编程

前提是安装了AndroidStudio。 一、配置 1、打开terminal终端 2、进入当前用户的home目录 3、创建(存在则不创建).bash_profile文件 3.1、创建 touch .bash_profile 3.2、打开 vim .bash_profile 4、进入编辑模式,在文末编辑添加如下代码 4.1、编辑模式,按字母键i 4.2、编

Flutter之CustomPaint 绘制贝塞尔曲线图表(三)-爱代码爱编程

简介 继上两篇所说,在功能实现后,补全这个虽然残缺但是比较有学习价值的DEMO: Flutter - 仿Airbnb的价格区间筛选器。(一) Flutter - 仿Airbnb的价格区间筛选器。(二) Flutter-CustomPaint 绘制贝塞尔曲线图表(三) 页面布局 如之前一样,我习惯把介绍写到注释里,这样方便联系代码,不会导致

课后作业:仿照微信生成类似的菜单-爱代码爱编程

文章目录 一.利用配置文件生成菜单(一)实现步骤1、创建安卓应用【WeChatMenu】2、将背景图片拷贝到drawable目录中3、修改主布局资源文件activity_main.xml4、新建menu目录,在里面创建main.xml文件5、修改字符串资源文件strings.xml6、修改主界面类 - MainActivity(二)运行效果 一

Fragment和Activity之间的通信-爱代码爱编程

Fragment和Activity通信 1. Fragment->Activity 可以在fragment编写interface接口,实例化mListener字段(初始化设置为null)、在Activity实现回调接口,在Fragment调用onAttach的时候实现,实例化mListener字段,实现调用。参考文章:https://blog.c

Android 签名配置,查看签名信息-爱代码爱编程

gradle 自动签名打包 配置build.gradle apply plugin: 'com.android.application' android { compileSdkVersion 30 buildToolsVersion "30.0.2" defaultConfig { applicationId

Hexo + Coding 搭建博客之自定义域名的坑-爱代码爱编程

前言 Hexo 搭建的博客一般都是托管到第三方网站,如GitHub、Gitee、Coding。 通常情况下,大多数教程或者是 Hexo 官网都介绍的是部署到 GitHub,但众所周知,GitHub的访问速度实在跟不上节奏,故而出现了双访问通道的部署方式: 国内线路部署到 Coding国外线路部署到 GitHub本文主要讨论部署到 Coding 时,应

DNS服务器systemctl start named启动失败-爱代码爱编程

systemctl restart named Job for named.service failed because the control process exited with error code. See “systemctl status named.service” and “journalctl -xe” for details. 命令

macOS MacbookPro Mac 通过 Terminal 设置 DNS-爱代码爱编程

macOS MacbookPro Mac 通过 Terminal 设置 DNS macOS 通过 Terminal 终端设置网络的 DNS 有时候对应不同的网络需要切换不同的 DNS 网络才正常,比如从住的地方到工作的地方,使用的网络运营商不同,对应的 DNS 解析速度就不同,进而导致网络不好。 一、普通设置网络 DNS 的步骤 经常性的打开网络偏

5-1:名字与地址转换-爱代码爱编程

gethostbyname,gethostbyaddr struct hostent* gethostbyname(const char*hostname); struct hostent { char* h_name;// 主机名 char** h_aliases;// 别名列表 int h_addrtype; int h_length;//

golang:第三方库,dns解析库miekg/dns-爱代码爱编程

文章目录 一、dns解析库miekg/dns1.1 dns查询 一、dns解析库miekg/dns 第三方dns库miekgmiekg/dns 可以做dns查询、可以做local dns、可以做授权dns。 由于下载不到:golang.org/x/crypto/ed25519,需要将代码中相关的代码删除掉,做dns查询时不会用到该部分代码。

dns域名解析过程-爱代码爱编程

DNS即Domain Name System,是域名解析服务的意思。它在互联网的作用是:把域名转换成为网络可以识别的ip地址。人们习惯记忆域名,但机器间互相只认IP地址,域名与IP地址之间是一一对应的,它们之间的转换工作称为域名解析,域名解析需要由专门的域名解析服务器来完成,整个过程是自动进行的。比如:上网时输入的www.baidu.com会自动转换成为2