6 require 'public_suffix'
12 whitelist_file: "ns-whitelist.txt",
13 blacklist_file: "ns-blacklist.txt",
14 cdnlist_file: "cdn-testlist.txt",
15 chnroutes_file: "/usr/share/china_ip_list.txt"
18 @whitelist = load_list whitelist_file
19 @blacklist = load_list blacklist_file
20 @cdnlist = load_list cdnlist_file
21 @tld_ns = Concurrent::Hash.new
24 @chnroutes = load_list(chnroutes_file).map { |line| IPAddr.new line }
26 puts "Failed to load chnroutes, CDN check disabled".red
31 def load_list(filename)
32 File.readlines(filename).each do |line|
33 line if !line.chomp!.empty? and !line.start_with?("#")
37 def test_cn_ip(domain, response: nil)
39 raise "chnroutes not loaded"
43 if response != nil && !response.empty?
44 answers = response.filter_map { |n, r| r if n.to_s == domain && r.class == Resolv::DNS::Resource::IN::A }
47 if answers == nil || answers.empty?
48 answers = resolve(domain, 'A')
51 answers.each do |answer|
52 answer = IPAddr.new answer.address.to_s
53 if @chnroutes.any? { |range| range.include? answer }
61 def resolve(domain, rdtype="A", server: nil, with_glue: false)
62 rdtype = Kernel.const_get("Resolv::DNS::Resource::IN::#{rdtype}")
65 resolver = Resolv::DNS.new
67 resolver = Resolv::DNS.new(nameserver: @dns)
70 server = [server] unless server.is_a? Array
71 resolver = Resolv::DNS.new(nameserver: server)
74 resolver.getresources(domain, rdtype)
76 # Workaround for https://github.com/ruby/resolv/issues/27
79 n0 = Resolv::DNS::Name.create domain
80 resolver.fetch_resource(domain, rdtype) {|reply, reply_name|
81 reply.each_resource {|n, ttl, data|
82 if n0 == n && data.is_a?(rdtype)
93 def get_ns_for_tld(tld)
94 if !@tld_ns.has_key? tld
95 answers = resolve(tld + ".", "NS")
97 answers.each do |answer|
98 ips = resolve answer.name.to_s
100 results << ip.address.to_s
103 @tld_ns[tld] = results
109 def check_whitelist(nameservers)
110 @whitelist.each { |pattern| nameservers.each {|ns| return pattern if ns.end_with? pattern }}
114 def check_blacklist(nameservers)
115 @blacklist.each { |pattern| nameservers.each {|ns| return pattern if ns.end_with? pattern }}
119 def check_cdnlist(domain)
123 def check_domain(domain, enable_cdnlist: true)
127 tld_ns = get_ns_for_tld(PublicSuffix.parse(domain, ignore_private: true).tld)
128 rescue PublicSuffix::DomainNotAllowed, PublicSuffix::DomainInvalid
129 yield nil, "Invalid domain #{domain}"
132 response, glue = self.resolve(
138 response.each do |rdata|
140 nameserver = rdata.name.to_s
141 if PublicSuffix.valid?(nameserver, ignore_private: true)
142 nameservers << nameserver
145 if result = check_whitelist(nameservers)
146 yield true, "NS Whitelist #{result} matched for domain #{domain}" if block_given?
149 rescue NoMethodError => e
150 puts "Ignoring error: #{e}"
155 @cdnlist.each do |testdomain|
156 if testdomain == domain or testdomain.end_with? "." + domain
157 if result = check_cdnlist(testdomain)
158 yield true, "CDN List matched (#{testdomain}) and verified #{result} for domain #{domain}" if block_given?
164 # Assuming CDNList for non-TLDs
165 if domain.count(".") > 1 and PublicSuffix.domain(domain, ignore_private: true) != domain
166 if result = check_cdnlist(domain)
167 yield true, "CDN List matched and verified #{result} for domain #{domain}" if block_given?
168 return true if result
173 if result = check_blacklist(nameservers)
174 yield false, "NS Blacklist #{result} matched for domain #{domain}" if block_given?
178 nameservers.each do |nameserver|
179 if result = test_cn_ip(nameserver, response: glue)
180 yield true, "NS #{nameserver} verified #{result} for domain #{domain}" if block_given?
185 if !nameservers.empty?
186 yield false, "NS #{nameservers[0]} not verified for domain #{domain}" if block_given?
189 yield nil, "Failed to get correct name server for domain #{domain}" if block_given?
194 def check_domain_verbose(domain, show_green: false, **kwargs)
195 check_domain(domain, **kwargs) do |result, message|
197 puts message.green if show_green
198 elsif result == false
206 def check_domain_list(domain_list, sample: 30, show_green: false, jobs: Concurrent.processor_count)
207 domains = load_list domain_list
209 domains = domains.sample(sample)
213 pool = Concurrent::FixedThreadPool.new(jobs)
214 domains.each do |domain|
216 if check_domain_verbose(domain, show_green: show_green)
217 yield domain if block_given?
222 pool.wait_for_termination
226 # Operates on the raw file to preserve commented out lines
227 def CheckRedundant(lines, disabled_lines, domain)
228 new_line = "server=/#{domain}/114.114.114.114\n"
229 disabled_line = "#server=/#{domain}/114.114.114.114"
230 if lines.include? new_line
231 puts "Domain already exists: #{domain}"
233 elsif disabled_lines.any? { |line| line.start_with? disabled_line }
234 puts "Domain already disabled: #{domain}"
237 # Check for duplicates
239 while test_domain.include? '.'
240 test_domain = test_domain.partition('.').last
241 _new_line = "server=/#{test_domain}/114.114.114.114\n"
242 _disabled_line = "#server=/#{test_domain}/114.114.114.114"
243 if lines.include? _new_line
244 puts "Redundant domain already exists: #{test_domain}"
246 elsif disabled_lines.any? { |line| line.start_with? _disabled_line }
247 puts "Redundant domain already disabled: #{test_domain}"
259 options = OpenStruct.new
260 options.file = "accelerated-domains.china.raw.txt"
262 options.verbose = false
265 OptionParser.new do |opts|
266 opts.banner = 'A simple verify library for dnsmasq-china-list'
268 opts.on("-f", "--file FILE", "File to check") do |f|
272 opts.on("-s", "--sample SAMPLE", Integer, "Verify only a limited sample. Pass 0 to example all entries") do |s|
276 opts.on("-v", "--[no-]verbose", "Show green results") do |v|
280 opts.on("-d", "--domain DOMAIN", "Verify a domain instead of checking a list. Will ignore the other list options.") do |d|
284 opts.on("-D", "--dns DNS", "Specify a DNS server to use instead of the system default one.") do |d|
288 opts.on_tail("-h", "--help", "Show this message") do
294 v = ChinaListVerify.new options.dns
297 exit v.check_domain_verbose(options.domain, show_green: options.verbose) == true
299 v.check_domain_list(options.file, sample: options.sample, show_green: options.verbose)