├── .gitmodules ├── README.md ├── jan21.png ├── make.sh ├── pom.xml ├── release-note.md └── server ├── README.md ├── benchmark-all-versions ├── benchmark-other-dns-server ├── blackhole.sh ├── config ├── blackhole.conf └── zones ├── pom.xml └── src ├── main ├── java │ └── us │ │ └── codecraft │ │ └── blackhole │ │ ├── BlackHole.java │ │ ├── answer │ │ ├── AbstractAnswerHandler.java │ │ ├── AnswerPatternProvider.java │ │ ├── AnswerProvider.java │ │ ├── CustomAnswerPatternProvider.java │ │ ├── CustomTempAnswerProvider.java │ │ ├── DomainPatternsContainer.java │ │ ├── PostAnswerHandler.java │ │ ├── PreAnswerHandler.java │ │ ├── SafeHostAnswerProvider.java │ │ └── TempAnswerProvider.java │ │ ├── antipollution │ │ ├── BlackListManager.java │ │ └── SafeHostManager.java │ │ ├── cache │ │ ├── CacheClient.java │ │ ├── CacheManager.java │ │ ├── EhcacheClient.java │ │ ├── MapCacheClient.java │ │ └── UDPPackage.java │ │ ├── concurrent │ │ └── ThreadPools.java │ │ ├── config │ │ ├── ConfigFileLoader.java │ │ ├── ConfigFileRefresher.java │ │ ├── Configure.java │ │ ├── DomainPattern.java │ │ ├── ZonesFileLoader.java │ │ ├── ZonesFileRefresher.java │ │ └── ZonesPattern.java │ │ ├── connector │ │ ├── UDPConnectionResponser.java │ │ ├── UDPConnectionWorker.java │ │ └── UDPSocketMonitor.java │ │ ├── container │ │ ├── Handler.java │ │ ├── HandlerManager.java │ │ ├── HeaderHandler.java │ │ ├── MessageWrapper.java │ │ └── QueryProcesser.java │ │ ├── context │ │ ├── RequestContext.java │ │ └── RequestContextProcessor.java │ │ ├── forward │ │ ├── ConnectionTimer.java │ │ ├── DNSHostsContainer.java │ │ ├── ForwardAnswer.java │ │ ├── ForwardAnswerProcessor.java │ │ ├── Forwarder.java │ │ ├── MultiUDPForwarder.java │ │ └── MultiUDPReceiver.java │ │ └── utils │ │ ├── DoubleKeyMap.java │ │ ├── MultiKeyMapBase.java │ │ ├── RecordBuilder.java │ │ ├── RecordUtils.java │ │ └── SpringLocator.java └── resources │ ├── ehcache.xml │ ├── log4j.xml │ └── spring │ └── applicationContext-blackhole.xml └── test └── java └── us └── codecraft └── blackhole └── multiforward ├── ForwardTimerTest.java └── MultiReceiverTest.java /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "wifesays"] 2 | path = wifesays 3 | url = git://github.com/code4craft/wifesays.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BlackHole 2 | ========= 3 | 4 | ### 1. 简介 5 | 6 | BlackHole是一个Java编写的DNS服务器,它可以进行DNS缓存,也支持自定义域名配置,并可以防止DNS污染。比起老牌的DNS软件pdnsd、BIND,BlackHole功能比较简单,但是更容易使用,性能也更好。 7 | 8 | BlackHole还包含一个Web管理模块[**Hostd**](https://github.com/code4craft/hostd),可以让每个用户管理自己的域名配置,并且彼此之间不冲突。 9 | 10 | ### 2. 用途 11 | 12 | #### DNS缓存 13 | 14 | BlackHole具有DNS缓存以及持久化的功能,可以作为一个DNS缓存服务器使用,以加速DNS访问。 15 | 16 | BlackHole缓存性能优秀,可以支持每秒50000次随机查询,平均响应时间0.3ms,高于pdnsd及BIND([测试报告](https://github.com/code4craft/blackhole/blob/master/server/benchmark-other-dns-server))。 17 | 18 | #### hosts风格自定义域名 19 | 20 | BlackHole也支持修改域名配置,配置域名的方式非常简单,与hosts文件一致,并且支持通配符(目前仅支持A记录)。 21 | 22 | 例如: 23 | 24 | 127.0.0.1 *.codecraft.us 25 | 26 | 表示将所有以.codecraft.us形式结尾的域名全部指向127.0.0.1。 27 | 28 | #### 防止DNS污染 29 | 30 | BlackHole还可以通过UDP特征判断的方式防止DNS污染攻击,对于某些无法访问的网站可以起到作用。BlackHole防止DNS的方式参见:[http://code4craft.github.com/blog/2013/02/25/blackhole-anti-dns-poison/](http://code4craft.github.com/blog/2013/02/25/blackhole-anti-dns-poison/) 31 | 32 | 33 | ### 3. 安装及配置 34 | 35 | 你使用自动脚本进行安装BlackHole: 36 | 37 | curl http://code4craft.github.io/blackhole/install.sh | [sudo] sh 38 | 39 | BlackHole的另一个编译后版本保存在[https://github.com/code4craft/blackhole-bin](https://github.com/code4craft/blackhole-bin),如果以上脚本对你所在环境不可用,那么可以clone这个项目到某一目录。 40 | 41 | git clone https://github.com/code4craft/blackhole-bin.git /usr/local/blackhole 42 | 43 | 通过sudo /usr/local/blackhole/blackhole.sh start可以启动BlackHole。 44 | 45 | Windows系统可将文件保存到任意目录,并运行start.bat(Win7下无需用管理员权限启动),若弹出终端界面并且持续运行,则启动成功。 46 | 47 | 各种问题解决、具体的设置以及技术细节请看[Blackhole Server Docs](https://github.com/code4craft/blackhole/blob/master/server/README.md)。 48 | 49 | ### 4. 协议 50 | 51 | BlackHole的连接部分参考了EagleDNS的代码,遵守LGPLv3协议。 52 | 53 | 作者邮箱: 54 | code4crafter@gmail.com 55 | 56 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/code4craft/blackhole/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 57 | 58 | -------------------------------------------------------------------------------- /jan21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code4craft/blackhole/11bd95718cf107877ad3ee6b070dc390c3b98d4b/jan21.png -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | mkdir -p /usr/local/blackhole/ 3 | mkdir -p /usr/local/blackhole/lib 4 | if [ ! -d /usr/local/blackhole/config ] 5 | then 6 | mkdir -p /usr/local/blackhole/config 7 | cp ./server/config/* /usr/local/blackhole/config/ 8 | fi 9 | cp ./server/blackhole.sh /usr/local/blackhole/ 10 | cp ./server/target/blackhole*.jar /usr/local/blackhole/blackhole.jar 11 | rsync -avz --delete ./server/target/lib/ /usr/local/blackhole/lib/ 12 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | us.codecraft 4 | blackhole-module 5 | pom 6 | 0.0.1-SNAPSHOT 7 | 8 | ./wifesays 9 | ./server/ 10 | 11 | 12 | -------------------------------------------------------------------------------- /release-note.md: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ---- 3 | *2013-8-12* `version:1.2.3` 4 | 5 | bugfix: [issue#11 Forward的请求包不合规范](https://github.com/code4craft/blackhole/issues/11) 6 | 7 | *2013-7-16* `version:1.2.2` 8 | 9 | 极大优化了A记录配置的性能。 10 | 11 | 修复数万条配置的情况下,性能明显下降的问题[issue#9](https://github.com/code4craft/blackhole/issues/9)。 12 | 13 | 新增NS格式的配置。 14 | 15 | *2013-6-22* `version:1.2.1` 16 | 17 | 增加自定义缓存过期时间的功能。 18 | 19 | 优化了缓存目录地址。 20 | 21 | 修复了缓存持久化不生效的bug。 22 | 23 | 详细进行了性能测试,并更新了文档。 24 | 25 | *2013-5-31* `version:1.2.0` 26 | 27 | * [issues#5](https://github.com/code4craft/blackhole/issues/5) 对不同的用户组提供不同的DNS响应,从而可以让每个用户管理自己的DNS配置。 28 | 29 | * 开发了一个Web模块[Hostd](http://code4craft.github.io/hostd/),类似修改hosts,可以供所有用户进行自己DNS配置的管理。 30 | 31 | * [issues#8](https://github.com/code4craft/blackhole/issues/8) 对forward的外部DNS设定优先级 便于企业内网配置自己的DNS地址,防止被响应被覆盖。 32 | 33 | *2013-5-7* 34 | 35 | * 修复启动失败后进程不退出的bug。 36 | 37 | *2013-4-27* `version:1.1.3` 38 | 39 | 1.1.3发布。 40 | 41 | * 重写了代理模式,​改为纯异步I/O实现,大大降低使用线程数,并提高了25%的效率。 42 | 43 | * 修复一个高并发下某些响应丢失的bug。 44 | 45 | *2013-4-2* `version:1.1.2` 46 | 47 | 1.1.2发布,修复了一个返回空响应体导致DNS查找失败的问题,从此稳定性大大提高[https://github.com/code4craft/blackhole/issues/3](https://github.com/code4craft/blackhole/issues/3)。 48 | 49 | BlackHole也迎来第一位企业级用户。争取发展成为一个公司内网使用的简单可配置的DNS服务器。 50 | 51 | *2013-3-24* `version:1.1.1` 52 | 53 | 1.1.1发布,增加NS配置功能,详情见[https://github.com/code4craft/blackhole/blob/master/server/README.md](https://github.com/code4craft/blackhole/blob/master/server/README.md) 54 | 55 | *2013-2-25* `version: 1.1` 56 | 57 | 1.1发布,偷偷增加反DNS污染功能。原理: 58 | [http://my.oschina.net/flashsword/blog/110276](http://my.oschina.net/flashsword/blog/110276) 59 | 60 | *2012-12-31* `version: 1.0-alpha` 61 | 62 | 1.0-alpha发布,加入功能: 63 | 64 | * DNS转发代理,并可缓存结果 65 | * 可配置多个转发服务器 66 | * 增加超时机制 67 | 68 | *2012-12-17* `version: 0.1 ` 69 | 70 | 为了满足模拟邮件发送的需求,开发了第一个版本。可以拦截所有请求到某一IP。从EagelDNS的框架修改而来。 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | BlackHole 2 | ========= 3 | 4 | ### 1. 技术结构 5 | 6 | BlackHole的连接处理部分参考了EagleDNS,使用了反应堆模式。 7 | 8 | BlackHole底层使用dnsjava进行DNS解析,并使用EhCache进行缓存和持久化。 9 | 10 | BlackHole的UDP代理部分使用了纯异步的逻辑,支持同时代理多个外部DNS服务器,并使用最快的响应结果。 11 | 12 | ### 2. 安装 13 | 14 | #### 下载编译版本 15 | 16 | BlackHole的编译后版本保存在[https://github.com/code4craft/blackhole-bin](https://github.com/code4craft/blackhole-bin),直接clone这个项目到某一目录即可。 17 | 18 | git clone https://github.com/code4craft/blackhole-bin.git /usr/local/blackhole 19 | 20 | 你也可以使用自动脚本进行安装: 21 | 22 | curl http://code4craft.github.io/blackhole/install.sh | sh 23 | 24 | #### 从源码编译 25 | 26 | 你也可以使用源码,进行修改和编译。 27 | 28 | git clone https://github.com/code4craft/blackhole.git --recursive 29 | cd blackhole 30 | mvn clean package 31 | sh make.sh 32 | 33 | ### 3. 启动 34 | 35 | 然后通过sudo /usr/local/blackhole/blackhole.sh start可以启动。 36 | 37 | 如果启动时提示53端口被占用,可以查看一下是否已在本地开启其他dns服务。 38 | 39 | Ubuntu下默认开启了dnsmasq,如果启动时提示53端口被占用,可以查看dnsmasq是否已开启: 40 | 41 | ps -ef | grep dnsmasq 42 | 43 | 禁用dnsmasq的方法:修改/etc/NetworkManager/NetworkManager.conf,注释掉dns=dnsmasq即可。 44 | 45 | ### 4. 配置BlackHole: 46 | 47 | 如果你没有将BlackHole安装到/usr/local/blackhole,则需要修改blackhole.sh,将HOME_DIR更改为你的安装目录。 48 | 49 | BlackHole目前有两个配置文件,分别是**config/blackhole.conf**和**config/zones**。 50 | **BlackHole的配置文件都是修改后动态生效的!** 51 | 52 | #### conf/blackhole.conf 53 | 54 | 是blackhole的主要配置文件。 55 | 56 | 配置格式如下: 57 | 58 | key=value 59 | 60 | 例如: 61 | 62 | DNS=192.168.0.1 63 | 64 | 目前可配置项如下: 65 |  66 | * **DNS** 67 | 68 | BlackHole并没有递归查找DNS的功能,如果遇到未在本地配置的域名请求,它会做一个UDP代理的工作,将请求发向一个已有的DNS服务器,并返回这个DNS服务器的结果。 69 | 70 | 支持多个DNS服务器配置,BlackHole会优先采用较前面的配置的结果。**如果你所在的内网配置了DNS拦截,将你的内网DNS服务器放到第一位可以保证这些拦截优先生效。** 71 | 72 | * **DNS_TIMEOUT** 73 | 74 | 请求外部DNS的超时时间,单位为毫秒。 75 | 76 | * **CACHE** 77 | 78 | 转发模式下,是否对DNS结果进行缓存。默认是true。 79 | 80 | * **CACHE_EXPIRE** 81 | 82 | 设置缓存的过期时间,单位是秒。不设置此项或设置为0,则使用默认值。默认缓存的失效时间是DNS结果的TLL值。 83 | 84 | * **TTL** 85 | 86 | BlackHole拦截的DNS请求的过期时间,单位是秒。 87 | 88 | * **LOG** 89 | 90 | 日志的等级。可选配置为('DEBUG','INFO','WARN'),对应log4j的等级。开启'DEBUG'级别的日志会显示每次请求和应答,但是会大大降低吞吐量。 91 | 92 | * **SAFE_BOX** 93 | 94 | 是否开启反DNS污染功能。关闭此项可增加吞吐量。 95 | 96 | * **FAKE_DNS** 97 | 98 | 伪造的DNS服务器,用于检测DNS污染。一般无须更改。 99 | 100 | #### conf/zones 101 | 102 | zones是DNS系统的域名配置文件。BlackHole简化了zones的配置,只支持A记录(因为作者觉得A记录够用了!)。 103 | 104 | BlackHole的配置跟Hosts文件是一样的,但是支持通配符"*"。 105 | 106 | 例如: 107 | 108 | 127.0.0.1 *.codecraf.us 109 | 110 | *可以匹配任意长度的任意字符(包括'.')。这项配置可以将所有以.codecraf.us形式结尾的域名全部指向127.0.0.1。 111 | 112 | 当然,你也可以这样配置: 113 | 114 | 127.0.0.1 * 115 | 116 | 这表示把所有请求导向本地。这个配置在某些场合(例如测试邮件发送服务)会有用。 117 | 118 | BlackHole还支持NS记录的配置。NS记录的意思是,对于某些域名的请求,总是向某个DNS服务器查找结果。例如,你可以使用组合配置: 119 | 120 | 173.194.72.103 *.google.com 121 | NS 8.8.8.8 docs.google.com 122 | 123 | 这两项配置的意思是:将*.google.com的地址都指向173.194.72.103,但是对于docs.google.com域名,向8.8.8.8进行查询并作为最终结果。 124 | 125 | 如果两条规则出现冲突,前面的规则会生效。 126 | 127 | ### 5. 动态管理BlackHole: 128 | 129 | blackhole的监控模块使用了作者的另一个开源项目[wifesays](https://github.com/flashsword20/wifesays)。wifesays是一个简单的Java进程内TCP服务器,使用40310端口作为监控端口,基于TCP协议上封装了一些简单的文本命令。 130 | 131 | 可以使用 132 | 133 | java -jar wifesays.jar -cCOMMAND 134 | 135 | 来通知BlackHole。你也可以直接用telnet来管理BlackHole: 136 | 137 | telnet 127.0.0.1 40310 138 | >COMMAND 139 | 140 | COMMAND为命令。目前支持的命令为: 141 | 142 | * reload 143 | 144 | 重新读取配置,包括zones和blackhole.conf。 145 | 146 | * shutdown 147 | 148 | 退出。 149 | 150 | * clear_cache 151 | 152 | 清除缓存(仅当使用缓存时有效)。 153 | 154 | * stat_cache 155 | 156 | 显示缓存状态。 157 | 158 | * dump_cache 159 | 160 | 将缓存输出到文件cache.dump,便于调试。 161 | 162 | 例如: 163 | 164 | 重新读取配置文件的命令为: 165 | 166 | java -jar wifesays.jar -creload 167 | 168 | 这些操作都集成到了blackhole.sh脚本中。支持SHELL的系统可以使用以下命令快速操作: 169 | 170 | blackhole.sh {start|stop|restart|reload|zones|config|cache} 171 | 172 | ### 6. 协议 173 | 174 | BlackHole的连接部分参考了EagleDNS的代码,遵守LGPLv3协议。 175 | 176 | 作者邮箱: 177 | code4crafter@gmail.com 178 | -------------------------------------------------------------------------------- /server/benchmark-all-versions: -------------------------------------------------------------------------------- 1 | ###1.1.2 2 | Queries per second: 17020.533091 qps 3 | 4 | Queries per second: 17968.598926 qps 5 | 6 | Queries per second: 9929.740407 qps 7 | 8 | Queries per second: 6730.895729 qps 9 | 10 | Queries per second: 13743.787636 qps 11 | 12 | Queries per second: 17946.626016 qps 13 | 14 | Queries per second: 15181.713176 qps 15 | 16 | Queries per second: 8420.535018 qps 17 | 18 | Queries per second: 7355.708610 qps 19 | 20 | Queries per second: 7070.555627 qps 21 | 22 | Avg: 12137 23 | 24 | -------- 25 | 26 | ###1.1.3 27 | Queries per second: 19214.104324 qps 28 | 29 | Queries per second: 8840.774761 qps 30 | 31 | Queries per second: 12835.476201 qps 32 | 33 | Queries per second: 20727.255323 qps 34 | 35 | Queries per second: 17055.128750 qps 36 | 37 | Queries per second: 20098.186952 qps 38 | 39 | Queries per second: 8976.868585 qps 40 | 41 | Queries per second: 16209.558405 qps 42 | 43 | Queries per second: 18838.347946 qps 44 | 45 | Queries per second: 10039.901489 qps 46 | 47 | Avg: 15284 48 | 49 | -------- 50 | 51 | ###1.2.1 52 | memory: 136.7M 53 | threads: 58 54 | 55 | ------- 56 | 57 | ###1.2.2 58 | memory: 129.3M 59 | threads: 34 -------------------------------------------------------------------------------- /server/benchmark-other-dns-server: -------------------------------------------------------------------------------- 1 | Benchmark 2 | 3 | ----------------------------------------------------- 4 | ----------------------------------------------------- 5 | 6 | Using queryperf in bind 9.9.3 7 | 8 | Environment:Mac Air MD231CH/A 9 | 10 | CPU: 1.8Ghz Intel Core i5 11 | 12 | ----------------------------------------------------- 13 | 14 | blackhole 1.2.1 15 | 16 | Memory Usage: 140M 17 | 18 | Statistics: 19 | 20 | Parse input file: once 21 | Ended due to: reaching end of file 22 | 23 | Queries sent: 141300 queries 24 | Queries completed: 141300 queries 25 | Queries lost: 0 queries 26 | Queries delayed(?): 0 queries 27 | 28 | RTT max: 0.214731 sec 29 | RTT min: 0.000059 sec 30 | RTT average: 0.000282 sec 31 | RTT std deviation: 0.001746 sec 32 | RTT out of range: 0 queries 33 | 34 | Percentage completed: 100.00% 35 | Percentage lost: 0.00% 36 | 37 | Started at: Sat Jun 22 19:39:18 2013 38 | Finished at: Sat Jun 22 19:39:21 2013 39 | Ran for: 2.643923 seconds 40 | 41 | Queries per second: 53443.311322 qps 42 | 43 | ----------------------------------------------------- 44 | 45 | BIND 9.9.3 46 | 47 | Memory Usage: 37M 48 | 49 | Statistics: 50 | 51 | Parse input file: once 52 | Ended due to: reaching end of file 53 | 54 | Queries sent: 141300 queries 55 | Queries completed: 141300 queries 56 | Queries lost: 0 queries 57 | Queries delayed(?): 0 queries 58 | 59 | RTT max: 0.231236 sec 60 | RTT min: 0.000018 sec 61 | RTT average: 0.000421 sec 62 | RTT std deviation: 0.001912 sec 63 | RTT out of range: 0 queries 64 | 65 | Percentage completed: 100.00% 66 | Percentage lost: 0.00% 67 | 68 | Started at: Sat Jun 22 20:24:01 2013 69 | Finished at: Sat Jun 22 20:24:04 2013 70 | Ran for: 3.473272 seconds 71 | 72 | Queries per second: 40682.100337 qps 73 | 74 | ----------------------------------------------------- 75 | 76 | pdnsd 77 | 78 | Memory Usage: 1.5M 79 | 80 | Statistics: 81 | 82 | Parse input file: once 83 | Ended due to: reaching end of file 84 | 85 | Queries sent: 141300 queries 86 | Queries completed: 141300 queries 87 | Queries lost: 0 queries 88 | Queries delayed(?): 0 queries 89 | 90 | RTT max: 0.559270 sec 91 | RTT min: 0.000135 sec 92 | RTT average: 0.001076 sec 93 | RTT std deviation: 0.006703 sec 94 | RTT out of range: 0 queries 95 | 96 | Percentage completed: 100.00% 97 | Percentage lost: 0.00% 98 | 99 | Started at: Sat Jun 22 19:56:29 2013 100 | Finished at: Sat Jun 22 19:56:37 2013 101 | Ran for: 7.891417 seconds 102 | 103 | Queries per second: 17905.529514 qps -------------------------------------------------------------------------------- /server/blackhole.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | HOME_DIR=/usr/local/blackhole 3 | PATH=$PATH:$HOME_DIR 4 | export PATH 5 | JVM_OPTION="" 6 | 7 | function doCache() 8 | { 9 | case "$1" in 10 | stat) 11 | java -jar $HOME_DIR/lib/wifesays-1.0.0-alpha.jar -cstat_cache 12 | ;; 13 | dump) 14 | echo "Dump cache to $HOME_DIR/cache.dump" 15 | java -jar $HOME_DIR/lib/wifesays-1.0.0-alpha.jar -cdump_cache > /dev/null 16 | ;; 17 | clear) 18 | echo "Clear cache" 19 | java -jar $HOME_DIR/lib/wifesays-1.0.0-alpha.jar -cclear_cache 20 | ;; 21 | *) 22 | echo "Usage: $0 cache {dump|clear|stat}" 23 | ;; 24 | esac 25 | } 26 | 27 | case "$1" in 28 | start) 29 | echo "Starting blackhole..." 30 | java -jar ${JVM_OPTION} -Djava.io.tmpdir="$HOME_DIR/cache" $HOME_DIR/blackhole.jar -d"$HOME_DIR">> $HOME_DIR/log & 31 | ;; 32 | stop) 33 | echo "Stopping blackhole" 34 | java -jar $HOME_DIR/lib/wifesays-1.0.0-alpha.jar -cshutdown > /dev/null 35 | ;; 36 | cache) 37 | doCache $2 38 | ;; 39 | restart) 40 | echo "Stopping blackhole..." 41 | java -jar $HOME_DIR/lib/wifesays-1.0.0-alpha.jar -cshutdown > /dev/null; 42 | sleep 2; 43 | echo "Starting blackhole..." 44 | java -jar ${JVM_OPTION} -Djava.io.tmpdir="$HOME_DIR/cache" $HOME_DIR/blackhole.jar -d"$HOME_DIR">> $HOME_DIR/log & 45 | ;; 46 | reload) 47 | echo "Reloading blackhole" 48 | java -jar $HOME_DIR/lib/wifesays-1.0.0-alpha.jar -creload > /dev/null 49 | ;; 50 | zones) 51 | vi $HOME_DIR/config/zones 52 | java -jar $HOME_DIR/lib/wifesays-1.0.0-alpha.jar -creload > /dev/null 53 | ;; 54 | config) 55 | vi $HOME_DIR/config/blackhole.conf 56 | java -jar $HOME_DIR/lib/wifesays-1.0.0-alpha.jar -creload > /dev/null 57 | ;; 58 | *) 59 | echo "Usage: $0 {start|stop|reload|zones|config|restart|cache}" 60 | ;; 61 | esac 62 | 63 | exit 0 64 | -------------------------------------------------------------------------------- /server/config/blackhole.conf: -------------------------------------------------------------------------------- 1 | #######################################Configure file of blackhole##################################### 2 | 3 | #######################################important##################################### 4 | #External dns. You can add more reliable dns. 5 | dns=8.8.8.8 6 | #Timeout of external dns in millisecond. 7 | dns_timeout=3000 8 | 9 | #ttl of intercepted answers.what is ttl? see http://en.wikipedia.org/wiki/Time_to_live#DNS_records 10 | ttl=1 11 | 12 | #Whether use ehcache for external dns records.When it is changed, the program must be restarted to take effort. 13 | cache=true 14 | 15 | #Cache expire time in seconds. Use ttl of respective DNS answer as default. 16 | cache_expire=864000 17 | 18 | #fake dns server.Used to detect dns poison. 19 | fake_dns=144.223.234.234 20 | 21 | #When turn it on, we will look for a valid address for the domains which are poisoned and save it to file "safebox" 22 | safe_box=true 23 | 24 | #Num of threads for process request. One thread is enough for local usage. Set it to 10 if you use it as a server for many users. 25 | thread_num=1 26 | 27 | #log4j level,there are 3 levels:[debug,info,warn] 28 | log=info 29 | -------------------------------------------------------------------------------- /server/config/zones: -------------------------------------------------------------------------------- 1 | ###################### Black Hole Zones config file ############################ 2 | # 3 | # Syntax: 4 | # address domain [type] 5 | # like: 127.0.0.1 codecraft.us 6 | # Char "*" matches all chars with any length. 7 | # Lines starting with "#" will be considered as comments. 8 | # Type field is optional.Line without type will be considered supporting all types. 9 | # If more than one line works for a query, the first line will have effect. 10 | # 11 | # Samples: 12 | # 13 | # 127.0.0.1 codecraft.us 14 | # response all domain query end with "codecraft.us" to 127.0.0.1 15 | # 16 | # 127.0.0.1 * 17 | # response all query with all domain to 127.0.0.1 18 | # 19 | # 127.0.0.1 flashsword20.*.* 20 | # response all query like "flashsword20.diandian.com" "flashsword20.github.com" 21 | # 22 | -------------------------------------------------------------------------------- /server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | us.codecraft 6 | blackhole 7 | 1.2.2 8 | blackhole 9 | A simple DNS authoritative server.It can easily be configured to intercept some kind of request to one address. 10 | 11 | 3.1.1.RELEASE 12 | 13 | 14 | 15 | dnsjava 16 | dnsjava 17 | 2.1.1 18 | 19 | 20 | junit 21 | junit 22 | 4.7 23 | test 24 | 25 | 26 | us.codecraft 27 | wifesays 28 | 1.0.0-alpha 29 | 30 | 31 | net.sf.ehcache 32 | ehcache-core 33 | 2.6.2 34 | 35 | 36 | org.slf4j 37 | slf4j-log4j12 38 | 1.7.1 39 | 40 | 41 | org.apache.commons 42 | commons-lang3 43 | 3.1 44 | 45 | 46 | 47 | 48 | 49 | org.apache.maven.plugins 50 | maven-dependency-plugin 51 | 52 | 53 | copy-dependencies 54 | package 55 | 56 | copy-dependencies 57 | 58 | 59 | ${project.build.directory}/lib 60 | false 61 | false 62 | true 63 | 64 | 65 | 66 | 67 | 68 | maven-compiler-plugin 69 | 70 | 1.6 71 | 1.6 72 | UTF-8 73 | 74 | 75 | 76 | org.apache.maven.plugins 77 | maven-resources-plugin 78 | 79 | UTF-8 80 | 81 | 82 | 83 | org.apache.maven.plugins 84 | maven-jar-plugin 85 | 86 | 87 | 88 | true 89 | ./lib/ 90 | us.codecraft.blackhole.BlackHole 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/BlackHole.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole; 2 | 3 | import java.io.IOException; 4 | import java.net.UnknownHostException; 5 | 6 | import org.apache.commons.cli.CommandLine; 7 | import org.apache.commons.cli.CommandLineParser; 8 | import org.apache.commons.cli.Option; 9 | import org.apache.commons.cli.Options; 10 | import org.apache.commons.cli.ParseException; 11 | import org.apache.commons.cli.PosixParser; 12 | import org.apache.log4j.Logger; 13 | import org.springframework.context.support.ClassPathXmlApplicationContext; 14 | import org.springframework.stereotype.Component; 15 | 16 | import us.codecraft.blackhole.config.Configure; 17 | import us.codecraft.blackhole.connector.UDPSocketMonitor; 18 | import us.codecraft.blackhole.utils.SpringLocator; 19 | 20 | /** 21 | * Entry of application. aa 22 | * 23 | * @author yihua.huang@dianping.com 24 | * @date Dec 14, 2012 25 | */ 26 | @Component 27 | public class BlackHole { 28 | 29 | private boolean isShutDown = false; 30 | 31 | private static Logger logger = Logger.getLogger(BlackHole.class); 32 | 33 | private UDPSocketMonitor udpSocketMonitor; 34 | 35 | public void start() throws UnknownHostException, IOException { 36 | udpSocketMonitor = SpringLocator.getBean(UDPSocketMonitor.class); 37 | udpSocketMonitor.start(); 38 | } 39 | 40 | private static void parseArgs(String[] args) throws ParseException { 41 | Options options = new Options(); 42 | options.addOption(new Option("d", true, "home path")); 43 | CommandLineParser commandLineParser = new PosixParser(); 44 | CommandLine commandLine = commandLineParser.parse(options, args); 45 | readOptions(commandLine); 46 | } 47 | 48 | private static void readOptions(CommandLine commandLine) { 49 | if (commandLine.hasOption("d")) { 50 | String filename = commandLine.getOptionValue("d"); 51 | Configure.FILE_PATH = filename; 52 | } 53 | } 54 | 55 | /** 56 | * @param args 57 | */ 58 | public static void main(String[] args) { 59 | try { 60 | parseArgs(args); 61 | } catch (ParseException e1) { 62 | logger.warn("parse args error"); 63 | } 64 | SpringLocator.applicationContext = new ClassPathXmlApplicationContext( 65 | "classpath*:/spring/applicationContext*.xml"); 66 | BlackHole blackHole = SpringLocator.getBean(BlackHole.class); 67 | try { 68 | blackHole.start(); 69 | } catch (UnknownHostException e) { 70 | logger.warn("init failed ", e); 71 | } catch (IOException e) { 72 | logger.warn("init failed ", e); 73 | } 74 | while (!blackHole.isShutDown) { 75 | try { 76 | Thread.sleep(10000000); 77 | } catch (InterruptedException e) { 78 | e.printStackTrace(); 79 | } 80 | } 81 | 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/answer/AbstractAnswerHandler.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.answer; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.xbill.DNS.DClass; 5 | import org.xbill.DNS.Record; 6 | import org.xbill.DNS.Section; 7 | import org.xbill.DNS.Type; 8 | import us.codecraft.blackhole.container.Handler; 9 | import us.codecraft.blackhole.container.MessageWrapper; 10 | import us.codecraft.blackhole.utils.RecordBuilder; 11 | 12 | import java.util.List; 13 | import java.util.regex.Matcher; 14 | import java.util.regex.Pattern; 15 | 16 | /** 17 | * @author yihua.huang@dianping.com
18 | * @date: 13-7-14
19 | * Time: 下午4:36
20 | */ 21 | public abstract class AbstractAnswerHandler implements Handler { 22 | 23 | protected abstract List getaAnswerProviders(); 24 | 25 | protected Logger logger = Logger.getLogger(getClass()); 26 | 27 | // b._dns-sd._udp.0.129.37.10.in-addr.arpa. 28 | private final Pattern filterPTRPattern = Pattern 29 | .compile(".*\\.(\\d+\\.\\d+\\.\\d+\\.\\d+\\.in-addr\\.arpa\\.)"); 30 | 31 | private String filterPTRQuery(String query) { 32 | Matcher matcher = filterPTRPattern.matcher(query); 33 | if (matcher.matches()) { 34 | return matcher.group(1); 35 | } else { 36 | return query; 37 | } 38 | } 39 | 40 | /* 41 | * (non-Javadoc) 42 | * 43 | * @see us.codecraft.blackhole.server.Handler#handle(org.xbill.DNS.Message, 44 | * org.xbill.DNS.Message) 45 | */ 46 | @Override 47 | public boolean handle(MessageWrapper request, MessageWrapper response) { 48 | Record question = request.getMessage().getQuestion(); 49 | String query = question.getName().toString(); 50 | int type = question.getType(); 51 | if (type == Type.PTR) { 52 | query = filterPTRQuery(query); 53 | } 54 | // some client will query with any 55 | if (type == Type.ANY) { 56 | type = Type.A; 57 | } 58 | if (logger.isDebugEnabled()) { 59 | logger.debug("query \t" + Type.string(type) + "\t" 60 | + DClass.string(question.getDClass()) + "\t" + query); 61 | } 62 | for (AnswerProvider answerProvider : getaAnswerProviders()) { 63 | String answer = answerProvider.getAnswer(query, type); 64 | if (answer != null) { 65 | try { 66 | Record record = new RecordBuilder() 67 | .dclass(question.getDClass()) 68 | .name(question.getName()).answer(answer).type(type) 69 | .toRecord(); 70 | response.getMessage().addRecord(record, Section.ANSWER); 71 | if (logger.isDebugEnabled()) { 72 | logger.debug("answer\t" + Type.string(type) + "\t" 73 | + DClass.string(question.getDClass()) + "\t" 74 | + answer); 75 | } 76 | response.setHasRecord(true); 77 | return false; 78 | } catch (Throwable e) { 79 | logger.warn("handling exception " + e); 80 | } 81 | } 82 | } 83 | return true; 84 | } 85 | } -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/answer/AnswerPatternProvider.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.answer; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Component; 6 | import org.xbill.DNS.Address; 7 | import org.xbill.DNS.Type; 8 | 9 | /** 10 | * Read the config to domainPatterns and process the request record. 11 | * 12 | * @author yihua.huang@dianping.com 13 | * @date Dec 14, 2012 14 | */ 15 | @Component 16 | public class AnswerPatternProvider implements AnswerProvider { 17 | 18 | private DomainPatternsContainer domainPatternsContainer = new DomainPatternsContainer(); 19 | 20 | private Logger logger = Logger.getLogger(getClass()); 21 | 22 | /** 23 | * When the address configured as "DO_NOTHING",it will not return any 24 | * address. 25 | */ 26 | public static final String DO_NOTHING = "do_nothing"; 27 | private static final String FAKE_MX_PREFIX = "mail."; 28 | private static final String FAKE_CANME_PREFIX = "cname."; 29 | 30 | @Autowired 31 | private TempAnswerProvider tempAnswerContainer; 32 | 33 | /* 34 | * (non-Javadoc) 35 | * 36 | * @see 37 | * us.codecraft.blackhole.answer.AnswerProvider#getAnswer(java.lang.String, 38 | * int) 39 | */ 40 | @Override 41 | public String getAnswer(String query, int type) { 42 | if (type == Type.PTR) { 43 | return null; 44 | } 45 | String ip = domainPatternsContainer.getIp(query); 46 | if (ip == null || ip.equals(DO_NOTHING)) { 47 | return null; 48 | } 49 | if (type == Type.MX) { 50 | String fakeMXHost = fakeMXHost(query); 51 | tempAnswerContainer.add(fakeMXHost, Type.A, ip); 52 | return fakeMXHost; 53 | } 54 | if (type == Type.CNAME) { 55 | String fakeCNAMEHost = fakeCNAMEHost(query); 56 | tempAnswerContainer.add(fakeCNAMEHost, Type.A, ip); 57 | return fakeCNAMEHost; 58 | } 59 | try { 60 | tempAnswerContainer.add(reverseIp(ip), Type.PTR, query); 61 | } catch (Throwable e) { 62 | logger.info("not a ip, ignored"); 63 | } 64 | return ip; 65 | } 66 | 67 | /** 68 | * generate a fake MX host 69 | * 70 | * @param domain 71 | * @return 72 | */ 73 | private String fakeMXHost(String domain) { 74 | return FAKE_MX_PREFIX + domain; 75 | } 76 | 77 | /** 78 | * @param domain 79 | * @return 80 | */ 81 | private String fakeCNAMEHost(String domain) { 82 | return FAKE_CANME_PREFIX + domain; 83 | } 84 | 85 | private String reverseIp(String ip) { 86 | int[] array = Address.toArray(ip); 87 | StringBuilder stringBuilder = new StringBuilder(); 88 | for (int i = array.length - 1; i >= 0; i--) { 89 | stringBuilder.append(array[i] + "."); 90 | } 91 | stringBuilder.append("in-addr.arpa."); 92 | return stringBuilder.toString(); 93 | } 94 | 95 | public void setDomainPatternsContainer(DomainPatternsContainer domainPatternsContainer) { 96 | this.domainPatternsContainer = domainPatternsContainer; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/answer/AnswerProvider.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.answer; 2 | 3 | /** 4 | * Provide the answer.An answerContainer must be registered in 5 | * {@link PreAnswerHandler#regitestProviders()} before it takes effect. 6 | * 7 | * @author yihua.huang@dianping.com 8 | * @date Dec 14, 2012 9 | */ 10 | public interface AnswerProvider { 11 | 12 | /** 13 | * 14 | * @param query 15 | * @param type 16 | * @return 17 | */ 18 | public String getAnswer(String query, int type); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/answer/CustomAnswerPatternProvider.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.answer; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Component; 6 | import org.xbill.DNS.Address; 7 | import org.xbill.DNS.Type; 8 | import us.codecraft.blackhole.context.RequestContext; 9 | import us.codecraft.blackhole.utils.DoubleKeyMap; 10 | 11 | import java.util.HashMap; 12 | import java.util.LinkedHashMap; 13 | import java.util.Map; 14 | import java.util.Map.Entry; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | import java.util.regex.Matcher; 17 | import java.util.regex.Pattern; 18 | 19 | /** 20 | * Read the config to domainPatterns and process the request record. 21 | * 22 | * @author yihua.huang@dianping.com 23 | * @date Dec 14, 2012 24 | */ 25 | @Component 26 | public class CustomAnswerPatternProvider implements AnswerProvider { 27 | 28 | 29 | //TODO:domainPatternsContainer 30 | private volatile DoubleKeyMap domainPatterns = new DoubleKeyMap(new ConcurrentHashMap>(), LinkedHashMap.class); 31 | 32 | private volatile DoubleKeyMap domainTexts = new DoubleKeyMap(new ConcurrentHashMap>(), HashMap.class); 33 | 34 | private Logger logger = Logger.getLogger(getClass()); 35 | 36 | /** 37 | * When the address configured as "DO_NOTHING",it will not return any 38 | * address. 39 | */ 40 | public static final String DO_NOTHING = "do_nothing"; 41 | private static final String FAKE_MX_PREFIX = "mail."; 42 | private static final String FAKE_CANME_PREFIX = "cname."; 43 | 44 | @Autowired 45 | private CustomTempAnswerProvider customTempAnswerProvider; 46 | 47 | /* 48 | * (non-Javadoc) 49 | * 50 | * @see 51 | * us.codecraft.blackhole.answer.AnswerProvider#getAnswer(java.lang.String, 52 | * int) 53 | */ 54 | @Override 55 | public String getAnswer(String query, int type) { 56 | if (type == Type.PTR) { 57 | return null; 58 | } 59 | String clientIp = RequestContext.getClientIp(); 60 | String ip = domainTexts.get(clientIp,query); 61 | if (ip!=null){ 62 | return ip; 63 | } 64 | Map patternsForIp = domainPatterns.get(clientIp); 65 | if (patternsForIp == null) { 66 | return null; 67 | } 68 | for (Entry entry : patternsForIp.entrySet()) { 69 | Matcher matcher = entry.getKey().matcher(query); 70 | if (matcher.find()) { 71 | String answer = entry.getValue(); 72 | if (answer.equals(DO_NOTHING)) { 73 | return null; 74 | } 75 | if (type == Type.MX) { 76 | String fakeMXHost = fakeMXHost(query); 77 | customTempAnswerProvider.add(clientIp, fakeMXHost, Type.A, answer); 78 | return fakeMXHost; 79 | } 80 | if (type == Type.CNAME) { 81 | String fakeCNAMEHost = fakeCNAMEHost(query); 82 | customTempAnswerProvider.add(clientIp, fakeCNAMEHost, Type.A, answer); 83 | return fakeCNAMEHost; 84 | } 85 | try { 86 | customTempAnswerProvider.add(clientIp, reverseIp(answer), Type.PTR, query); 87 | } catch (Throwable e) { 88 | logger.info("not a ip, ignored"); 89 | } 90 | return answer; 91 | } 92 | } 93 | return null; 94 | } 95 | 96 | /** 97 | * generate a fake MX host 98 | * 99 | * @param domain 100 | * @return 101 | */ 102 | private String fakeMXHost(String domain) { 103 | return FAKE_MX_PREFIX + domain; 104 | } 105 | 106 | /** 107 | * @param domain 108 | * @return 109 | */ 110 | private String fakeCNAMEHost(String domain) { 111 | return FAKE_CANME_PREFIX + domain; 112 | } 113 | 114 | private String reverseIp(String ip) { 115 | int[] array = Address.toArray(ip); 116 | StringBuilder stringBuilder = new StringBuilder(); 117 | for (int i = array.length - 1; i >= 0; i--) { 118 | stringBuilder.append(array[i] + "."); 119 | } 120 | stringBuilder.append("in-addr.arpa."); 121 | return stringBuilder.toString(); 122 | } 123 | 124 | public void setDomainPatterns(DoubleKeyMap domainPatterns) { 125 | this.domainPatterns = domainPatterns; 126 | } 127 | 128 | public DoubleKeyMap getDomainPatterns() { 129 | return domainPatterns; 130 | } 131 | 132 | public DoubleKeyMap getDomainTexts() { 133 | return domainTexts; 134 | } 135 | 136 | public void setDomainTexts(DoubleKeyMap domainTexts) { 137 | this.domainTexts = domainTexts; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/answer/CustomTempAnswerProvider.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.answer; 2 | 3 | import org.springframework.stereotype.Component; 4 | import us.codecraft.blackhole.context.RequestContext; 5 | import us.codecraft.blackhole.utils.DoubleKeyMap; 6 | 7 | import java.util.Map; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | /** 11 | * @author yihua.huang@dianping.com 12 | * @date Dec 14, 2012 13 | */ 14 | @Component 15 | public class CustomTempAnswerProvider implements AnswerProvider { 16 | 17 | private Map> container; 18 | 19 | public CustomTempAnswerProvider() { 20 | container = new ConcurrentHashMap>(); 21 | } 22 | 23 | /* 24 | * (non-Javadoc) 25 | * 26 | * @see 27 | * us.codecraft.blackhole.answer.AnswerProvider#getAnswer(java.lang.String, 28 | * int) 29 | */ 30 | @Override 31 | public String getAnswer(String query, int type) { 32 | String ip = RequestContext.getClientIp(); 33 | DoubleKeyMap stringIntegerStringDoubleKeyMap = container.get(ip); 34 | if (stringIntegerStringDoubleKeyMap==null){ 35 | return null; 36 | } 37 | return stringIntegerStringDoubleKeyMap.get(query, type); 38 | } 39 | 40 | public void add(String clientIp,String query, int type, String answer) { 41 | DoubleKeyMap stringIntegerStringDoubleKeyMap = container.get(clientIp); 42 | if (stringIntegerStringDoubleKeyMap==null){ 43 | stringIntegerStringDoubleKeyMap = new DoubleKeyMap(ConcurrentHashMap.class); 44 | container.put(clientIp,stringIntegerStringDoubleKeyMap); 45 | } 46 | stringIntegerStringDoubleKeyMap.put(query, type, answer); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/answer/DomainPatternsContainer.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.answer; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | /** 9 | * @author yihua.huang@dianping.com
10 | * @date: 13-7-15
11 | * Time: 上午8:18
12 | */ 13 | public class DomainPatternsContainer { 14 | 15 | private volatile Map domainPatterns = new HashMap(); 16 | 17 | private volatile Map domainTexts = new HashMap(); 18 | 19 | public Map getDomainPatterns() { 20 | return domainPatterns; 21 | } 22 | 23 | public void setDomainPatterns(Map domainPatterns) { 24 | this.domainPatterns = domainPatterns; 25 | } 26 | 27 | public Map getDomainTexts() { 28 | return domainTexts; 29 | } 30 | 31 | public void setDomainTexts(Map domainTexts) { 32 | this.domainTexts = domainTexts; 33 | } 34 | 35 | public String getIp(String domain) { 36 | String ip = domainTexts.get(domain); 37 | if (ip != null) { 38 | return ip; 39 | } 40 | for (Map.Entry entry : domainPatterns.entrySet()) { 41 | Matcher matcher = entry.getKey().matcher(domain); 42 | if (matcher.find()) { 43 | return entry.getValue(); 44 | } 45 | } 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/answer/PostAnswerHandler.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.answer; 2 | 3 | import org.springframework.beans.factory.InitializingBean; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.util.LinkedList; 7 | import java.util.List; 8 | 9 | /** 10 | * @author yihua.huang@dianping.com 11 | * @date Dec 14, 2012 12 | */ 13 | @Component("postAnswerHandler") 14 | public class PostAnswerHandler extends AbstractAnswerHandler implements InitializingBean { 15 | 16 | private List answerProviders; 17 | 18 | /* 19 | * (non-Javadoc) 20 | * 21 | * @see 22 | * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 23 | */ 24 | @Override 25 | public void afterPropertiesSet() throws Exception { 26 | regitestProviders(); 27 | } 28 | 29 | public void regitestProviders() { 30 | answerProviders = new LinkedList(); 31 | } 32 | 33 | @Override 34 | protected List getaAnswerProviders() { 35 | return answerProviders; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/answer/PreAnswerHandler.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.answer; 2 | 3 | import org.springframework.beans.factory.InitializingBean; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | 10 | /** 11 | * @author yihua.huang@dianping.com 12 | * @date Dec 14, 2012 13 | */ 14 | @Component("preAnswerHandler") 15 | public class PreAnswerHandler extends AbstractAnswerHandler implements InitializingBean { 16 | 17 | private List answerProviders; 18 | 19 | @Autowired 20 | private CustomTempAnswerProvider customTempAnswerProvider; 21 | 22 | @Autowired 23 | private CustomAnswerPatternProvider customAnswerPatternProvider; 24 | 25 | @Autowired 26 | private AnswerPatternProvider answerPatternProvider; 27 | 28 | @Autowired 29 | private TempAnswerProvider tempAnswerContainer; 30 | 31 | @Autowired 32 | private SafeHostAnswerProvider safeHostAnswerProvider; 33 | 34 | 35 | /* 36 | * (non-Javadoc) 37 | * 38 | * @see 39 | * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 40 | */ 41 | @Override 42 | public void afterPropertiesSet() throws Exception { 43 | regitestProviders(); 44 | } 45 | 46 | public void regitestProviders() { 47 | answerProviders = new LinkedList(); 48 | answerProviders.add(customTempAnswerProvider); 49 | answerProviders.add(customAnswerPatternProvider); 50 | answerProviders.add(tempAnswerContainer); 51 | answerProviders.add(answerPatternProvider); 52 | answerProviders.add(safeHostAnswerProvider); 53 | } 54 | 55 | @Override 56 | protected List getaAnswerProviders() { 57 | return answerProviders; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/answer/SafeHostAnswerProvider.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.answer; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Component; 6 | import org.xbill.DNS.Type; 7 | import us.codecraft.blackhole.antipollution.SafeHostManager; 8 | import us.codecraft.blackhole.config.Configure; 9 | 10 | /** 11 | * @author yihua.huang@dianping.com 12 | * @date Feb 20, 2013 13 | */ 14 | @Component 15 | public class SafeHostAnswerProvider implements AnswerProvider { 16 | 17 | @Autowired 18 | private SafeHostManager safeBoxService; 19 | 20 | @Autowired 21 | private Configure configure; 22 | 23 | /* 24 | * (non-Javadoc) 25 | * 26 | * @see 27 | * us.codecraft.blackhole.answer.AnswerProvider#getAnswer(java.lang.String, 28 | * int) 29 | */ 30 | @Override 31 | public String getAnswer(String query, int type) { 32 | if (!configure.isEnableSafeBox()) { 33 | return null; 34 | } 35 | if (type == Type.A || type == Type.AAAA) { 36 | return safeBoxService.get(StringUtils.removeEnd(query, ".")); 37 | } 38 | return null; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/answer/TempAnswerProvider.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.answer; 2 | 3 | import org.springframework.stereotype.Component; 4 | import us.codecraft.blackhole.utils.DoubleKeyMap; 5 | 6 | import java.util.concurrent.ConcurrentHashMap; 7 | 8 | /** 9 | * @author yihua.huang@dianping.com 10 | * @date Dec 14, 2012 11 | */ 12 | @Component 13 | public class TempAnswerProvider implements AnswerProvider { 14 | 15 | private DoubleKeyMap container; 16 | 17 | public TempAnswerProvider() { 18 | container = new DoubleKeyMap( 19 | ConcurrentHashMap.class); 20 | } 21 | 22 | /* 23 | * (non-Javadoc) 24 | * 25 | * @see 26 | * us.codecraft.blackhole.answer.AnswerProvider#getAnswer(java.lang.String, 27 | * int) 28 | */ 29 | @Override 30 | public String getAnswer(String query, int type) { 31 | return container.get(query, type); 32 | } 33 | 34 | public void add(String query, int type, String answer) { 35 | container.put(query, type, answer); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/antipollution/BlackListManager.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.antipollution; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.springframework.beans.factory.InitializingBean; 5 | import org.springframework.stereotype.Component; 6 | import org.xbill.DNS.Message; 7 | import us.codecraft.blackhole.config.Configure; 8 | import us.codecraft.wifesays.me.ShutDownAble; 9 | import us.codecraft.wifesays.me.StandReadyWorker; 10 | 11 | import java.io.*; 12 | import java.util.HashSet; 13 | import java.util.Map; 14 | import java.util.Set; 15 | import java.util.concurrent.ConcurrentHashMap; 16 | import java.util.concurrent.locks.ReentrantReadWriteLock; 17 | 18 | /** 19 | * @author yihua.huang@dianping.com 20 | * @date Feb 19, 2013 21 | */ 22 | @Component 23 | public class BlackListManager extends StandReadyWorker implements 24 | InitializingBean, ShutDownAble { 25 | 26 | private Logger logger = Logger.getLogger(getClass()); 27 | 28 | private Map> invalidAddresses = new ConcurrentHashMap>(); 29 | 30 | private Set blacklist = new HashSet(); 31 | 32 | private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 33 | 34 | private static final String FLUSH_CMD = "flush"; 35 | 36 | public void registerInvalidAddress(Message query, String address) { 37 | String questionName = query.getQuestion().getName().toString(); 38 | logger.info("register error address " + address + " for query " 39 | + questionName); 40 | Set questionNames = invalidAddresses.get(address); 41 | if (questionNames == null) { 42 | questionNames = new HashSet(); 43 | invalidAddresses.put(address, questionNames); 44 | } 45 | questionNames.add(questionName); 46 | if (questionNames.size() >= 2) { 47 | try { 48 | readWriteLock.writeLock().lock(); 49 | blacklist.add(address); 50 | } finally { 51 | readWriteLock.writeLock().unlock(); 52 | } 53 | } 54 | } 55 | 56 | public void addToBlacklist(String address) { 57 | try { 58 | readWriteLock.writeLock().lock(); 59 | blacklist.add(address); 60 | } finally { 61 | readWriteLock.writeLock().unlock(); 62 | } 63 | } 64 | 65 | public boolean inBlacklist(String address) { 66 | try { 67 | readWriteLock.readLock().lock(); 68 | return blacklist.contains(address); 69 | } finally { 70 | readWriteLock.readLock().unlock(); 71 | } 72 | } 73 | 74 | public void flushToFile(String filename) throws IOException { 75 | PrintWriter writer = new PrintWriter(new File(filename)); 76 | for (String address : blacklist) { 77 | writer.println(address); 78 | } 79 | writer.close(); 80 | } 81 | 82 | public void loadFromFile(String filename) throws IOException { 83 | BufferedReader bufferedReader = new BufferedReader(new FileReader( 84 | new File(filename))); 85 | String line = null; 86 | while ((line = bufferedReader.readLine()) != null) { 87 | line = line.trim(); 88 | if (logger.isDebugEnabled()) { 89 | logger.debug("load blacklist address " + line); 90 | } 91 | blacklist.add(line); 92 | } 93 | bufferedReader.close(); 94 | } 95 | 96 | /* 97 | * (non-Javadoc) 98 | * 99 | * @see us.codecraft.wifesays.me.ShutDownAble#shutDown() 100 | */ 101 | @Override 102 | public void shutDown() { 103 | String filename = Configure.FILE_PATH + "/blacklist"; 104 | try { 105 | flushToFile(filename); 106 | } catch (IOException e) { 107 | logger.warn("write to file " + filename + " error! " + e); 108 | } 109 | } 110 | 111 | /* 112 | * (non-Javadoc) 113 | * 114 | * @see 115 | * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 116 | */ 117 | @Override 118 | public void afterPropertiesSet() throws Exception { 119 | String filename = Configure.FILE_PATH + "/blacklist"; 120 | try { 121 | loadFromFile(filename); 122 | } catch (IOException e) { 123 | logger.warn("load file " + filename + " error! " + e); 124 | } 125 | } 126 | 127 | /* 128 | * (non-Javadoc) 129 | * 130 | * @see 131 | * us.codecraft.wifesays.me.StandReady#doWhatYouShouldDo(java.lang.String) 132 | */ 133 | @Override 134 | public String doWhatYouShouldDo(String whatWifeSays) { 135 | if (FLUSH_CMD.equalsIgnoreCase(whatWifeSays)) { 136 | String filename = Configure.FILE_PATH + "/blacklist"; 137 | try { 138 | flushToFile(filename); 139 | } catch (IOException e) { 140 | logger.warn("write to file " + filename + " error! " + e); 141 | } 142 | return "SUCCESS"; 143 | } 144 | return null; 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/antipollution/SafeHostManager.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.antipollution; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.springframework.beans.factory.InitializingBean; 5 | import org.springframework.stereotype.Component; 6 | import us.codecraft.blackhole.config.Configure; 7 | import us.codecraft.wifesays.me.ShutDownAble; 8 | import us.codecraft.wifesays.me.StandReadyWorker; 9 | 10 | import java.io.*; 11 | import java.util.Map; 12 | import java.util.Map.Entry; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | 15 | /** 16 | * @author yihua.huang@dianping.com 17 | * @date Feb 20, 2013 18 | */ 19 | @Component 20 | public class SafeHostManager extends StandReadyWorker implements 21 | InitializingBean, ShutDownAble { 22 | 23 | private Logger logger = Logger.getLogger(getClass()); 24 | 25 | private Map poisons = new ConcurrentHashMap(); 26 | 27 | private Map answers = new ConcurrentHashMap(); 28 | 29 | private static final String FLUSH_CMD = "flush"; 30 | 31 | public void flushToFile(String filename) throws IOException { 32 | PrintWriter writer = new PrintWriter(new File(filename)); 33 | for (Entry address : answers.entrySet()) { 34 | writer.println(address.getValue() + "\t" + address.getKey()); 35 | } 36 | filename.charAt(1); 37 | writer.close(); 38 | } 39 | 40 | public void loadFromFile(String filename) throws IOException { 41 | BufferedReader bufferedReader = new BufferedReader(new FileReader( 42 | new File(filename))); 43 | String line = null; 44 | while ((line = bufferedReader.readLine()) != null) { 45 | line = line.trim(); 46 | if (line.startsWith("#")) { 47 | break; 48 | } 49 | String[] split = line.split("\\s"); 50 | if (split.length <= 1) { 51 | logger.info("error record \"" + line + "\", ignored."); 52 | } 53 | answers.put(split[1], split[0]); 54 | poisons.put(split[1], Boolean.TRUE); 55 | if (logger.isDebugEnabled()) { 56 | logger.debug("load blacklist address " + line); 57 | } 58 | } 59 | bufferedReader.close(); 60 | } 61 | 62 | public void add(String domain, String address) { 63 | answers.put(domain, address); 64 | } 65 | 66 | public boolean isPoisoned(String domain) { 67 | Boolean poisoned = poisons.get(domain); 68 | if (poisoned == null) { 69 | return false; 70 | } 71 | return poisoned; 72 | } 73 | 74 | public void setPoisoned(String domain) { 75 | poisons.put(domain, Boolean.TRUE); 76 | } 77 | 78 | public String get(String domain) { 79 | return answers.get(domain); 80 | } 81 | 82 | /* 83 | * (non-Javadoc) 84 | * 85 | * @see us.codecraft.wifesays.me.ShutDownAble#shutDown() 86 | */ 87 | @Override 88 | public void shutDown() { 89 | String filename = Configure.FILE_PATH + "/safehost"; 90 | try { 91 | flushToFile(filename); 92 | } catch (IOException e) { 93 | logger.warn("write to file " + filename + " error! " + e); 94 | } 95 | } 96 | 97 | /* 98 | * (non-Javadoc) 99 | * 100 | * @see 101 | * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 102 | */ 103 | @Override 104 | public void afterPropertiesSet() throws Exception { 105 | String filename = Configure.FILE_PATH + "/safehost"; 106 | try { 107 | loadFromFile(filename); 108 | } catch (IOException e) { 109 | logger.warn("load file " + filename + " error! " + e); 110 | } 111 | } 112 | 113 | /* 114 | * (non-Javadoc) 115 | * 116 | * @see 117 | * us.codecraft.wifesays.me.StandReady#doWhatYouShouldDo(java.lang.String) 118 | */ 119 | @Override 120 | public String doWhatYouShouldDo(String whatWifeSays) { 121 | if (FLUSH_CMD.equalsIgnoreCase(whatWifeSays)) { 122 | String filename = Configure.FILE_PATH + "/safehost"; 123 | try { 124 | flushToFile(filename); 125 | } catch (IOException e) { 126 | logger.warn("write to file " + filename + " error! " + e); 127 | } 128 | return "SUCCESS"; 129 | } 130 | return null; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/cache/CacheClient.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.cache; 2 | 3 | /** 4 | * @author yihua.huang@dianping.com 5 | * @date Dec 19, 2012 6 | */ 7 | public interface CacheClient { 8 | 9 | /** 10 | * If "cache" configure is turned off, it can get the elemets already in 11 | * cache. 12 | * 13 | * @param key 14 | * @param value 15 | * @param expireTime 16 | * in s 17 | * @return 18 | */ 19 | boolean set(String key, T value, int expireTime); 20 | 21 | /** 22 | * If "cache" configure is turned off, it can't be setted and return false. 23 | * 24 | * @param key 25 | * @return 26 | */ 27 | T get(String key); 28 | 29 | /** 30 | * init cache 31 | * 32 | * @throws Exception 33 | */ 34 | void init() throws Exception; 35 | 36 | void clearCache(); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/cache/CacheManager.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.cache; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.springframework.beans.factory.InitializingBean; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Qualifier; 7 | import org.springframework.stereotype.Component; 8 | import org.xbill.DNS.Message; 9 | import org.xbill.DNS.Section; 10 | import us.codecraft.blackhole.config.Configure; 11 | import us.codecraft.blackhole.utils.RecordUtils; 12 | 13 | import java.util.concurrent.ExecutorService; 14 | import java.util.concurrent.Executors; 15 | 16 | /** 17 | * @author yihua.huang@dianping.com 18 | * @date Dec 19, 2012 19 | */ 20 | @Component 21 | public class CacheManager implements InitializingBean { 22 | 23 | @Autowired 24 | private Configure configure; 25 | 26 | private volatile ExecutorService cacheSaveExecutors; 27 | 28 | private Logger logger = Logger.getLogger(getClass()); 29 | 30 | @Qualifier("EhcacheClient") 31 | @Autowired 32 | private CacheClient cacheClient; 33 | 34 | public byte[] getResponseFromCache(Message query) { 35 | if (!configure.isUseCache()) { 36 | return null; 37 | } 38 | UDPPackage udpPackage = cacheClient 39 | .get(getCacheKey(query)); 40 | if (udpPackage == null) { 41 | return null; 42 | } 43 | byte[] bytes = udpPackage.getBytes(query.getHeader().getID()); 44 | return bytes; 45 | } 46 | 47 | public boolean set(Message query, T value, int expireTime) { 48 | return cacheClient.set(getCacheKey(query), value, expireTime); 49 | } 50 | 51 | public T get(Message query) { 52 | return cacheClient.get(getCacheKey(query)); 53 | } 54 | 55 | public boolean set(String key, T value, int expireTime) { 56 | return cacheClient.set(key, value, expireTime); 57 | } 58 | 59 | public T get(String key) { 60 | return cacheClient.get(key); 61 | } 62 | 63 | public String getCacheKey(Message query) { 64 | return RecordUtils.recordKey(query.getQuestion()); 65 | } 66 | 67 | private int minTTL(Message response) { 68 | return (int) Math.min(RecordUtils.maxTTL(response 69 | .getSectionArray(Section.ANSWER)), RecordUtils.maxTTL(response 70 | .getSectionArray(Section.ADDITIONAL))); 71 | } 72 | 73 | public void setResponseToCache(final Message query, final byte[] responseBytes) { 74 | if (configure.isUseCache()) { 75 | getCacheSaveExecutors().execute(new Runnable() { 76 | 77 | @Override 78 | public void run() { 79 | try { 80 | Message response = new Message(responseBytes); 81 | int expireTime; 82 | if (configure.getCacheExpire() > 0) { 83 | expireTime = configure.getCacheExpire(); 84 | } else { 85 | expireTime = minTTL(response); 86 | } 87 | cacheClient.set(getCacheKey(query), new UDPPackage( 88 | responseBytes), expireTime); 89 | } catch (Throwable e) { 90 | logger.warn("set to cache error ", e); 91 | } 92 | 93 | } 94 | }); 95 | } 96 | 97 | } 98 | 99 | /** 100 | * @return the cacheSaveExecutors 101 | */ 102 | private ExecutorService getCacheSaveExecutors() { 103 | if (cacheSaveExecutors == null) { 104 | synchronized (this) { 105 | cacheSaveExecutors = Executors.newFixedThreadPool(configure.getThreadNum()); 106 | } 107 | } 108 | return cacheSaveExecutors; 109 | } 110 | 111 | /* 112 | * (non-Javadoc) 113 | * 114 | * @see 115 | * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 116 | */ 117 | @Override 118 | public void afterPropertiesSet() throws Exception { 119 | if (configure.isUseCache()) { 120 | getCacheSaveExecutors(); 121 | } 122 | } 123 | 124 | public void clearCache() { 125 | cacheClient.clearCache(); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/cache/EhcacheClient.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.cache; 2 | 3 | import net.sf.ehcache.Cache; 4 | import net.sf.ehcache.CacheManager; 5 | import net.sf.ehcache.Element; 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.apache.log4j.Logger; 8 | import org.springframework.beans.factory.InitializingBean; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.core.io.ClassPathResource; 11 | import org.springframework.stereotype.Component; 12 | import us.codecraft.blackhole.config.Configure; 13 | import us.codecraft.wifesays.me.ShutDownAble; 14 | import us.codecraft.wifesays.me.StandReadyWorker; 15 | 16 | import java.io.FileWriter; 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.PrintWriter; 20 | import java.util.List; 21 | import java.util.concurrent.Executors; 22 | import java.util.concurrent.ScheduledExecutorService; 23 | import java.util.concurrent.TimeUnit; 24 | 25 | /** 26 | * @author yihua.huang@dianping.com 27 | * @date Dec 19, 2012 28 | */ 29 | @Component("EhcacheClient") 30 | public class EhcacheClient extends StandReadyWorker implements CacheClient, 31 | InitializingBean, ShutDownAble { 32 | 33 | private Logger logger = Logger.getLogger(EhcacheClient.class); 34 | 35 | @Autowired 36 | private Configure configure; 37 | 38 | private static volatile CacheManager manager; 39 | 40 | private static final String CACHE_NAME = "DNS"; 41 | 42 | private static final String CACHE_CONF = "ehcache.xml"; 43 | 44 | private static final String CLEAR = "clear_cache"; 45 | 46 | private static final String DUMP = "dump_cache"; 47 | 48 | private static final String STAT = "stat_cache"; 49 | 50 | private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); 51 | 52 | /** 53 | * (non-Jsdoc) 54 | * 55 | * @see com.CacheClient.mail.postman.cache.service.impl.CacheClient#init() 56 | */ 57 | @Override 58 | public void init() { 59 | ClassPathResource classPathResource = new ClassPathResource(CACHE_CONF); 60 | InputStream inputStream = null; 61 | try { 62 | inputStream = classPathResource.getInputStream(); 63 | if (manager == null) { 64 | synchronized (EhcacheClient.class) { 65 | if (manager == null) { 66 | manager = new CacheManager(inputStream); 67 | scheduledExecutorService.scheduleAtFixedRate(new Runnable() { 68 | @Override 69 | public void run() { 70 | if (logger.isDebugEnabled()) { 71 | logger.debug("start to flush cache to disk"); 72 | } 73 | try { 74 | Cache cache = manager.getCache(CACHE_NAME); 75 | cache.flush(); 76 | } catch (Exception e) { 77 | logger.warn("flush cache error!", e); 78 | } 79 | } 80 | }, 1, 1, TimeUnit.MINUTES); 81 | } 82 | } 83 | } 84 | } catch (Exception e) { 85 | logger.error("init ehcache error!", e); 86 | } finally { 87 | try { 88 | inputStream.close(); 89 | } catch (IOException e) { 90 | logger.warn("close error", e); 91 | } 92 | inputStream = null; 93 | } 94 | } 95 | 96 | /** 97 | * (non-Jsdoc) 98 | * 99 | * @see com.dianping.mail.postman.cache.service.CacheService#get(java.lang.String) 100 | */ 101 | @SuppressWarnings("unchecked") 102 | @Override 103 | public T get(String key) { 104 | if (manager == null || key == null) { 105 | return null; 106 | } 107 | if (manager == null) { 108 | return null; 109 | } 110 | Cache cache = manager.getCache(CACHE_NAME); 111 | Element element = cache.get(key); 112 | if (element == null) { 113 | return null; 114 | } 115 | T value = (T) element.getObjectValue(); 116 | return value; 117 | } 118 | 119 | /** 120 | * (non-Jsdoc) 121 | * 122 | * @see com.dianping.mail.postman.cache.service.CacheService#set(java.lang.String, 123 | * java.lang.Object, int) 124 | */ 125 | @Override 126 | public boolean set(String key, T value, int expireTime) { 127 | if (!configure.isUseCache()) { 128 | return false; 129 | } 130 | if (key == null || value == null) { 131 | throw new IllegalArgumentException( 132 | "key and value should not be null"); 133 | } 134 | if (manager == null) { 135 | return false; 136 | } 137 | Cache cache = manager.getCache(CACHE_NAME); 138 | Element element = new Element(key, value, Boolean.FALSE, 0, expireTime); 139 | cache.put(element); 140 | return true; 141 | } 142 | 143 | /** 144 | * telnet control for caches 145 | * @param whatWifeSays 146 | * @return 147 | */ 148 | @Override 149 | public String doWhatYouShouldDo(String whatWifeSays) { 150 | if (manager == null) { 151 | return "CACHE NOT USED"; 152 | } 153 | if (DUMP.equalsIgnoreCase(whatWifeSays)) { 154 | final String dumpFilename = Configure.FILE_PATH + "/cache.dump"; 155 | Cache cache = manager.getCache(CACHE_NAME); 156 | List keys = (List) cache.getKeys(); 157 | try { 158 | PrintWriter writer = new PrintWriter(new FileWriter(dumpFilename)); 159 | for (String key : keys) { 160 | writer.println(key + "\t" + cache.get(key)); 161 | } 162 | writer.close(); 163 | } catch (IOException e) { 164 | logger.error("dumpfile error", e); 165 | } 166 | return keys.size() + "_caches_are_dumped_to_file_'" + dumpFilename + "'"; 167 | } else if (CLEAR.equalsIgnoreCase(whatWifeSays)) { 168 | clearCache(); 169 | return "REMOVE SUCCESS"; 170 | } else if (STAT.equalsIgnoreCase(whatWifeSays)) { 171 | Cache cache = manager.getCache(CACHE_NAME); 172 | return cache.getSize()+" records in cache, try 'cache dump' to more info"; 173 | } 174 | if (whatWifeSays.startsWith(CLEAR)) { 175 | String[] split = whatWifeSays.split(":"); 176 | if (split.length >= 2) { 177 | String address = split[1]; 178 | if (!address.endsWith(".")) { 179 | address += "."; 180 | } 181 | String type = "A"; 182 | if (split.length > 2) { 183 | type = split[2]; 184 | } 185 | String key = address + " " + type; 186 | if (manager == null) { 187 | return "CACHE NOT USED"; 188 | } 189 | Cache cache = manager.getCache(CACHE_NAME); 190 | if (cache.get(key) == null) { 191 | return "KEY " + key + " NOT EXIST"; 192 | } 193 | cache.remove(key); 194 | return "REMOVE SUCCESS"; 195 | } 196 | } 197 | return null; 198 | } 199 | 200 | public void clearCache() { 201 | if (manager==null){ 202 | return; 203 | } 204 | Cache cache = manager.getCache(CACHE_NAME); 205 | if (cache==null){ 206 | return; 207 | } 208 | @SuppressWarnings("unchecked") 209 | List keys = cache.getKeys(); 210 | logger.info(keys.size() + " cached records cleared"); 211 | if (logger.isDebugEnabled()) { 212 | logger.debug("[" + StringUtils.join(keys, ",") + "]"); 213 | } 214 | cache.removeAll(); 215 | } 216 | 217 | /* 218 | * (non-Javadoc) 219 | * 220 | * @see 221 | * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 222 | */ 223 | @Override 224 | public void afterPropertiesSet() throws Exception { 225 | if (configure.isUseCache()) { 226 | Thread thread = new Thread() { 227 | @Override 228 | public void run() { 229 | init(); 230 | } 231 | }; 232 | thread.setDaemon(true); 233 | thread.start(); 234 | } 235 | } 236 | 237 | /* 238 | * (non-Javadoc) 239 | * 240 | * @see us.codecraft.wifesays.me.ShutDownAble#shutDown() 241 | */ 242 | @Override 243 | public void shutDown() { 244 | try { 245 | Cache cache = manager.getCache(CACHE_NAME); 246 | cache.flush(); 247 | manager.shutdown(); 248 | logger.info("flush cache to disk success!"); 249 | } catch (Exception e) { 250 | logger.warn("flush cache error!", e); 251 | } 252 | } 253 | 254 | } 255 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/cache/MapCacheClient.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.cache; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.util.Map; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | 8 | /** 9 | * Cache in ConcurrentHashMap. Just for test.
10 | * Not in use.
11 | * @author code4crafer@gmail.com 12 | * Date: 13-6-23 13 | * Time: 下午12:08 14 | */ 15 | @Component("MapCacheClient") 16 | public class MapCacheClient implements CacheClient{ 17 | 18 | private Map map = new ConcurrentHashMap(); 19 | 20 | @Override 21 | public boolean set(String key, T value, int expireTime) { 22 | map.put(key,value); 23 | return true; 24 | } 25 | 26 | @Override 27 | public T get(String key) { 28 | return (T)map.get(key); 29 | } 30 | 31 | @Override 32 | public void init() throws Exception { 33 | } 34 | 35 | @Override 36 | public void clearCache() { 37 | map = new ConcurrentHashMap(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/cache/UDPPackage.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.cache; 2 | 3 | import org.xbill.DNS.Message; 4 | 5 | import java.io.IOException; 6 | import java.io.Serializable; 7 | import java.util.Arrays; 8 | 9 | /** 10 | * @author yihua.huang@dianping.com 11 | * @date Dec 19, 2012 12 | */ 13 | public class UDPPackage implements Serializable { 14 | 15 | private static final long serialVersionUID = 1L; 16 | private byte[] bytes; 17 | private static final int MASK = (1 << 8) - 1; 18 | 19 | public UDPPackage(byte[] bytes) { 20 | this.bytes = bytes; 21 | } 22 | 23 | /** 24 | * copy on write 25 | * 26 | * @param version 27 | * @return 28 | */ 29 | public byte[] getBytes(int version) { 30 | byte[] bytes = Arrays.copyOf(this.bytes, this.bytes.length); 31 | bytes[1] = (byte) (version & MASK); 32 | bytes[0] = (byte) ((version >> 8) & MASK); 33 | return bytes; 34 | } 35 | 36 | /** 37 | * block 38 | * 39 | * @param version 40 | * @return 41 | */ 42 | public synchronized byte[] getBytesSync(int version) { 43 | byte[] bytes = Arrays.copyOf(this.bytes, this.bytes.length); 44 | bytes[1] = (byte) (version & MASK); 45 | bytes[0] = (byte) ((version >> 8) & MASK); 46 | return bytes; 47 | } 48 | 49 | public static void main(String[] args) { 50 | byte[] b = new byte[] { 0, 1, 2 }; 51 | UDPPackage package1 = new UDPPackage(b); 52 | package1.getBytes(128); 53 | System.out.println(package1); 54 | 55 | package1.getBytes(1); 56 | System.out.println(package1); 57 | } 58 | 59 | /* 60 | * (non-Javadoc) 61 | * 62 | * @see java.lang.Object#toString() 63 | */ 64 | @Override 65 | public String toString() { 66 | try { 67 | final Message message = new Message(bytes); 68 | return message.toString(); 69 | } catch (IOException e) { 70 | 71 | if (bytes == null || bytes.length == 0) { 72 | return "[]"; 73 | } 74 | StringBuilder stringBuilder = new StringBuilder(); 75 | stringBuilder.append("["); 76 | stringBuilder.append(bytes[0]); 77 | for (int i = 1; i < bytes.length; i++) { 78 | stringBuilder.append(","); 79 | stringBuilder.append(bytes[i]); 80 | } 81 | stringBuilder.append("]"); 82 | return stringBuilder.toString(); 83 | } 84 | 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/concurrent/ThreadPools.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.concurrent; 2 | 3 | import org.apache.commons.logging.Log; 4 | import org.apache.commons.logging.LogFactory; 5 | import org.springframework.beans.factory.InitializingBean; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Component; 8 | import us.codecraft.blackhole.config.Configure; 9 | 10 | import java.util.concurrent.ExecutorService; 11 | import java.util.concurrent.LinkedBlockingQueue; 12 | import java.util.concurrent.ThreadPoolExecutor; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | /** 16 | * @author yihua.huang@dianping.com 17 | * @date Apr 11, 2013 18 | */ 19 | @Component 20 | public class ThreadPools implements InitializingBean { 21 | 22 | private ThreadPoolExecutor mainProcessExecutor; 23 | private ThreadPoolExecutor udpReceiverExecutor; 24 | private int threadNum = 0; 25 | @Autowired 26 | private Configure configure; 27 | private Log logger = LogFactory.getLog(getClass()); 28 | 29 | public void resize() { 30 | if (threadNum != configure.getThreadNum()) { 31 | threadNum = configure.getThreadNum(); 32 | logger.info("Thread num changed, resize to " + threadNum); 33 | if (threadNum < configure.getThreadNum()) { 34 | mainProcessExecutor.setMaximumPoolSize(threadNum); 35 | mainProcessExecutor.setCorePoolSize(threadNum); 36 | udpReceiverExecutor.setMaximumPoolSize(threadNum); 37 | udpReceiverExecutor.setCorePoolSize(threadNum); 38 | } else { 39 | mainProcessExecutor.setCorePoolSize(threadNum); 40 | mainProcessExecutor.setMaximumPoolSize(threadNum); 41 | udpReceiverExecutor.setCorePoolSize(threadNum); 42 | udpReceiverExecutor.setMaximumPoolSize(threadNum); 43 | } 44 | } 45 | } 46 | 47 | public ExecutorService getMainProcessExecutor() { 48 | return mainProcessExecutor; 49 | } 50 | 51 | public ExecutorService getUdpReceiverExecutor() { 52 | return udpReceiverExecutor; 53 | } 54 | 55 | /* 56 | * (non-Javadoc) 57 | * 58 | * @see 59 | * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 60 | */ 61 | @Override 62 | public void afterPropertiesSet() throws Exception { 63 | threadNum = configure.getThreadNum(); 64 | mainProcessExecutor = new ThreadPoolExecutor(threadNum, threadNum, 0L, 65 | TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); 66 | udpReceiverExecutor = new ThreadPoolExecutor(threadNum, threadNum, 0L, 67 | TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/config/ConfigFileLoader.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.config; 2 | 3 | import org.apache.commons.lang3.BooleanUtils; 4 | import org.apache.log4j.Level; 5 | import org.apache.log4j.Logger; 6 | import org.springframework.beans.factory.InitializingBean; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Component; 9 | import us.codecraft.blackhole.concurrent.ThreadPools; 10 | import us.codecraft.blackhole.forward.DNSHostsContainer; 11 | import us.codecraft.wifesays.me.ReloadAble; 12 | 13 | import java.io.BufferedReader; 14 | import java.io.FileReader; 15 | import java.net.InetSocketAddress; 16 | 17 | /** 18 | * @author yihua.huang@dianping.com 19 | * @date Dec 28, 2012 20 | */ 21 | @Component 22 | public class ConfigFileLoader implements InitializingBean, ReloadAble { 23 | 24 | @Autowired 25 | private DNSHostsContainer dnsHostsContainer; 26 | 27 | @Autowired 28 | private Configure configure; 29 | 30 | @Autowired 31 | private ThreadPools threadPools; 32 | 33 | private boolean reloadOff = false; 34 | 35 | /** 36 | * @param reloadOff the reloadOff to set 37 | */ 38 | public void setReloadOff(boolean reloadOff) { 39 | this.reloadOff = reloadOff; 40 | } 41 | 42 | private Logger logger = Logger.getLogger(getClass()); 43 | 44 | public void readConfig(String filename) { 45 | try { 46 | BufferedReader bufferedReader = new BufferedReader(new FileReader( 47 | filename)); 48 | String line = null; 49 | dnsHostsContainer.clearHosts(); 50 | while ((line = bufferedReader.readLine()) != null) { 51 | line = line.trim(); 52 | if (line.startsWith("#")) { 53 | continue; 54 | } 55 | try { 56 | String[] items = line.split("="); 57 | if (items.length < 2) { 58 | continue; 59 | } 60 | String key = items[0]; 61 | String value = items[1]; 62 | boolean configed = config(key, value); 63 | if (configed) { 64 | logger.info("read config success:\t" + line); 65 | } 66 | } catch (Exception e) { 67 | logger.warn("parse config line error:\t" + line, e); 68 | } 69 | } 70 | bufferedReader.close(); 71 | } catch (Throwable e) { 72 | logger.warn("read config file failed:" + filename, e); 73 | } 74 | } 75 | 76 | private boolean config(String key, String value) { 77 | if (key.equalsIgnoreCase("ttl")) { 78 | configure.setTtl(Integer.parseInt(value)); 79 | } else if (key.equalsIgnoreCase("dns")) { 80 | dnsHostsContainer.addHost(new InetSocketAddress(value, 81 | Configure.DNS_PORT)); 82 | } else if (key.equalsIgnoreCase("cache")) { 83 | configure.setUseCache(BooleanUtils.toBooleanObject(value)); 84 | } else if (key.equalsIgnoreCase("log")) { 85 | configLogLevel(value); 86 | } else if (key.equalsIgnoreCase("dns_timeout")) { 87 | int dnsTimeout = Integer.parseInt(value); 88 | configure.setDnsTimeOut(dnsTimeout); 89 | dnsHostsContainer.setTimeout(dnsTimeout); 90 | } else if (key.equalsIgnoreCase("fake_dns")) { 91 | configure.setFakeDnsServer(value); 92 | } else if (key.equalsIgnoreCase("safe_box")) { 93 | configure.setEnableSafeBox(BooleanUtils.toBooleanObject(value)); 94 | } else if (key.equalsIgnoreCase("thread_num")) { 95 | configure.setThreadNum(Integer.parseInt(value)); 96 | } else if (key.equalsIgnoreCase("cache_expire")) { 97 | configure.setCacheExpire(Integer.parseInt(value)); 98 | } else { 99 | return false; 100 | } 101 | return true; 102 | } 103 | 104 | private void configLogLevel(String value) { 105 | Logger rootLogger = Logger.getRootLogger(); 106 | if ("debug".equalsIgnoreCase(value)) { 107 | rootLogger.setLevel(Level.DEBUG); 108 | } else if ("info".equalsIgnoreCase(value)) { 109 | rootLogger.setLevel(Level.INFO); 110 | } else if ("warn".equalsIgnoreCase(value)) { 111 | rootLogger.setLevel(Level.WARN); 112 | } else if ("trace".equalsIgnoreCase(value)) { 113 | rootLogger.setLevel(Level.TRACE); 114 | } else { 115 | return; 116 | } 117 | configure.setLoggerLevel(value); 118 | 119 | } 120 | 121 | /* 122 | * (non-Javadoc) 123 | * 124 | * @see us.codecraft.wifesays.me.ReloadAble#reload() 125 | */ 126 | @Override 127 | public void reload() { 128 | if (!reloadOff) { 129 | readConfig(Configure.getConfigFilename()); 130 | } 131 | threadPools.resize(); 132 | } 133 | 134 | /* 135 | * (non-Javadoc) 136 | * 137 | * @see 138 | * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 139 | */ 140 | @Override 141 | public void afterPropertiesSet() throws Exception { 142 | readConfig(Configure.getConfigFilename()); 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/config/ConfigFileRefresher.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.config; 2 | 3 | import org.springframework.beans.factory.InitializingBean; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.io.File; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.ScheduledExecutorService; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | /** 13 | * @author yihua.huang@dianping.com 14 | * @date Dec 29, 2012 15 | */ 16 | @Component 17 | public class ConfigFileRefresher implements InitializingBean { 18 | 19 | @Autowired 20 | private ConfigFileLoader configFileLoader; 21 | 22 | private ScheduledExecutorService scheduledExecutorService = Executors 23 | .newScheduledThreadPool(1); 24 | 25 | private long lastFileModifiedTime; 26 | 27 | /* 28 | * (non-Javadoc) 29 | * 30 | * @see 31 | * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 32 | */ 33 | public void afterPropertiesSet() throws Exception { 34 | File configFile = new File(Configure.getConfigFilename()); 35 | lastFileModifiedTime = configFile.lastModified(); 36 | 37 | scheduledExecutorService.scheduleWithFixedDelay(new Runnable() { 38 | 39 | public void run() { 40 | File configFile = new File(Configure.getConfigFilename()); 41 | // When two files' last modify time not equal, we consider it is 42 | // changed. 43 | synchronized (this) { 44 | if (configFile.lastModified() != lastFileModifiedTime) { 45 | lastFileModifiedTime = configFile.lastModified(); 46 | configFileLoader.reload(); 47 | } 48 | } 49 | } 50 | }, 500, 500, TimeUnit.MILLISECONDS); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/config/Configure.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.config; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import java.net.InetSocketAddress; 6 | import java.net.SocketAddress; 7 | 8 | /** 9 | * @author yihua.huang@dianping.com 10 | * @date Dec 14, 2012 11 | */ 12 | @Component 13 | public class Configure { 14 | public final static long DEFAULT_TTL = 2000; 15 | public final static int DEFAULT_DNS_TIMEOUT = 2000; 16 | public static String FILE_PATH = "/usr/local/blackhole/"; 17 | 18 | public final static int DEFAULT_MX_PRIORY = 10; 19 | 20 | private long ttl = DEFAULT_TTL; 21 | 22 | private int mxPriory = DEFAULT_MX_PRIORY; 23 | 24 | public final static int DNS_PORT = 53; 25 | 26 | private int dnsTimeOut = DEFAULT_DNS_TIMEOUT; 27 | 28 | private String loggerLevel; 29 | 30 | private boolean enableSafeBox = true; 31 | 32 | private SocketAddress fakeDnsServer; 33 | 34 | private boolean useCache = true; 35 | 36 | private int threadNum = 4; 37 | 38 | private static String configFilename; 39 | 40 | private int cacheExpire; 41 | 42 | public static String getConfigFilename() { 43 | if (configFilename == null) { 44 | return Configure.FILE_PATH + "/config/blackhole.conf"; 45 | } else { 46 | return configFilename; 47 | } 48 | } 49 | 50 | public static void setConfigFilename(String name) { 51 | configFilename = name; 52 | } 53 | 54 | private static String zonesFilename; 55 | 56 | public static void setZonesFilename(String name) { 57 | zonesFilename = name; 58 | } 59 | 60 | public static String getZonesFilename() { 61 | if (zonesFilename == null) { 62 | return Configure.FILE_PATH + "/config/zones"; 63 | } else { 64 | return zonesFilename; 65 | } 66 | } 67 | 68 | public boolean isEnableSafeBox() { 69 | return enableSafeBox; 70 | } 71 | 72 | public void setEnableSafeBox(boolean enableSafeBox) { 73 | this.enableSafeBox = enableSafeBox; 74 | } 75 | 76 | /** 77 | * @return the useCache 78 | */ 79 | public boolean isUseCache() { 80 | return useCache; 81 | } 82 | 83 | /** 84 | * 85 | * @return 86 | */ 87 | public long getTTL() { 88 | return ttl; 89 | } 90 | 91 | /** 92 | * @return the ttl 93 | */ 94 | public long getTtl() { 95 | return ttl; 96 | } 97 | 98 | /** 99 | * @param ttl 100 | * the ttl to set 101 | */ 102 | public void setTtl(long ttl) { 103 | this.ttl = ttl; 104 | } 105 | 106 | /** 107 | * @return the mxPriory 108 | */ 109 | public int getMxPriory() { 110 | return mxPriory; 111 | } 112 | 113 | /** 114 | * @param mxPriory 115 | * the mxPriory to set 116 | */ 117 | public void setMxPriory(int mxPriory) { 118 | this.mxPriory = mxPriory; 119 | } 120 | 121 | /** 122 | * @return the dnsTimeOut 123 | */ 124 | public int getDnsTimeOut() { 125 | return dnsTimeOut; 126 | } 127 | 128 | /** 129 | * @return the loggerLevel 130 | */ 131 | public String getLoggerLevel() { 132 | return loggerLevel; 133 | } 134 | 135 | /** 136 | * @param loggerLevel 137 | * the loggerLevel to set 138 | */ 139 | public void setLoggerLevel(String loggerLevel) { 140 | this.loggerLevel = loggerLevel; 141 | } 142 | 143 | /** 144 | * @param dnsTimeOut 145 | * the dnsTimeOut to set 146 | */ 147 | public void setDnsTimeOut(int dnsTimeOut) { 148 | this.dnsTimeOut = dnsTimeOut; 149 | } 150 | 151 | /** 152 | * @param useCache 153 | * the useCache to set 154 | */ 155 | public void setUseCache(boolean useCache) { 156 | this.useCache = useCache; 157 | } 158 | 159 | public SocketAddress getFakeDnsServer() { 160 | return fakeDnsServer; 161 | } 162 | 163 | public void setFakeDnsServer(String fakeDnsServer) { 164 | this.fakeDnsServer = new InetSocketAddress(fakeDnsServer, DNS_PORT); 165 | } 166 | 167 | public int getThreadNum() { 168 | return threadNum; 169 | } 170 | 171 | public void setThreadNum(int threadNum) { 172 | this.threadNum = threadNum; 173 | } 174 | 175 | public int getCacheExpire() { 176 | return cacheExpire; 177 | } 178 | 179 | public void setCacheExpire(int cacheExpire) { 180 | this.cacheExpire = cacheExpire; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/config/DomainPattern.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.config; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | /** 6 | * Compile a pattern to regex or plain text. 7 | * 8 | * @author yihua.huang@dianping.com
9 | * @date: 13-7-15
10 | * Time: 上午7:27
11 | */ 12 | public class DomainPattern { 13 | 14 | private Pattern regexPattern; 15 | 16 | private String fullTextMatch; 17 | 18 | private boolean useRegex; 19 | 20 | public Pattern getRegexPattern() { 21 | return regexPattern; 22 | } 23 | 24 | public void setRegexPattern(Pattern regexPattern) { 25 | this.regexPattern = regexPattern; 26 | } 27 | 28 | public String getFullTextMatch() { 29 | return fullTextMatch; 30 | } 31 | 32 | public void setFullTextMatch(String fullTextMatch) { 33 | this.fullTextMatch = fullTextMatch; 34 | } 35 | 36 | public boolean isUseRegex() { 37 | return useRegex; 38 | } 39 | 40 | public void setUseRegex(boolean useRegex) { 41 | this.useRegex = useRegex; 42 | } 43 | 44 | /** 45 | * @param domain 46 | * @return 47 | */ 48 | public static DomainPattern parse(String domain) { 49 | DomainPattern domainPattern = new DomainPattern(); 50 | if (domain.contains("*")) { 51 | Pattern pattern = compileStringToPattern(domain); 52 | domainPattern.setRegexPattern(pattern); 53 | domainPattern.setUseRegex(true); 54 | } else { 55 | domainPattern.setFullTextMatch(domain+"."); 56 | domainPattern.setUseRegex(false); 57 | } 58 | return domainPattern; 59 | } 60 | 61 | private static Pattern compileStringToPattern(String patternStr) { 62 | patternStr = "^" + patternStr; 63 | patternStr += "."; 64 | patternStr = patternStr.replace(".", "\\."); 65 | patternStr = patternStr.replace("*", ".*"); 66 | patternStr += "$"; 67 | return Pattern.compile(patternStr); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/config/ZonesFileLoader.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.config; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.apache.log4j.Logger; 5 | import org.springframework.beans.factory.InitializingBean; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Component; 8 | import us.codecraft.blackhole.answer.AnswerPatternProvider; 9 | import us.codecraft.blackhole.answer.CustomAnswerPatternProvider; 10 | import us.codecraft.blackhole.answer.DomainPatternsContainer; 11 | import us.codecraft.blackhole.cache.CacheManager; 12 | import us.codecraft.blackhole.forward.DNSHostsContainer; 13 | import us.codecraft.blackhole.utils.DoubleKeyMap; 14 | import us.codecraft.wifesays.me.ReloadAble; 15 | 16 | import java.io.BufferedReader; 17 | import java.io.FileReader; 18 | import java.util.HashMap; 19 | import java.util.LinkedHashMap; 20 | import java.util.Map; 21 | import java.util.concurrent.ConcurrentHashMap; 22 | import java.util.regex.Pattern; 23 | 24 | /** 25 | * @author yihua.huang@dianping.com 26 | * @date Dec 28, 2012 27 | */ 28 | @Component 29 | public class ZonesFileLoader implements InitializingBean, ReloadAble { 30 | 31 | @Autowired 32 | private Configure configure; 33 | 34 | @Autowired 35 | private AnswerPatternProvider answerPatternContainer; 36 | 37 | @Autowired 38 | private CacheManager cacheManager; 39 | 40 | @Autowired 41 | private CustomAnswerPatternProvider customAnswerPatternProvider; 42 | 43 | @Autowired 44 | private DNSHostsContainer dnsHostsContainer; 45 | 46 | private Logger logger = Logger.getLogger(getClass()); 47 | 48 | public void readConfig(String filename) { 49 | try { 50 | DomainPatternsContainer domainPatternsContainer = new DomainPatternsContainer(); 51 | DomainPatternsContainer nsDomainPatternContainer = new DomainPatternsContainer(); 52 | DoubleKeyMap customAnswerPatternsTemp = new DoubleKeyMap( 53 | new ConcurrentHashMap>(), LinkedHashMap.class); 54 | DoubleKeyMap customAnswerTextsTemp = new DoubleKeyMap( 55 | new ConcurrentHashMap>(), HashMap.class); 56 | BufferedReader bufferedReader = new BufferedReader(new FileReader(filename)); 57 | String line = null; 58 | while ((line = bufferedReader.readLine()) != null) { 59 | ZonesPattern zonesPattern = ZonesPattern.parse(line); 60 | if (zonesPattern == null) { 61 | continue; 62 | } 63 | try { 64 | if (zonesPattern.getUserIp() == null) { 65 | for (Pattern pattern : zonesPattern.getPatterns()) { 66 | domainPatternsContainer.getDomainPatterns().put(pattern, zonesPattern.getTargetIp()); 67 | } 68 | for (String text : zonesPattern.getTexts()) { 69 | domainPatternsContainer.getDomainTexts().put(text, zonesPattern.getTargetIp()); 70 | } 71 | } else { 72 | for (Pattern pattern : zonesPattern.getPatterns()) { 73 | customAnswerPatternsTemp.put(zonesPattern.getUserIp(), pattern, zonesPattern.getTargetIp()); 74 | } 75 | for (String text : zonesPattern.getTexts()) { 76 | customAnswerTextsTemp.put(zonesPattern.getUserIp(), text, zonesPattern.getTargetIp()); 77 | } 78 | } 79 | logger.info("read config success:\t" + line); 80 | } catch (Exception e) { 81 | logger.warn("parse config line error:\t" + line + "\t" , e); 82 | } 83 | // For NS 84 | if (line.startsWith("NS")) { 85 | line = StringUtils.removeStartIgnoreCase(line, "NS").trim(); 86 | zonesPattern = ZonesPattern.parse(line); 87 | if (zonesPattern == null) { 88 | continue; 89 | } 90 | try { 91 | for (Pattern pattern : zonesPattern.getPatterns()) { 92 | nsDomainPatternContainer.getDomainPatterns().put(pattern,zonesPattern.getTargetIp()); 93 | } 94 | for (String text : zonesPattern.getTexts()) { 95 | nsDomainPatternContainer.getDomainTexts().put(text,zonesPattern.getTargetIp()); 96 | } 97 | logger.info("read config success:\t" + line); 98 | } catch (Exception e) { 99 | logger.warn("parse config line error:\t" + line + "\t" + e); 100 | } 101 | } 102 | } 103 | answerPatternContainer.setDomainPatternsContainer(domainPatternsContainer); 104 | customAnswerPatternProvider.setDomainPatterns(customAnswerPatternsTemp); 105 | customAnswerPatternProvider.setDomainTexts(customAnswerTextsTemp); 106 | dnsHostsContainer.setDomainPatternsContainer(nsDomainPatternContainer); 107 | bufferedReader.close(); 108 | } catch (Throwable e) { 109 | logger.warn("read config file failed:" + filename, e); 110 | } 111 | } 112 | 113 | /* 114 | * (non-Javadoc) 115 | * 116 | * @see us.codecraft.wifesays.me.ReloadAble#reload() 117 | */ 118 | @Override 119 | public void reload() { 120 | readConfig(Configure.getZonesFilename()); 121 | } 122 | 123 | /* 124 | * (non-Javadoc) 125 | * 126 | * @see 127 | * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 128 | */ 129 | @Override 130 | public void afterPropertiesSet() throws Exception { 131 | reload(); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/config/ZonesFileRefresher.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.config; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.springframework.beans.factory.InitializingBean; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | import us.codecraft.blackhole.answer.CustomAnswerPatternProvider; 8 | import us.codecraft.blackhole.utils.RecordUtils; 9 | import us.codecraft.wifesays.me.StandReadyWorker; 10 | 11 | import java.io.File; 12 | import java.net.UnknownHostException; 13 | import java.util.concurrent.Executors; 14 | import java.util.concurrent.ScheduledExecutorService; 15 | import java.util.concurrent.TimeUnit; 16 | import java.util.regex.Pattern; 17 | 18 | /** 19 | * @author yihua.huang@dianping.com 20 | * @date Dec 29, 2012 21 | */ 22 | @Component 23 | public class ZonesFileRefresher extends StandReadyWorker implements InitializingBean { 24 | 25 | @Autowired 26 | private Configure configure; 27 | 28 | @Autowired 29 | private ZonesFileLoader zonesFileLoader; 30 | 31 | @Autowired 32 | private CustomAnswerPatternProvider customAnswerPatternProvider; 33 | 34 | private ScheduledExecutorService scheduledExecutorService = Executors 35 | .newScheduledThreadPool(1); 36 | 37 | private long lastFileModifiedTime; 38 | 39 | //delete_zones_ip_192.168.0.1 40 | private static final String DELETE_ZONES_IP = "delete_zones_ip_"; 41 | //add_zones_ip_192.168.0.1:127.0.0.1 *.dianping.com 42 | private static final String ADD_ZONES_IP = "add_zones_ip_"; 43 | 44 | /* 45 | * (non-Javadoc) 46 | * 47 | * @see 48 | * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 49 | */ 50 | public void afterPropertiesSet() throws Exception { 51 | File zonesFile = new File(Configure.getZonesFilename()); 52 | lastFileModifiedTime = zonesFile.lastModified(); 53 | 54 | scheduledExecutorService.scheduleWithFixedDelay(new Runnable() { 55 | 56 | public void run() { 57 | File zonesFile = new File(Configure.getZonesFilename()); 58 | // When two files' last modify time not equal, we consider it is 59 | // changed. 60 | synchronized (this) { 61 | if (zonesFile.lastModified() != lastFileModifiedTime) { 62 | lastFileModifiedTime = zonesFile.lastModified(); 63 | zonesFileLoader.reload(); 64 | } 65 | } 66 | } 67 | }, 500, 500, TimeUnit.MILLISECONDS); 68 | } 69 | 70 | @Override 71 | public String doWhatYouShouldDo(String whatWifeSays) { 72 | if (StringUtils.startsWithIgnoreCase(whatWifeSays, ADD_ZONES_IP)) { 73 | String line = StringUtils.removeStart(whatWifeSays, ADD_ZONES_IP); 74 | try { 75 | ZonesPattern zonesPattern = ZonesPattern.parse(line); 76 | if (zonesPattern == null) { 77 | return "PARSE ERROR"; 78 | } 79 | for (Pattern pattern : zonesPattern.getPatterns()) { 80 | customAnswerPatternProvider.getDomainPatterns().put(zonesPattern.getUserIp(), pattern, zonesPattern.getTargetIp()); 81 | } 82 | for (String text : zonesPattern.getTexts()) { 83 | customAnswerPatternProvider.getDomainTexts().put(zonesPattern.getUserIp(), text, zonesPattern.getTargetIp()); 84 | } 85 | return "SUCCESS, " + zonesPattern.getPatterns().size() + " patterns added."; 86 | } catch (UnknownHostException e) { 87 | return "ERROR " + e; 88 | } 89 | } else if (StringUtils.startsWithIgnoreCase(whatWifeSays, DELETE_ZONES_IP)) { 90 | String ip = StringUtils.removeStart(whatWifeSays, DELETE_ZONES_IP); 91 | if (RecordUtils.isValidIpv4Address(ip)) { 92 | customAnswerPatternProvider.getDomainPatterns().remove(ip); 93 | customAnswerPatternProvider.getDomainTexts().remove(ip); 94 | return "REMOVE SUCCESS"; 95 | } else { 96 | return "ERROR, invalid ip " + ip; 97 | } 98 | } 99 | 100 | return null; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/config/ZonesPattern.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.config; 2 | 3 | 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.xbill.DNS.Address; 6 | import us.codecraft.blackhole.answer.AnswerPatternProvider; 7 | import us.codecraft.blackhole.utils.RecordUtils; 8 | 9 | import java.net.UnknownHostException; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.regex.Pattern; 13 | 14 | /** 15 | * User: cairne 16 | * Date: 13-5-11 17 | * Time: 下午9:02 18 | */ 19 | public class ZonesPattern { 20 | 21 | private String userIp; 22 | 23 | private String targetIp; 24 | 25 | private List patterns = new ArrayList(); 26 | 27 | private List texts = new ArrayList(); 28 | 29 | public List getPatterns() { 30 | return patterns; 31 | } 32 | 33 | public void setPatterns(List patterns) { 34 | this.patterns = patterns; 35 | } 36 | 37 | public String getTargetIp() { 38 | return targetIp; 39 | } 40 | 41 | public void setTargetIp(String targetIp) { 42 | this.targetIp = targetIp; 43 | } 44 | 45 | public String getUserIp() { 46 | return userIp; 47 | } 48 | 49 | public void setUserIp(String userIp) { 50 | this.userIp = userIp; 51 | } 52 | 53 | public List getTexts() { 54 | return texts; 55 | } 56 | 57 | public void setTexts(List texts) { 58 | this.texts = texts; 59 | } 60 | 61 | public static ZonesPattern parse(String line) throws UnknownHostException { 62 | ZonesPattern zonesPattern = new ZonesPattern(); 63 | line = line.trim(); 64 | if (line.startsWith("#")) { 65 | return null; 66 | } 67 | if (line.contains(":")) { 68 | String userIp = StringUtils.trim(StringUtils.substringBefore(line, ":")); 69 | zonesPattern.setUserIp(userIp); 70 | line = StringUtils.trim(StringUtils.substringAfter(line, ":")); 71 | Address.getByAddress(userIp); 72 | } 73 | String[] items = line.split("[\\s_]+"); 74 | if (items.length < 2) { 75 | return null; 76 | } 77 | if (items[0].equalsIgnoreCase("NS")) { 78 | boolean configIp = RecordUtils 79 | .areValidIpv4Addresses(items[1]); 80 | zonesPattern.setTargetIp(AnswerPatternProvider.DO_NOTHING); 81 | for (int i = configIp ? 2 : 1; i < items.length; i++) { 82 | String pattern = items[i]; 83 | parseDomainPattern(zonesPattern, pattern); 84 | } 85 | 86 | } else { 87 | String ip = items[0]; 88 | Address.getByAddress(ip); 89 | zonesPattern.setTargetIp(ip); 90 | for (int i = 1; i < items.length; i++) { 91 | String pattern = items[i]; 92 | parseDomainPattern(zonesPattern, pattern); 93 | } 94 | } 95 | 96 | return zonesPattern; 97 | } 98 | 99 | private static void parseDomainPattern(ZonesPattern zonesPattern, String pattern) { 100 | DomainPattern domainPattern = DomainPattern.parse(pattern); 101 | if (domainPattern.isUseRegex()) { 102 | zonesPattern.getPatterns().add(domainPattern.getRegexPattern()); 103 | } else { 104 | zonesPattern.getTexts().add(domainPattern.getFullTextMatch()); 105 | } 106 | } 107 | 108 | 109 | } 110 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/connector/UDPConnectionResponser.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.connector; 2 | 3 | import org.apache.log4j.Logger; 4 | 5 | import java.io.IOException; 6 | import java.net.DatagramPacket; 7 | import java.net.DatagramSocket; 8 | 9 | public class UDPConnectionResponser { 10 | 11 | private static final Logger logger = Logger 12 | .getLogger(UDPConnectionResponser.class); 13 | 14 | private final DatagramSocket socket; 15 | private final DatagramPacket inDataPacket; 16 | 17 | public UDPConnectionResponser(DatagramSocket socket, 18 | DatagramPacket inDataPacket) { 19 | super(); 20 | this.socket = socket; 21 | this.inDataPacket = inDataPacket; 22 | } 23 | 24 | public DatagramPacket getInDataPacket() { 25 | return inDataPacket; 26 | } 27 | 28 | public void response(byte[] response) { 29 | 30 | try { 31 | 32 | if (response == null) { 33 | return; 34 | } 35 | DatagramPacket outdp = new DatagramPacket(response, 36 | response.length, inDataPacket.getAddress(), 37 | inDataPacket.getPort()); 38 | 39 | outdp.setData(response); 40 | outdp.setLength(response.length); 41 | outdp.setAddress(inDataPacket.getAddress()); 42 | outdp.setPort(inDataPacket.getPort()); 43 | 44 | try { 45 | socket.send(outdp); 46 | } catch (IOException e) { 47 | 48 | logger.debug("Error sending UDP response to " 49 | + inDataPacket.getAddress() + ", " + e); 50 | } 51 | 52 | } catch (Throwable e) { 53 | 54 | logger.warn( 55 | "Error processing UDP connection from " 56 | + inDataPacket.getSocketAddress() + ", ", e); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/connector/UDPConnectionWorker.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.connector; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.xbill.DNS.Message; 5 | import us.codecraft.blackhole.container.QueryProcesser; 6 | import us.codecraft.blackhole.context.RequestContextProcessor; 7 | import us.codecraft.blackhole.forward.Forwarder; 8 | 9 | import java.net.DatagramPacket; 10 | 11 | public class UDPConnectionWorker implements Runnable { 12 | 13 | private static final Logger logger = Logger 14 | .getLogger(UDPConnectionWorker.class); 15 | 16 | private final UDPConnectionResponser responser; 17 | private final DatagramPacket inDataPacket; 18 | 19 | private QueryProcesser queryProcesser; 20 | private Forwarder forwarder; 21 | 22 | public UDPConnectionWorker(DatagramPacket inDataPacket, 23 | QueryProcesser queryProcesser, UDPConnectionResponser responser, 24 | Forwarder forwarder) { 25 | super(); 26 | this.responser = responser; 27 | this.inDataPacket = inDataPacket; 28 | this.queryProcesser = queryProcesser; 29 | this.forwarder = forwarder; 30 | } 31 | 32 | public void run() { 33 | 34 | try { 35 | 36 | RequestContextProcessor.processRequest(inDataPacket); 37 | byte[] response = queryProcesser.process(inDataPacket.getData()); 38 | if (response != null) { 39 | responser.response(response); 40 | } else { 41 | Message query = new Message( 42 | inDataPacket.getData()); 43 | forwarder.forward(query.toWire(), query, responser); 44 | } 45 | } catch (Throwable e) { 46 | 47 | logger.warn( 48 | "Error processing UDP connection from " 49 | + inDataPacket.getSocketAddress() + ", ", e); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/connector/UDPSocketMonitor.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.connector; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import us.codecraft.blackhole.concurrent.ThreadPools; 6 | import us.codecraft.blackhole.container.QueryProcesser; 7 | import us.codecraft.blackhole.forward.Forwarder; 8 | 9 | import java.io.IOException; 10 | import java.net.*; 11 | import java.util.concurrent.ExecutorService; 12 | 13 | /** 14 | * Authored by EagleDNS 15 | * http://www.unlogic.se/projects/eagledns 16 | * 17 | * @author yihua.huang@dianping.com 18 | * @date 2012-12-17 19 | */ 20 | public class UDPSocketMonitor extends Thread { 21 | 22 | private Logger log = Logger.getLogger(this.getClass()); 23 | 24 | private InetAddress addr; 25 | private int port; 26 | private static final short udpLength = 512; 27 | private DatagramSocket socket; 28 | @Autowired 29 | private QueryProcesser queryProcesser; 30 | @Autowired 31 | private Forwarder forwarder; 32 | @Autowired 33 | private ThreadPools threadPools; 34 | 35 | public UDPSocketMonitor(String host, int port) { 36 | super(); 37 | try { 38 | this.addr = Inet4Address.getByName(host); 39 | this.port = port; 40 | socket = new DatagramSocket(port, addr); 41 | } catch (IOException e) { 42 | System.err.println("Startup fail, 53 port is taken or has no privilege. Check if you are running in root, or another DNS server is running."); 43 | log.error("Startup fail, 53 port is taken or has no privilege", e); 44 | System.exit(-1); 45 | } 46 | 47 | this.setDaemon(true); 48 | } 49 | 50 | @Override 51 | public void run() { 52 | ExecutorService executorService = threadPools.getMainProcessExecutor(); 53 | log.info("Starting UDP socket monitor on address " 54 | + this.getAddressAndPort()); 55 | 56 | while (true) { 57 | try { 58 | 59 | byte[] in = new byte[udpLength]; 60 | DatagramPacket indp = new DatagramPacket(in, in.length); 61 | indp.setLength(in.length); 62 | socket.receive(indp); 63 | executorService.execute(new UDPConnectionWorker(indp, 64 | queryProcesser, 65 | new UDPConnectionResponser(socket, indp), forwarder)); 66 | } catch (SocketException e) { 67 | 68 | // This is usally thrown on shutdown 69 | log.debug("SocketException thrown from UDP socket on address " 70 | + this.getAddressAndPort() + ", " + e); 71 | break; 72 | } catch (IOException e) { 73 | 74 | log.info("IOException thrown by UDP socket on address " 75 | + this.getAddressAndPort() + ", " + e); 76 | } 77 | } 78 | log.info("UDP socket monitor on address " + getAddressAndPort() 79 | + " shutdown"); 80 | } 81 | 82 | public String getAddressAndPort() { 83 | 84 | return addr.getHostAddress() + ":" + port; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/container/Handler.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.container; 2 | 3 | 4 | /** 5 | * 6 | * Handlers process the request and generate the response.
7 | * If there are more than one handler, they are processed as a chain.A handler 8 | * must be registered in {@link HandlerManager#registerHandlers()} before it 9 | * takes effect. 10 | * 11 | * @author yihua.huang@dianping.com 12 | * @date Dec 14, 2012 13 | */ 14 | public interface Handler { 15 | 16 | /** 17 | * Process the request and generate the response.
18 | * If there are more than one handler, they are processed as a chain.
19 | * Unlike java servlet-api, the request is completely constructed before 20 | * handling(not a stream),and the response will be sent to client only if 21 | * all handlers process completed. 22 | * 23 | * @param request 24 | * message from client 25 | * @param response 26 | * message to client 27 | * @return true: pass the request to next handler
28 | * false: finish the entire handle process. 29 | */ 30 | public boolean handle(MessageWrapper request, MessageWrapper response); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/container/HandlerManager.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.container; 2 | 3 | import org.springframework.beans.factory.InitializingBean; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Component; 6 | import us.codecraft.blackhole.answer.PostAnswerHandler; 7 | import us.codecraft.blackhole.answer.PreAnswerHandler; 8 | 9 | import java.util.Collections; 10 | import java.util.LinkedList; 11 | import java.util.List; 12 | 13 | /** 14 | * @author yihua.huang@dianping.com 15 | * @date Dec 14, 2012 16 | */ 17 | @Component 18 | public class HandlerManager implements InitializingBean { 19 | 20 | private List preHandlers; 21 | 22 | private List postHandlers; 23 | 24 | @Autowired 25 | private PreAnswerHandler preAnswerHandler; 26 | 27 | @Autowired 28 | private PostAnswerHandler postAnswerHandler; 29 | 30 | @Autowired 31 | private HeaderHandler headerHandler; 32 | 33 | /* 34 | * (non-Javadoc) 35 | * 36 | * @see 37 | * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 38 | */ 39 | @Override 40 | public void afterPropertiesSet() throws Exception { 41 | registerHandlers(); 42 | } 43 | 44 | public void registerHandlers() { 45 | preHandlers = new LinkedList(); 46 | preHandlers.add(headerHandler); 47 | preHandlers.add(preAnswerHandler); 48 | postHandlers = new LinkedList(); 49 | postHandlers.add(postAnswerHandler); 50 | } 51 | 52 | /** 53 | * @return the handlers 54 | */ 55 | public List getPreHandlers() { 56 | return Collections.unmodifiableList(preHandlers); 57 | } 58 | 59 | /** 60 | * @return the handlers 61 | */ 62 | public List getPostHandlers() { 63 | return Collections.unmodifiableList(postHandlers); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/container/HeaderHandler.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.container; 2 | 3 | import org.springframework.stereotype.Component; 4 | import org.xbill.DNS.Flags; 5 | import org.xbill.DNS.Record; 6 | import org.xbill.DNS.Section; 7 | 8 | /** 9 | * @author yihua.huang@dianping.com 10 | * @date Dec 14, 2012 11 | */ 12 | @Component 13 | public class HeaderHandler implements Handler { 14 | 15 | /* 16 | * (non-Javadoc) 17 | * 18 | * @see us.codecraft.blackhole.server.Handler#handle(org.xbill.DNS.Message, 19 | * org.xbill.DNS.Message) 20 | */ 21 | @Override 22 | public boolean handle(MessageWrapper request, MessageWrapper response) { 23 | response.getMessage().getHeader().setFlag(Flags.QR); 24 | if (request.getMessage().getHeader().getFlag(Flags.RD)) { 25 | response.getMessage().getHeader().setFlag(Flags.RD); 26 | } 27 | Record queryRecord = request.getMessage().getQuestion(); 28 | response.getMessage().addRecord(queryRecord, Section.QUESTION); 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/container/MessageWrapper.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.container; 2 | 3 | import org.xbill.DNS.Message; 4 | 5 | /** 6 | * Wrap the message because it is hard to modify some part of the message. 7 | * 8 | * @author yihua.huang@dianping.com 9 | * @date Mar 24, 2013 10 | */ 11 | public class MessageWrapper { 12 | 13 | private Message message; 14 | 15 | private boolean hasRecord; 16 | 17 | /** 18 | * @param message 19 | */ 20 | public MessageWrapper(Message message) { 21 | if (message == null) { 22 | throw new IllegalArgumentException("Message should not be null!"); 23 | } 24 | this.hasRecord = false; 25 | this.message = message; 26 | } 27 | 28 | public Message getMessage() { 29 | return message; 30 | } 31 | 32 | public boolean hasRecord() { 33 | return hasRecord; 34 | } 35 | 36 | public void setHasRecord(boolean hasRecord) { 37 | this.hasRecord = hasRecord; 38 | } 39 | 40 | public void setMessage(Message message) { 41 | if (message == null) { 42 | throw new IllegalArgumentException("Message should not be null!"); 43 | } 44 | this.message = message; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/container/QueryProcesser.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.container; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Component; 6 | import org.xbill.DNS.Message; 7 | import us.codecraft.blackhole.cache.CacheManager; 8 | 9 | import java.io.IOException; 10 | 11 | /** 12 | * Main logic of blackhole.
13 | * Process the DNS query and return the answer. 14 | * 15 | * @author yihua.huang@dianping.com 16 | * @date Dec 14, 2012 17 | */ 18 | @Component 19 | public class QueryProcesser { 20 | 21 | @Autowired 22 | private HandlerManager handlerManager; 23 | 24 | private Logger logger = Logger.getLogger(getClass()); 25 | 26 | @Autowired 27 | private CacheManager cacheManager; 28 | 29 | public byte[] process(byte[] queryData) throws IOException { 30 | Message query = new Message(queryData); 31 | if (logger.isDebugEnabled()) { 32 | logger.debug("get query " 33 | + query.getQuestion().getName().toString()); 34 | } 35 | MessageWrapper responseMessage = new MessageWrapper(new Message(query 36 | .getHeader().getID())); 37 | for (Handler handler : handlerManager.getPreHandlers()) { 38 | boolean handle = handler.handle(new MessageWrapper(query), 39 | responseMessage); 40 | if (!handle) { 41 | break; 42 | } 43 | } 44 | byte[] response = null; 45 | if (responseMessage.hasRecord()) { 46 | response = responseMessage.getMessage().toWire(); 47 | return response; 48 | } 49 | 50 | byte[] cache = cacheManager.getResponseFromCache(query); 51 | if (cache != null) { 52 | return cache; 53 | } else { 54 | for (Handler handler : handlerManager.getPostHandlers()) { 55 | boolean handle = handler.handle(new MessageWrapper(query), 56 | responseMessage); 57 | if (!handle) { 58 | break; 59 | } 60 | } 61 | if (responseMessage.hasRecord()) { 62 | response = responseMessage.getMessage().toWire(); 63 | return response; 64 | } else { 65 | return null; 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/context/RequestContext.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.context; 2 | 3 | /** 4 | * User: cairne 5 | * Date: 13-5-11 6 | * Time: 下午7:56 7 | */ 8 | public class RequestContext { 9 | 10 | private static ThreadLocal clientIps = new ThreadLocal(); 11 | 12 | public static String getClientIp() { 13 | return clientIps.get(); 14 | } 15 | 16 | public static void setClientIps(String clientIp){ 17 | clientIps.set(clientIp); 18 | } 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/context/RequestContextProcessor.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.context; 2 | 3 | import java.net.DatagramPacket; 4 | 5 | /** 6 | * User: cairne 7 | * Date: 13-5-11 8 | * Time: 下午7:59 9 | */ 10 | public class RequestContextProcessor { 11 | 12 | public static void processRequest(DatagramPacket datagramPacket) { 13 | RequestContext.setClientIps(datagramPacket.getAddress().getHostAddress()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/forward/ConnectionTimer.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.forward; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.apache.log4j.Logger; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | import org.xbill.DNS.Message; 8 | import org.xbill.DNS.Record; 9 | import org.xbill.DNS.Section; 10 | import org.xbill.DNS.Type; 11 | import us.codecraft.blackhole.antipollution.SafeHostManager; 12 | import us.codecraft.blackhole.cache.CacheManager; 13 | 14 | import java.io.IOException; 15 | import java.net.*; 16 | import java.util.concurrent.ExecutorService; 17 | import java.util.concurrent.Executors; 18 | 19 | /** 20 | * Check the connecting time of host.Use to detect whether the DNS answer is 21 | * correct. 22 | * 23 | * @author yihua.huang@dianping.com 24 | * @date Feb 20, 2013 25 | */ 26 | @Component 27 | public class ConnectionTimer { 28 | 29 | @Autowired 30 | private SafeHostManager safeBoxService; 31 | /** 32 | * If the server doesn't support ICMP protocol, try http instead. 33 | */ 34 | private static final int PORT_FOR_TEST = 80; 35 | 36 | @Autowired 37 | private CacheManager cacheManager; 38 | 39 | /** 40 | * Time out for connection timer. 41 | */ 42 | private static final int TIME_OUT = 1000; 43 | /** 44 | * expire time for connect time cache. 45 | */ 46 | private static final int EXPIRE_TIME = 3600 * 1000 * 5; 47 | 48 | private ExecutorService checkExecutors = Executors.newFixedThreadPool(4); 49 | 50 | private Logger logger = Logger.getLogger(getClass()); 51 | 52 | public void checkConnectTimeForAnswer(final Message query, 53 | final Message message) { 54 | checkExecutors.submit(new Runnable() { 55 | 56 | @Override 57 | public void run() { 58 | checkConnectTimeForAnswer0(query, message); 59 | } 60 | }); 61 | } 62 | 63 | private void checkConnectTimeForAnswer0(Message query, Message message) { 64 | byte[] answerBytes = cacheManager.getResponseFromCache(query); 65 | Message answerInCache = null; 66 | if (answerBytes != null) { 67 | try { 68 | answerInCache = new Message(answerBytes); 69 | MessageCheckResult checkConnectionTime = checkConnectionTime(answerInCache); 70 | if (checkConnectionTime == MessageCheckResult.UNCHANGED) { 71 | return; 72 | } else if (checkConnectionTime == MessageCheckResult.CHANGEDE_BUT_USEFUL) { 73 | // remove unreachable address and save 74 | if (logger.isDebugEnabled()) { 75 | logger.debug("update record in cahce " + message); 76 | } 77 | cacheManager.setResponseToCache(query, answerInCache.toWire()); 78 | return; 79 | } 80 | } catch (IOException e) { 81 | } 82 | } 83 | MessageCheckResult checkConnectionTime = checkConnectionTime(message); 84 | if (checkConnectionTime != MessageCheckResult.ALL_TIMEOUT) { 85 | if (logger.isDebugEnabled()) { 86 | logger.debug("set new message to cahce " + message); 87 | } 88 | cacheManager.setResponseToCache(query, message.toWire()); 89 | } 90 | } 91 | 92 | private enum MessageCheckResult { 93 | ALL_TIMEOUT, CHANGEDE_BUT_USEFUL, UNCHANGED; 94 | } 95 | 96 | private MessageCheckResult checkConnectionTime(Message message) { 97 | boolean changed = false; 98 | Record[] answers = message.getSectionArray(Section.ANSWER); 99 | for (Record answer : answers) { 100 | if (answer.getType() == Type.A || answer.getType() == Type.AAAA 101 | || answer.getType() == Type.CNAME) { 102 | String address = StringUtils.removeEnd(answer.rdataToString(), 103 | "."); 104 | long connectTime = getConnectTime(address); 105 | if (connectTime >= TIME_OUT) { 106 | changed = true; 107 | message.removeRecord(answer, Section.ANSWER); 108 | } else { 109 | String domain = StringUtils.removeEnd(message.getQuestion() 110 | .getName().toString(), "."); 111 | if (safeBoxService.isPoisoned(domain) 112 | && safeBoxService.get(domain) == null 113 | && (answer.getType() == Type.A || answer.getType() == Type.AAAA)) { 114 | safeBoxService.add(domain, address); 115 | } 116 | } 117 | } 118 | } 119 | if (message.getSectionArray(Section.ANSWER).length == 0) { 120 | return MessageCheckResult.ALL_TIMEOUT; 121 | } else if (changed) { 122 | 123 | return MessageCheckResult.CHANGEDE_BUT_USEFUL; 124 | } else { 125 | return MessageCheckResult.UNCHANGED; 126 | } 127 | } 128 | 129 | private long getConnectTime(String address) { 130 | Long connectTime = cacheManager. get(address); 131 | if (connectTime == null) { 132 | connectTime = checkConnectTimeForAddress(address); 133 | cacheManager.set(address, connectTime, EXPIRE_TIME); 134 | } 135 | return connectTime; 136 | } 137 | 138 | public long checkConnectTimeForAddress(String address) { 139 | Socket socket = null; 140 | try { 141 | long startTime = System.currentTimeMillis(); 142 | boolean icmpReachable = InetAddress.getByName(address).isReachable( 143 | TIME_OUT); 144 | long timeCost = System.currentTimeMillis() - startTime; 145 | if (icmpReachable) { 146 | return timeCost; 147 | } 148 | socket = new Socket(); 149 | startTime = System.currentTimeMillis(); 150 | socket.connect(new InetSocketAddress(address, PORT_FOR_TEST), 151 | TIME_OUT); 152 | timeCost = System.currentTimeMillis() - startTime; 153 | socket.close(); 154 | return timeCost; 155 | } catch (UnknownHostException e) { 156 | logger.warn("unkown host " + address + " " + e); 157 | } catch (SocketTimeoutException e) { 158 | } catch (SocketException e) { 159 | } catch (IOException e) { 160 | logger.warn("connect " + address + " error " + e); 161 | } finally { 162 | if (socket != null) { 163 | try { 164 | socket.close(); 165 | } catch (IOException e) { 166 | } 167 | } 168 | } 169 | return TIME_OUT; 170 | } 171 | } -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/forward/DNSHostsContainer.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.forward; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.springframework.stereotype.Component; 5 | import us.codecraft.blackhole.answer.DomainPatternsContainer; 6 | 7 | import java.net.InetSocketAddress; 8 | import java.net.SocketAddress; 9 | import java.util.*; 10 | import java.util.Map.Entry; 11 | import java.util.concurrent.ConcurrentHashMap; 12 | 13 | /** 14 | * @author yihua.huang@dianping.com 15 | * @date Dec 21, 2012 16 | */ 17 | @Component 18 | public class DNSHostsContainer { 19 | 20 | private DomainPatternsContainer domainPatternsContainer; 21 | 22 | private int timeout = 3000; 23 | 24 | private int order; 25 | 26 | private Map requestTimes = new ConcurrentHashMap(); 27 | 28 | private Logger logger = Logger.getLogger(getClass()); 29 | 30 | public void clearHosts() { 31 | requestTimes = new ConcurrentHashMap(); 32 | order = 0; 33 | } 34 | 35 | public void addHost(SocketAddress address) { 36 | requestTimes.put(address, order++); 37 | logger.info("add dns address " + address); 38 | } 39 | 40 | public int getOrder(SocketAddress socketAddress) { 41 | Integer order = requestTimes.get(socketAddress); 42 | return order == null ? 0 : order; 43 | } 44 | 45 | /** 46 | * @param timeout 47 | * the timeout to set 48 | */ 49 | public void setTimeout(int timeout) { 50 | this.timeout = timeout; 51 | } 52 | 53 | public List getAllAvaliableHosts(String domain) { 54 | String ip = domainPatternsContainer.getIp(domain); 55 | if (ip != null) { 56 | List socketAddresses = new ArrayList(); 57 | socketAddresses.add(new InetSocketAddress(ip, 53)); 58 | return socketAddresses; 59 | } 60 | List results = new ArrayList(); 61 | Iterator> iterator = requestTimes.entrySet().iterator(); 62 | while (iterator.hasNext()) { 63 | Entry next = iterator.next(); 64 | results.add(next.getKey()); 65 | } 66 | return results; 67 | } 68 | 69 | public void setDomainPatternsContainer(DomainPatternsContainer domainPatternsContainer) { 70 | this.domainPatternsContainer = domainPatternsContainer; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/forward/ForwardAnswer.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.forward; 2 | 3 | import org.xbill.DNS.Message; 4 | import us.codecraft.blackhole.connector.UDPConnectionResponser; 5 | 6 | import java.util.concurrent.atomic.AtomicInteger; 7 | 8 | /** 9 | * @author yihua.huang@dianping.com 10 | * @date Apr 10, 2013 11 | */ 12 | public class ForwardAnswer { 13 | 14 | private final Message query; 15 | 16 | private final UDPConnectionResponser responser; 17 | 18 | private final long startTime; 19 | 20 | private AtomicInteger minOrder = new AtomicInteger(Integer.MAX_VALUE); 21 | 22 | /** 23 | * Answer that not return immediately. It will be returned when countDown<=0 or ForwardAnswer is removed. 24 | */ 25 | private Message tempAnswer; 26 | 27 | /** 28 | * Number of external DNS response which not recevied yet. 29 | */ 30 | private final AtomicInteger countDown; 31 | 32 | /** 33 | * @param query 34 | * @param responser 35 | */ 36 | public ForwardAnswer(Message query, UDPConnectionResponser responser, int initCount) { 37 | super(); 38 | this.query = query; 39 | this.responser = responser; 40 | this.startTime = System.currentTimeMillis(); 41 | this.countDown = new AtomicInteger(initCount); 42 | } 43 | 44 | public UDPConnectionResponser getResponser() { 45 | return responser; 46 | } 47 | 48 | public long getStartTime() { 49 | return startTime; 50 | } 51 | 52 | public Message getQuery() { 53 | return query; 54 | } 55 | 56 | /** 57 | * When the DNS server' order is less than minOrder, it will be processed. 58 | * 59 | * @param order 60 | * @return 61 | */ 62 | public boolean shouldProcess(int order) { 63 | return minOrder.get() > order; 64 | } 65 | 66 | /** 67 | * Set the order as minOrder to prevent DNS server with larger order to be processed. 68 | * 69 | * @param order 70 | * @return 71 | */ 72 | public boolean confirmProcess(int order) { 73 | int minOrderi = minOrder.get(); 74 | while (order < minOrderi) { 75 | boolean b = minOrder.compareAndSet(minOrderi, order); 76 | if (b) { 77 | return true; 78 | } else { 79 | minOrderi = minOrder.get(); 80 | } 81 | } 82 | return false; 83 | } 84 | 85 | public Message getTempAnswer() { 86 | return tempAnswer; 87 | } 88 | 89 | public void setTempAnswer(Message tempAnswer) { 90 | this.tempAnswer = tempAnswer; 91 | } 92 | 93 | public int decrCountDown() { 94 | return countDown.decrementAndGet(); 95 | } 96 | 97 | public int getCountDown() { 98 | return countDown.get(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/forward/ForwardAnswerProcessor.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.forward; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.apache.log4j.Logger; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Service; 7 | import org.xbill.DNS.Message; 8 | import org.xbill.DNS.Record; 9 | import org.xbill.DNS.Section; 10 | import org.xbill.DNS.Type; 11 | import us.codecraft.blackhole.antipollution.BlackListManager; 12 | import us.codecraft.blackhole.antipollution.SafeHostManager; 13 | import us.codecraft.blackhole.cache.CacheManager; 14 | import us.codecraft.blackhole.config.Configure; 15 | import us.codecraft.blackhole.utils.RecordUtils; 16 | 17 | import java.io.IOException; 18 | import java.net.SocketAddress; 19 | 20 | /** 21 | * User: cairne 22 | * Date: 13-5-19 23 | * Time: 下午7:28 24 | */ 25 | @Service 26 | public class ForwardAnswerProcessor { 27 | 28 | @Autowired 29 | private CacheManager cacheManager; 30 | @Autowired 31 | private BlackListManager blackListManager; 32 | @Autowired 33 | private DNSHostsContainer dnsHostsContainer; 34 | 35 | @Autowired 36 | private ConnectionTimer connectionTimer; 37 | 38 | @Autowired 39 | private SafeHostManager safeBoxService; 40 | 41 | private Logger logger = Logger.getLogger(getClass()); 42 | 43 | @Autowired 44 | private Configure configure; 45 | 46 | public void handleAnswer(byte[] answer, Message message, SocketAddress remoteAddress, ForwardAnswer forwardAnswer) 47 | throws IOException { 48 | // fake dns server return an answer, it must be dns pollution 49 | if (configure.getFakeDnsServer() != null 50 | && remoteAddress.equals(configure.getFakeDnsServer())) { 51 | addToBlacklist(message); 52 | String domain = StringUtils.removeEnd(message.getQuestion() 53 | .getName().toString(), "."); 54 | safeBoxService.setPoisoned(domain); 55 | return; 56 | } 57 | if (logger.isTraceEnabled()) { 58 | logger.trace("get message from " + remoteAddress + "\n" + message); 59 | } 60 | if (forwardAnswer == null) { 61 | logger.info("Received messages for " 62 | + message.getQuestion().getName().toString() 63 | + " after timeout!"); 64 | return; 65 | } 66 | if (configure.isEnableSafeBox()) { 67 | answer = removeFakeAddress(message, answer); 68 | } 69 | 70 | if (answer != null) { 71 | forwardAnswer.decrCountDown(); 72 | int order = dnsHostsContainer.getOrder(remoteAddress); 73 | if (!RecordUtils.hasAnswer(message)) { 74 | forwardAnswer.setTempAnswer(message); 75 | if (forwardAnswer.getCountDown() <= 0) { 76 | forwardAnswer.getResponser().response(answer); 77 | forwardAnswer.setTempAnswer(null); 78 | } 79 | } else { 80 | if (forwardAnswer.confirmProcess(order)) { 81 | forwardAnswer.getResponser().response(answer); 82 | if (logger.isDebugEnabled()) { 83 | logger.debug("response message " + message.getHeader().getID() 84 | + " from " + remoteAddress + " to " 85 | + forwardAnswer.getResponser().getInDataPacket().getPort()); 86 | } 87 | cacheManager.setResponseToCache(message, answer); 88 | } 89 | } 90 | } 91 | } 92 | 93 | private void addToBlacklist(Message message) { 94 | for (Record answer : message.getSectionArray(Section.ANSWER)) { 95 | String address = StringUtils.removeEnd(answer.rdataToString(), "."); 96 | if (!blackListManager.inBlacklist(address)) { 97 | logger.info("detected dns poisoning, add address " + address 98 | + " to blacklist"); 99 | blackListManager.addToBlacklist(address); 100 | } 101 | } 102 | } 103 | 104 | private byte[] removeFakeAddress(Message message, byte[] bytes) { 105 | Record[] answers = message.getSectionArray(Section.ANSWER); 106 | boolean changed = false; 107 | for (Record answer : answers) { 108 | String address = StringUtils.removeEnd(answer.rdataToString(), "."); 109 | if ((answer.getType() == Type.A || answer.getType() == Type.AAAA) 110 | && blackListManager.inBlacklist(address)) { 111 | if (!changed) { 112 | // copy on write 113 | message = (Message) message.clone(); 114 | } 115 | message.removeRecord(answer, Section.ANSWER); 116 | changed = true; 117 | } 118 | } 119 | if (changed && message.getQuestion().getType() == Type.A 120 | && (message.getSectionArray(Section.ANSWER) == null || message 121 | .getSectionArray(Section.ANSWER).length == 0) 122 | && (message.getSectionArray(Section.ADDITIONAL) == null || message 123 | .getSectionArray(Section.ADDITIONAL).length == 0) 124 | && (message.getSectionArray(Section.AUTHORITY) == null || message 125 | .getSectionArray(Section.AUTHORITY).length == 0)) { 126 | logger.info("remove message " + message.getQuestion()); 127 | return null; 128 | } 129 | if (changed) { 130 | return message.toWire(); 131 | } 132 | return bytes; 133 | } 134 | 135 | 136 | } 137 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/forward/Forwarder.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.forward; 2 | 3 | import org.xbill.DNS.Message; 4 | import us.codecraft.blackhole.connector.UDPConnectionResponser; 5 | 6 | import java.net.SocketAddress; 7 | import java.util.List; 8 | 9 | /** 10 | * @author yihua.huang@dianping.com 11 | * @date Jan 16, 2013 12 | */ 13 | public interface Forwarder { 14 | 15 | /** 16 | * Forward query bytes to external DNS host(s) and get a valid DNS answer. 17 | * 18 | * @param queryBytes 19 | * @param query 20 | * @return 21 | */ 22 | public void forward(final byte[] queryBytes, Message query, 23 | UDPConnectionResponser responser); 24 | 25 | /** 26 | * Forward query bytes to external DNS host(s) and get a valid DNS answer. 27 | * 28 | * @param queryBytes 29 | * @param query 30 | * @return 31 | */ 32 | public void forward(final byte[] queryBytes, Message query, 33 | List hosts, UDPConnectionResponser responser); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/forward/MultiUDPForwarder.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.forward; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Component; 6 | import org.xbill.DNS.Message; 7 | import us.codecraft.blackhole.config.Configure; 8 | import us.codecraft.blackhole.connector.UDPConnectionResponser; 9 | 10 | import java.io.IOException; 11 | import java.net.SocketAddress; 12 | import java.nio.ByteBuffer; 13 | import java.nio.channels.DatagramChannel; 14 | import java.util.List; 15 | 16 | /** 17 | * Forward DNS query to hosts contained in {@link DNSHostsContainer}.Use the 18 | * same port 40311 for all UDP diagram and the instance of 19 | * {@link MultiUDPReceiver} will listen on the port 40311.Use wait/notify to 20 | * synchronize. 21 | * 22 | * @author yihua.huang@dianping.com 23 | * @date Jan 16, 2013 24 | */ 25 | @Component 26 | public class MultiUDPForwarder implements Forwarder { 27 | 28 | private Logger logger = Logger.getLogger(getClass()); 29 | 30 | @Autowired 31 | private MultiUDPReceiver multiUDPReceiver; 32 | 33 | @Autowired 34 | private DNSHostsContainer dnsHostsContainer; 35 | 36 | @Autowired 37 | private Configure configure; 38 | 39 | /* 40 | * (non-Javadoc) 41 | * 42 | * @see us.codecraft.blackhole.forward.Forwarder#forward(byte[], 43 | * org.xbill.DNS.Message) 44 | */ 45 | @Override 46 | public void forward(byte[] queryBytes, Message query, 47 | UDPConnectionResponser responser) { 48 | // get address 49 | List allAvaliableHosts = dnsHostsContainer 50 | .getAllAvaliableHosts(query.getQuestion().getName().toString()); 51 | forward(queryBytes, query, allAvaliableHosts, responser); 52 | } 53 | 54 | /* 55 | * (non-Javadoc) 56 | * 57 | * @see us.codecraft.blackhole.forward.Forwarder#forward(byte[], 58 | * org.xbill.DNS.Message, java.util.List) 59 | */ 60 | @Override 61 | public void forward(byte[] queryBytes, Message query, 62 | List hosts, UDPConnectionResponser responser) { 63 | if (logger.isDebugEnabled()) { 64 | logger.debug("forward query " + query.getQuestion().getName() + "_" 65 | + query.getHeader().getID()); 66 | } 67 | // send to all address 68 | 69 | int initCount = hosts.size(); 70 | if (configure.getFakeDnsServer() != null) { 71 | // send fake dns query to detect dns poisoning 72 | hosts.add(0, configure.getFakeDnsServer()); 73 | } 74 | ForwardAnswer forwardAnswer = new ForwardAnswer(query, responser, initCount); 75 | try { 76 | multiUDPReceiver.registerReceiver(query, forwardAnswer); 77 | try { 78 | for (SocketAddress host : hosts) { 79 | send(queryBytes, host); 80 | logger.debug("forward query " 81 | + query.getQuestion().getName() + "_" 82 | + query.getHeader().getID()); 83 | } 84 | } catch (IOException e) { 85 | logger.warn("error", e); 86 | } 87 | } finally { 88 | multiUDPReceiver.delayRemoveAnswer(query, configure.getDnsTimeOut()); 89 | } 90 | } 91 | 92 | private void send(byte[] queryBytes, SocketAddress host) throws IOException { 93 | DatagramChannel datagramChannel = multiUDPReceiver.getDatagramChannel(); 94 | datagramChannel.send(ByteBuffer.wrap(queryBytes), host); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/forward/MultiUDPReceiver.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.forward; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.springframework.beans.factory.InitializingBean; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | import org.xbill.DNS.Message; 8 | import us.codecraft.blackhole.concurrent.ThreadPools; 9 | 10 | import java.io.IOException; 11 | import java.net.InetSocketAddress; 12 | import java.net.SocketAddress; 13 | import java.nio.ByteBuffer; 14 | import java.nio.channels.DatagramChannel; 15 | import java.util.Arrays; 16 | import java.util.Map; 17 | import java.util.concurrent.*; 18 | 19 | /** 20 | * Listen on port 40311 using reactor mode. 21 | * 22 | * @author yihua.huang@dianping.com 23 | * @date Jan 16, 2013 24 | */ 25 | @Component 26 | public class MultiUDPReceiver implements InitializingBean { 27 | 28 | /** 29 | * 30 | */ 31 | private static final int dnsPackageLength = 512; 32 | 33 | private Map answers = new ConcurrentHashMap(); 34 | 35 | private DatagramChannel datagramChannel; 36 | 37 | private final static int PORT_RECEIVE = 40311; 38 | 39 | private DelayQueue delayRemoveQueue = new DelayQueue(); 40 | 41 | private static class DelayStringKey implements Delayed { 42 | 43 | private final String key; 44 | 45 | private final long initDelay; 46 | 47 | private long startTime; 48 | 49 | /** 50 | * @param key 51 | * @param initDelay in ms 52 | */ 53 | public DelayStringKey(String key, long initDelay) { 54 | this.startTime = System.currentTimeMillis(); 55 | this.key = key; 56 | this.initDelay = initDelay; 57 | } 58 | 59 | public String getKey() { 60 | return key; 61 | } 62 | 63 | /* 64 | * (non-Javadoc) 65 | * 66 | * @see java.lang.Comparable#compareTo(java.lang.Object) 67 | */ 68 | @Override 69 | public int compareTo(Delayed o) { 70 | long delayA = startTime + initDelay - System.currentTimeMillis(); 71 | long delayB = o.getDelay(TimeUnit.MILLISECONDS); 72 | if (delayA > delayB) { 73 | return 1; 74 | } else if (delayA < delayB) { 75 | return -1; 76 | } else { 77 | return 0; 78 | } 79 | } 80 | 81 | /* 82 | * (non-Javadoc) 83 | * 84 | * @see 85 | * java.util.concurrent.Delayed#getDelay(java.util.concurrent.TimeUnit) 86 | */ 87 | @Override 88 | public long getDelay(TimeUnit unit) { 89 | return unit.convert( 90 | startTime + initDelay - System.currentTimeMillis(), 91 | TimeUnit.MILLISECONDS); 92 | } 93 | 94 | } 95 | 96 | @Autowired 97 | private ThreadPools threadPools; 98 | 99 | @Autowired 100 | private ForwardAnswerProcessor forwardAnswerProcessor; 101 | 102 | private Logger logger = Logger.getLogger(getClass()); 103 | 104 | /** 105 | * 106 | */ 107 | public MultiUDPReceiver() { 108 | super(); 109 | } 110 | 111 | 112 | private String getKey(Message message) { 113 | return message.getHeader().getID() + "_" 114 | + message.getQuestion().getName().toString() + "_" 115 | + message.getQuestion().getType(); 116 | } 117 | 118 | public void registerReceiver(Message message, ForwardAnswer forwardAnswer) { 119 | answers.put(getKey(message), forwardAnswer); 120 | } 121 | 122 | public ForwardAnswer getAnswer(Message message) { 123 | return answers.get(getKey(message)); 124 | } 125 | 126 | /** 127 | * Add answer to remove queue and remove when timeout. 128 | * @param message 129 | * @param timeOut 130 | */ 131 | public void delayRemoveAnswer(Message message, long timeOut) { 132 | delayRemoveQueue.add(new DelayStringKey(getKey(message), timeOut)); 133 | } 134 | 135 | private void receive() { 136 | ExecutorService processExecutors = threadPools.getUdpReceiverExecutor(); 137 | final ByteBuffer byteBuffer = ByteBuffer.allocate(dnsPackageLength); 138 | while (true) { 139 | try { 140 | byteBuffer.clear(); 141 | final SocketAddress remoteAddress = datagramChannel 142 | .receive(byteBuffer); 143 | final byte[] answer = Arrays.copyOfRange(byteBuffer.array(), 0, 144 | dnsPackageLength); 145 | processExecutors.submit(new Runnable() { 146 | 147 | @Override 148 | public void run() { 149 | try { 150 | final Message message = new Message(answer); 151 | forwardAnswerProcessor.handleAnswer(answer, message, remoteAddress, getAnswer(message)); 152 | } catch (Throwable e) { 153 | logger.warn("forward exception " + e); 154 | } 155 | } 156 | }); 157 | 158 | } catch (Throwable e) { 159 | logger.warn("receive exception" + e); 160 | } 161 | } 162 | } 163 | 164 | /* 165 | * (non-Javadoc) 166 | * 167 | * @see 168 | * org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 169 | */ 170 | @Override 171 | public void afterPropertiesSet() throws Exception { 172 | datagramChannel = DatagramChannel.open(); 173 | try { 174 | datagramChannel.socket().bind(new InetSocketAddress(PORT_RECEIVE)); 175 | } catch (IOException e) { 176 | System.err.println("Startup fail, maybe another process is running."); 177 | logger.error("Startup fail, maybe another process is running.", e); 178 | System.exit(-1); 179 | } 180 | datagramChannel.configureBlocking(true); 181 | Thread threadForReceive = new Thread(new Runnable() { 182 | 183 | @Override 184 | public void run() { 185 | receive(); 186 | 187 | } 188 | }); 189 | threadForReceive.setDaemon(true); 190 | threadForReceive.start(); 191 | Thread threadForRemove = new Thread(new Runnable() { 192 | 193 | @Override 194 | public void run() { 195 | removeAnswerFromQueue(); 196 | 197 | } 198 | }); 199 | threadForRemove.setDaemon(true); 200 | threadForRemove.start(); 201 | } 202 | 203 | private void removeAnswerFromQueue() { 204 | while (true) { 205 | try { 206 | DelayStringKey delayRemoveKey = delayRemoveQueue.take(); 207 | ForwardAnswer forwardAnswer = answers.get(delayRemoveKey.getKey()); 208 | if (forwardAnswer != null && forwardAnswer.getTempAnswer() != null) { 209 | forwardAnswer.getResponser().response(forwardAnswer.getTempAnswer().toWire()); 210 | } 211 | answers.remove(delayRemoveKey.getKey()); 212 | if (logger.isDebugEnabled()) { 213 | logger.debug("remove key " + delayRemoveKey.getKey()); 214 | } 215 | } catch (Exception e) { 216 | logger.warn("remove answer error", e); 217 | } 218 | } 219 | } 220 | 221 | /** 222 | * @return the datagramChannel 223 | */ 224 | public DatagramChannel getDatagramChannel() { 225 | return datagramChannel; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/utils/DoubleKeyMap.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.utils; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * @author yihua.huang@dianping.com 7 | * @date Dec 14, 2012 8 | */ 9 | public class DoubleKeyMap extends MultiKeyMapBase { 10 | private Map> map; 11 | 12 | public DoubleKeyMap() { 13 | init(); 14 | } 15 | 16 | public DoubleKeyMap(Map> map) { 17 | this(map,DEFAULT_CLAZZ); 18 | } 19 | 20 | public DoubleKeyMap(Class protoMapClass) { 21 | super(protoMapClass); 22 | init(); 23 | } 24 | 25 | private void init() { 26 | if (map == null) { 27 | map = this.>newMap(); 28 | } 29 | } 30 | 31 | /** 32 | * init map with protoMapClass 33 | * 34 | * @param protoMapClass 35 | */ 36 | @SuppressWarnings("rawtypes") 37 | public DoubleKeyMap(Map> map, Class protoMapClass) { 38 | super(protoMapClass); 39 | this.map = map; 40 | init(); 41 | } 42 | 43 | /** 44 | * @param key 45 | * @return 46 | */ 47 | public Map get(K1 key) { 48 | return map.get(key); 49 | } 50 | 51 | /** 52 | * @param key1 53 | * @param key2 54 | * @return 55 | */ 56 | public V get(K1 key1, K2 key2) { 57 | if (get(key1) == null) { 58 | return null; 59 | } 60 | return get(key1).get(key2); 61 | } 62 | 63 | 64 | /** 65 | * @param key1 66 | * @param submap 67 | * @return 68 | */ 69 | public V put(K1 key1, Map submap) { 70 | return put(key1, submap); 71 | } 72 | 73 | /** 74 | * @param key1 75 | * @param key2 76 | * @param value 77 | * @return 78 | */ 79 | public V put(K1 key1, K2 key2, V value) { 80 | if (map.get(key1) == null) { 81 | map.put(key1, this.newMap()); 82 | } 83 | return get(key1).put(key2, value); 84 | } 85 | 86 | /** 87 | * @param key1 88 | * @param key2 89 | * @return 90 | */ 91 | public V remove(K1 key1, K2 key2) { 92 | if (get(key1) == null) { 93 | return null; 94 | } 95 | V remove = get(key1).remove(key2); 96 | // 如果上一级map为空,把它也回收掉 97 | if (get(key1).size() == 0) { 98 | remove(key1); 99 | } 100 | return remove; 101 | } 102 | 103 | /** 104 | * @param key1 105 | * @return 106 | */ 107 | public Map remove(K1 key1) { 108 | Map remove = map.remove(key1); 109 | return remove; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/utils/MultiKeyMapBase.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.utils; 2 | 3 | /** 4 | * @author yihua.huang@dianping.com 5 | * @date Dec 14, 2012 6 | */ 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | /** 12 | * multikey map, some basic objects * 13 | * 14 | * @author yihua.huang 15 | */ 16 | public abstract class MultiKeyMapBase { 17 | 18 | protected static final Class DEFAULT_CLAZZ = HashMap.class; 19 | @SuppressWarnings("rawtypes") 20 | private Class protoMapClass = DEFAULT_CLAZZ; 21 | 22 | public MultiKeyMapBase() { 23 | } 24 | 25 | @SuppressWarnings("rawtypes") 26 | public MultiKeyMapBase(Class protoMapClass) { 27 | this.protoMapClass = protoMapClass; 28 | } 29 | 30 | @SuppressWarnings("unchecked") 31 | protected Map newMap() { 32 | try { 33 | return (Map) protoMapClass.newInstance(); 34 | } catch (InstantiationException e) { 35 | throw new IllegalArgumentException("wrong proto type map " 36 | + protoMapClass); 37 | } catch (IllegalAccessException e) { 38 | throw new IllegalArgumentException("wrong proto type map " 39 | + protoMapClass); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/utils/RecordBuilder.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.utils; 2 | 3 | import org.xbill.DNS.*; 4 | import us.codecraft.blackhole.config.Configure; 5 | 6 | import java.net.UnknownHostException; 7 | 8 | /** 9 | * @author yihua.huang@dianping.com 10 | * @date Dec 14, 2012 11 | */ 12 | public class RecordBuilder { 13 | 14 | private Configure configure = SpringLocator.getBean(Configure.class); 15 | 16 | private int type; 17 | 18 | private int dclass; 19 | 20 | private String answer; 21 | 22 | private Name name; 23 | 24 | private int priority = configure.getMxPriory(); 25 | 26 | private long ttl = configure.getTTL(); 27 | 28 | public Record toRecord() throws UnknownHostException, TextParseException { 29 | switch (type) { 30 | case Type.A: 31 | return new ARecord(name, dclass, ttl, Address.getByAddress(answer)); 32 | case Type.MX: 33 | return new MXRecord(name, dclass, ttl, priority, new Name(answer)); 34 | case Type.PTR: 35 | return new PTRRecord(name, dclass, ttl, new Name(answer)); 36 | case Type.CNAME: 37 | return new CNAMERecord(name, dclass, ttl, new Name(answer)); 38 | default: 39 | throw new IllegalStateException("type " + Type.string(type) 40 | + " is not supported "); 41 | } 42 | } 43 | 44 | /** 45 | * @param type 46 | * the type to set 47 | */ 48 | public RecordBuilder type(int type) { 49 | this.type = type; 50 | return this; 51 | } 52 | 53 | /** 54 | * @param dclass 55 | * the dclass to set 56 | */ 57 | public RecordBuilder dclass(int dclass) { 58 | this.dclass = dclass; 59 | return this; 60 | } 61 | 62 | /** 63 | * @param name 64 | * the name to set 65 | */ 66 | public RecordBuilder name(Name name) { 67 | this.name = name; 68 | return this; 69 | } 70 | 71 | /** 72 | * @param priority 73 | * the priority to set 74 | */ 75 | public RecordBuilder priority(int priority) { 76 | this.priority = priority; 77 | return this; 78 | } 79 | 80 | /** 81 | * @param ttl 82 | * the ttl to set 83 | */ 84 | public RecordBuilder ttl(long ttl) { 85 | this.ttl = ttl; 86 | return this; 87 | } 88 | 89 | /** 90 | * @param answer 91 | * the answer to set 92 | */ 93 | public RecordBuilder answer(String answer) { 94 | this.answer = answer; 95 | return this; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/utils/RecordUtils.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.utils; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.xbill.DNS.Message; 5 | import org.xbill.DNS.Record; 6 | import org.xbill.DNS.Section; 7 | import org.xbill.DNS.Type; 8 | import us.codecraft.blackhole.config.Configure; 9 | 10 | /** 11 | * @author yihua.huang@dianping.com 12 | * @date Dec 19, 2012 13 | */ 14 | public class RecordUtils { 15 | 16 | public static long maxTTL(Record[] records) { 17 | if (records == null || records.length == 0) { 18 | return Configure.DEFAULT_TTL; 19 | } 20 | long max = 0; 21 | for (Record record : records) { 22 | if (record.getTTL() > max) { 23 | max = record.getTTL(); 24 | } 25 | } 26 | return max; 27 | } 28 | 29 | public static boolean hasAnswer(Message message) { 30 | Record[] sectionArray = message.getSectionArray(Section.ANSWER); 31 | if (sectionArray == null || sectionArray.length == 0) { 32 | return false; 33 | } else { 34 | return true; 35 | } 36 | 37 | } 38 | 39 | public static long minTTL(Record[] records) { 40 | if (records == null || records.length == 0) { 41 | return Configure.DEFAULT_TTL; 42 | } 43 | long min = Integer.MAX_VALUE; 44 | for (Record record : records) { 45 | if (record.getTTL() < min) { 46 | min = record.getTTL(); 47 | } 48 | } 49 | return min; 50 | } 51 | 52 | public static String recordKey(Record record) { 53 | return record.getName().toString() + " " 54 | + Type.string(record.getType()); 55 | } 56 | 57 | public static boolean isValidIpv4Address(String address) { 58 | if (StringUtils.isBlank(address)) { 59 | return false; 60 | } 61 | String[] items = address.split("\\."); 62 | if (items.length != 4) { 63 | return false; 64 | } 65 | for (String item : items) { 66 | try { 67 | int parseInt = Integer.parseInt(item); 68 | if (parseInt < 0 || parseInt > 255) { 69 | return false; 70 | } 71 | } catch (NumberFormatException e) { 72 | return false; 73 | } 74 | } 75 | return true; 76 | } 77 | 78 | public static boolean areValidIpv4Addresses(String addresses) { 79 | if (StringUtils.isBlank(addresses)) { 80 | return false; 81 | } 82 | String[] items = addresses.split("\\,"); 83 | for (String item : items) { 84 | if (!isValidIpv4Address(item)) { 85 | return false; 86 | } 87 | } 88 | return true; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /server/src/main/java/us/codecraft/blackhole/utils/SpringLocator.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.utils; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.beans.factory.NoSuchBeanDefinitionException; 5 | import org.springframework.context.ApplicationContext; 6 | 7 | /** 8 | * @author yihua.huang@dianping.com 9 | * @date Dec 15, 2012 10 | */ 11 | public class SpringLocator { 12 | 13 | public static ApplicationContext applicationContext; 14 | 15 | public static Object getBean(String name) throws BeansException { 16 | return applicationContext.getBean(name); 17 | } 18 | 19 | public static T getBean(String name, Class requiredType) 20 | throws BeansException { 21 | return applicationContext.getBean(name, requiredType); 22 | } 23 | 24 | public static T getBean(Class requiredType) throws BeansException { 25 | return applicationContext.getBean(requiredType); 26 | } 27 | 28 | public static Object getBean(String name, Object... args) 29 | throws BeansException { 30 | return applicationContext.getBean(name, args); 31 | } 32 | 33 | public static boolean containsBean(String name) { 34 | return applicationContext.containsBean(name); 35 | } 36 | 37 | public static boolean isSingleton(String name) 38 | throws NoSuchBeanDefinitionException { 39 | return applicationContext.isSingleton(name); 40 | } 41 | 42 | public static boolean isPrototype(String name) 43 | throws NoSuchBeanDefinitionException { 44 | return applicationContext.isPrototype(name); 45 | } 46 | 47 | public static boolean isTypeMatch(String name, Class targetType) 48 | throws NoSuchBeanDefinitionException { 49 | return applicationContext.isTypeMatch(name, targetType); 50 | } 51 | 52 | public static Class getType(String name) 53 | throws NoSuchBeanDefinitionException { 54 | return applicationContext.getType(name); 55 | } 56 | 57 | public static String[] getAliases(String name) { 58 | return applicationContext.getAliases(name); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /server/src/main/resources/ehcache.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 20 | 27 | -------------------------------------------------------------------------------- /server/src/main/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /server/src/main/resources/spring/applicationContext-blackhole.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /server/src/test/java/us/codecraft/blackhole/multiforward/ForwardTimerTest.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.multiforward; 2 | 3 | import org.junit.Test; 4 | 5 | import us.codecraft.blackhole.forward.ConnectionTimer; 6 | 7 | /** 8 | * @author yihua.huang@dianping.com 9 | * @date Feb 20, 2013 10 | */ 11 | public class ForwardTimerTest { 12 | 13 | private ConnectionTimer forwardTimer = new ConnectionTimer(); 14 | 15 | @Test 16 | public void testCheckForwardTimeForAddress() { 17 | System.out.println(forwardTimer 18 | .checkConnectTimeForAddress("173.194.72.106")); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server/src/test/java/us/codecraft/blackhole/multiforward/MultiReceiverTest.java: -------------------------------------------------------------------------------- 1 | package us.codecraft.blackhole.multiforward; 2 | 3 | import java.io.IOException; 4 | import java.net.InetAddress; 5 | 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | import us.codecraft.blackhole.forward.MultiUDPReceiver; 10 | 11 | /** 12 | * @author yihua.huang@dianping.com 13 | * @date Feb 16, 2013 14 | */ 15 | public class MultiReceiverTest { 16 | 17 | private MultiUDPReceiver multireReceiver; 18 | 19 | @Before 20 | public void setUp() { 21 | multireReceiver = new MultiUDPReceiver(); 22 | } 23 | 24 | @Test 25 | public void testReach() throws IOException { 26 | InetAddress inetAddress = InetAddress.getByName("203.161.230.171"); 27 | System.out.println(inetAddress.isReachable(1000)); 28 | } 29 | 30 | } 31 | --------------------------------------------------------------------------------