// id-dns: locate the servers responding to anycast DNS requests. // Copyright 2019 Zack Weinberg & // Shinyoung Cho // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. See the file LICENSE in the // top level of the source tree containing this file for further details, // or consult . const ABOUT_CMD: &str = r#" Use Akamai's "whoami" service to determine the IP addresses of the physical servers responding to DNS queries sent to anycast addresses. "#; // // Imports // #[macro_use] extern crate failure; use std::io; use std::net::IpAddr; use std::vec::Vec; use failure::Error; use maxminddb::Reader as GeoDB; use structopt::StructOpt; use tokio::runtime::current_thread::Runtime; use trust_dns_resolver::lookup::TxtLookup; // // Main loop // /// Error type for process_response. #[derive(Debug, Fail)] #[fail(display="No \"ns\" record in response")] struct NoNSRecordError; fn parse_ip_in_txt(value: &[u8]) -> Result { let value = std::str::from_utf8(value.as_ref())?; let value = value.parse::()?; Ok(value) } fn process_response(resolver: IpAddr, response: TxtLookup) -> (IpAddr, Result) { for txt in response { if let [tag, value] = txt.txt_data() { if tag.as_ref() == b"ns" { return (resolver, parse_ip_in_txt(value)); } } } (resolver, Err(NoNSRecordError.into())) } fn run_queries(args: CommandArgs, runtime: &mut Runtime) -> Vec<(IpAddr, Result)> { use std::convert::Infallible; use trust_dns_resolver::AsyncResolver; use trust_dns_resolver::config::*; use futures::future::Future; use futures::stream::{Stream, FuturesUnordered}; // Pending queries. let mut queries: FuturesUnordered), Error=Infallible>>> = FuturesUnordered::new(); for addr in args.resolvers { let (client, task) = AsyncResolver::new( ResolverConfig::from_parts( None, vec![], NameServerConfigGroup::from_ips_clear( &[addr], 53)), ResolverOpts::default()); runtime.spawn(task); queries.push(Box::new( client.txt_lookup("whoami.ds.akahelp.net.") .then(move |result| match result { Ok(response) => Ok(process_response(addr, response)), Err(e) => Ok((addr, Err(e.into()))) }) )); } runtime.block_on(queries.collect()).unwrap() } fn lookup_geoip(db: &GeoDB, resolver: IpAddr, addr: IpAddr) -> Result<[String; 5], Error> { use maxminddb::MaxMindDBError::AddressNotFoundError as ANF; let city: maxminddb::geoip2::City = db.lookup(addr)?; let location = city.location.ok_or_else( || ANF(String::from("location missing")))?; let longitude = location.longitude.ok_or_else( || ANF(String::from("longitude missing")))?; let latitude = location.latitude.ok_or_else( || ANF(String::from("longitude missing")))?; let country = city.registered_country.ok_or_else( || ANF(String::from("registered country missing")))?; let ccode = country.iso_code.ok_or_else( || ANF(String::from("longitude missing")))?; Ok([resolver.to_string(), addr.to_string(), ccode, longitude.to_string(), latitude.to_string()]) } fn inner_main(args: CommandArgs) -> i32 { use std::io::{LineWriter, Write}; // lock and line-buffer stderr (eprintln! doesn't line-buffer, which is // probably a bug, but we can work around it for now). let stderr = io::stderr(); let stderr = stderr.lock(); let mut stderr = LineWriter::new(stderr); // Try to read the geoip database, if any. // This uses match blocks so it can return on error. let geodb = match args.geoip.as_ref() { None => None, Some(s) => match GeoDB::open(s) { Ok(db) => Some(db), Err(e) => { writeln!(stderr, "id-dns: {}: {}", s, e).unwrap(); return 1; } } }; // Do all the actual queries, then tear down the network stack. let results = { let mut runtime = Runtime::new().unwrap(); let results = run_queries(args, &mut runtime); if let Err(e) = runtime.run() { writeln!(stderr, "id-dns: {}", e).unwrap(); return 1; } results }; // Lock and buffer stdout. let stdout = io::stdout(); let stdout = stdout.lock(); let mut stdout = csv::Writer::from_writer(stdout); if let Err(e) = if geodb.is_none() { stdout.write_record(&["resolver", "address"]) } else { stdout.write_record( &["resolver", "address", "country", "longitude", "latitude"] ) } { writeln!(stderr, "id-dns: {}", e).unwrap(); return 1; } // Produce results. let mut status = 0; for (resolver, result) in results { match result { Err(e) => { status = 1; writeln!(stderr, "id-dns: {}: {}", resolver, e).unwrap(); }, Ok(addr) => match geodb { None => { stdout.write_record(&[resolver.to_string(), addr.to_string()]) .unwrap_or_else(|e| { status = 1; writeln!(stderr, "id-dns: {}", e).unwrap(); }); }, Some(ref db) => match lookup_geoip(db, resolver, addr) { Err(e) => { status = 1; writeln!(stderr, "id-dns: {}->{}: {}", resolver, addr, e).unwrap(); }, Ok(record) => { stdout.write_record(&record) .unwrap_or_else(|e| { status = 1; writeln!(stderr, "id-dns: {}", e).unwrap(); }); } } } } } stdout.flush().unwrap_or_else(|e| { status = 1; writeln!(stderr, "id-dns: {}", e).unwrap(); }); status } // // Command line parsing. // #[derive(Debug, StructOpt)] #[structopt( name="id-dns", raw(about="ABOUT_CMD"), raw(setting="structopt::clap::AppSettings::DeriveDisplayOrder"), raw(setting="structopt::clap::AppSettings::UnifiedHelpMessage"), )] struct CommandArgs { /// Anycast addresses of public DNS resolvers; a request to Akamai's /// whoami service will be sent via each. #[structopt(required=true)] resolvers: Vec, /// If provided, look up the results of the whoami queries in this /// MaxMind-style geoip database and report latitude, longitude, /// country code, and city name as well as the IP address of each. /// This should be a PathBuf, but maxminddb::open takes a &str. #[structopt(short="g", long="geoip")] geoip: Option } fn main() { std::process::exit(inner_main(CommandArgs::from_args())) }