Last active
April 19, 2022 16:37
-
-
Save neetsdkasu/3fb7123375d4b5a43289292ccd84015d to your computer and use it in GitHub Desktop.
DNSクエリ見てみる
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ネットワークアダプタの設定で | |
// DNSのIPを127.7.7.7に設定してPC内からのDNSクエリを盗み見る | |
// nslookup example.com 127.7.7.7 とかで動くかテストしながら開発 | |
fn main() -> std::io::Result<()> { | |
eprintln!("START"); | |
let mut bufs = Bufs::new(); | |
let res = bufs.start(); | |
for (i, (req, ans)) in bufs.iter().enumerate() { | |
println!("QUERY: {i}"); | |
println!("[REQUEST]"); | |
show_packet(req); | |
println!("[ANSWER]"); | |
show_packet(ans); | |
} | |
eprintln!("STOP"); | |
res | |
} | |
// DNS照会を受信する最大数 | |
// この数を超えたらプログラムは終了するため、パソコンはDNS照会できなくなる(笑) | |
// (タイムアウトでも終了するため最大数受信できるとは限らない) | |
const COUNT: usize = 100; | |
#[derive(Debug)] | |
struct Bufs { | |
req_bufs: Vec<Vec<u8>>, | |
req_sizes: Vec<usize>, | |
ans_bufs: Vec<Vec<u8>>, | |
ans_sizes: Vec<usize>, | |
count: usize, | |
} | |
impl Bufs { | |
fn new() -> Self { | |
Self { | |
req_bufs: vec![vec![0_u8; 512]; COUNT], | |
req_sizes: vec![0_usize; COUNT], | |
ans_bufs: vec![vec![0_u8; 512]; COUNT], | |
ans_sizes: vec![0_usize; COUNT], | |
count: 0, | |
} | |
} | |
fn start(&mut self) -> std::io::Result<()> { | |
use std::net::{SocketAddr, UdpSocket}; | |
use std::time::Duration; | |
// DNS照会を盗み見るためだけの偽のDNSサーバー | |
// 適当なローカルホストIPアドレスを割り当てる | |
let fake_dns = SocketAddr::from(([127, 7, 7, 7], 53)); | |
eprintln!("FAKE DNS SERVER: {:?}", fake_dns); | |
// 実在のDNSサーバ 8.8.8.8:53 など | |
let dns = SocketAddr::from(([8, 8, 4, 4], 53)); | |
// 実在のDNSサーバとやりとりするためのソケット用のアドレス | |
// ルータなどLAN内でPCに割り当てられたIPアドレス(192.168.10.3など)と適当な空きポート番号(9876など) | |
let cli = SocketAddr::from(([192, 168, 10, 18], 9981)); | |
let timeout = Duration::from_secs(1); | |
let srv_socket = UdpSocket::bind(fake_dns)?; | |
srv_socket.set_read_timeout(None)?; | |
srv_socket.set_write_timeout(Some(timeout))?; | |
let cli_socket = UdpSocket::bind(cli)?; | |
cli_socket.set_read_timeout(Some(timeout))?; | |
cli_socket.set_write_timeout(Some(timeout))?; | |
for i in 0..COUNT { | |
// DNS照会の受信 | |
let (number_of_bytes, req_addr) = srv_socket.recv_from(&mut self.req_bufs[i])?; | |
self.req_sizes[i] = number_of_bytes; | |
// DNS照会の転送 | |
let req = &self.req_bufs[i][..number_of_bytes]; | |
let _number_of_bytes = cli_socket.send_to(req, dns)?; | |
// DNS照会の応答の受信 | |
let (number_of_bytes, _) = cli_socket.recv_from(&mut self.ans_bufs[i])?; | |
self.ans_sizes[i] = number_of_bytes; | |
// DNS照会の応答の転送 | |
let ans = &self.ans_bufs[i][..number_of_bytes]; | |
let _number_of_bytes = srv_socket.send_to(ans, req_addr)?; | |
self.count += 1; | |
} | |
Ok(()) | |
} | |
fn iter(&self) -> BufsIterator<'_> { | |
BufsIterator { bufs: self, i: 0 } | |
} | |
} | |
#[derive(Debug)] | |
struct BufsIterator<'a> { | |
bufs: &'a Bufs, | |
i: usize, | |
} | |
impl<'a> Iterator for BufsIterator<'a> { | |
type Item = (&'a [u8], &'a [u8]); | |
fn next(&mut self) -> Option<Self::Item> { | |
if self.i >= self.bufs.count { | |
return None; | |
} | |
let i = self.i; | |
self.i += 1; | |
let number_of_bytes = self.bufs.req_sizes[i]; | |
let req = &self.bufs.req_bufs[i][..number_of_bytes]; | |
let number_of_bytes = self.bufs.ans_sizes[i]; | |
let ans = &self.bufs.ans_bufs[i][..number_of_bytes]; | |
Some((req, ans)) | |
} | |
} | |
const BAR: &str = | |
"--------------------------------------------------------------------------------"; | |
fn show_packet(buf: &[u8]) -> Option<()> { | |
let number_of_bytes = buf.len(); | |
println!("number_of_bytes: {number_of_bytes}"); | |
for chunk in buf.chunks(80) { | |
for ch in chunk { | |
let ch = *ch as char; | |
if ch.is_ascii_graphic() { | |
print!("{ch}"); | |
} else { | |
print!("."); | |
} | |
} | |
println!(); | |
} | |
match Message::read(buf) { | |
Some(message) => show_message(&message), | |
None => println!("failed to read message"), | |
} | |
println!("{}", BAR); | |
Some(()) | |
} | |
fn show_message(message: &Message) { | |
let header = &message.header; | |
show_header(header); | |
for (i, question) in message.questions.iter().enumerate() { | |
println!(" QUESTION SECTION: {} / {}", i + 1, header.qdcount); | |
show_question(question); | |
} | |
for (i, rr) in message.answers.iter().enumerate() { | |
println!(" ANSWER SECTION: {} / {}", i + 1, header.ancount); | |
show_resource_record(rr); | |
} | |
for (i, rr) in message.authorities.iter().enumerate() { | |
println!(" AUTHORITY RECORD SECTION: {} / {}", i + 1, header.nscount); | |
show_resource_record(rr); | |
} | |
for (i, rr) in message.additionals.iter().enumerate() { | |
println!( | |
" ADDITIONAL RECORD SECTION: {} / {}", | |
i + 1, | |
header.arcount | |
); | |
show_resource_record(rr); | |
} | |
} | |
fn show_header(header: &Header) { | |
println!(" ID: {}", header.id); | |
show_param(&header.param); | |
println!(" QDCOUNT: {}", header.qdcount); | |
println!(" ANCOUNT: {}", header.ancount); | |
println!(" NSCOUNT: {}", header.nscount); | |
println!(" ARCOUNT: {}", header.arcount); | |
} | |
fn show_param(param: &Param) { | |
let qr_text = get_qr_text(param.qr); | |
println!(" QR: {} {}", param.qr, qr_text); | |
let opcode_text = get_opcode_text(param.opcode); | |
println!(" Opcode: {} {}", param.opcode, opcode_text); | |
println!(" AA: {}", param.aa); | |
println!(" TC: {}", param.tc); | |
println!(" RD: {}", param.rd); | |
println!(" RA: {}", param.ra); | |
println!(" Z: {}", param.z); | |
let rcode_text = get_rcode_text(param.rcode); | |
println!(" RCODE: {} {}", param.rcode, rcode_text); | |
} | |
fn show_question(question: &Question) { | |
for word in &question.name { | |
println!(" LENGTH: {:<3} {}", word.len(), word); | |
} | |
let qtype_text = get_type_text(question.qtype); | |
println!(" QTYPE: {} {}", question.qtype, qtype_text); | |
let qclass_text = get_class_text(question.qclass); | |
println!(" QCLASS: {} {}", question.qclass, qclass_text); | |
} | |
fn show_resource_record(rr: &ResourceRecord) { | |
for word in &rr.name { | |
println!(" LENGTH: {:<3} {}", word.len(), word); | |
} | |
let rrtype_text = get_type_text(rr.rrtype); | |
println!(" TYPE: {} {}", rr.rrtype, rrtype_text); | |
let rrclass_text = get_class_text(rr.rrclass); | |
println!(" CLASS: {} {}", rr.rrclass, rrclass_text); | |
println!(" TTL: {}", rr.ttl); | |
println!(" RDLENGTH: {}", rr.rdlength); | |
println!(" RDATA: {:?}", rr.rdata); | |
} | |
#[derive(Debug)] | |
struct Message { | |
header: Header, | |
questions: Vec<Question>, | |
answers: Vec<ResourceRecord>, | |
authorities: Vec<ResourceRecord>, | |
additionals: Vec<ResourceRecord>, | |
} | |
#[derive(Debug)] | |
struct Header { | |
id: u16, | |
param: Param, | |
qdcount: u16, | |
ancount: u16, | |
nscount: u16, | |
arcount: u16, | |
} | |
#[derive(Debug)] | |
struct Param { | |
qr: u16, | |
opcode: u16, | |
aa: u16, | |
tc: u16, | |
rd: u16, | |
ra: u16, | |
z: u16, | |
rcode: u16, | |
} | |
#[derive(Debug)] | |
struct Question { | |
name: Vec<String>, | |
qtype: u16, | |
qclass: u16, | |
} | |
#[derive(Debug)] | |
struct ResourceRecord { | |
name: Vec<String>, | |
rrtype: u16, | |
rrclass: u16, | |
ttl: u32, | |
rdlength: u16, | |
rdata: Box<RData>, | |
} | |
#[allow(dead_code)] | |
#[derive(Debug)] | |
enum RData { | |
A { | |
address: [u8; 4], | |
}, | |
Cname { | |
cname: Vec<String>, | |
}, | |
Mb { | |
madname: Vec<String>, | |
}, | |
Md { | |
madname: Vec<String>, | |
}, | |
Mf { | |
madname: Vec<String>, | |
}, | |
Mg { | |
mgmname: Vec<String>, | |
}, | |
Minfo { | |
rmailbx: Vec<String>, | |
emailbx: Vec<String>, | |
}, | |
Mr { | |
newname: Vec<String>, | |
}, | |
Mx { | |
preference: u16, | |
exchange: Vec<String>, | |
}, | |
Null { | |
anything: Vec<u8>, | |
}, | |
Ns { | |
nsdname: Vec<String>, | |
}, | |
Ptr { | |
ptrdname: Vec<String>, | |
}, | |
Soa { | |
nname: Vec<String>, | |
rname: Vec<String>, | |
serial: u32, | |
refresh: u32, | |
retry: u32, | |
expire: u32, | |
minimum: u32, | |
}, | |
NotImlemented { | |
raw: Vec<u8>, | |
}, | |
Unknown { | |
raw: Vec<u8>, | |
}, | |
} | |
impl Message { | |
fn read(buf: &[u8]) -> Option<Self> { | |
let orig_buf = buf; | |
let (buf, header) = Header::read(buf)?; | |
let mut questions: Vec<Question> = Vec::new(); | |
let mut buf = buf; | |
for _ in 0..header.qdcount { | |
let (rest, question) = Question::read(orig_buf, buf)?; | |
buf = rest; | |
questions.push(question); | |
} | |
let mut answers: Vec<ResourceRecord> = Vec::new(); | |
for _ in 0..header.ancount { | |
let (rest, answer) = ResourceRecord::read(orig_buf, buf)?; | |
buf = rest; | |
answers.push(answer); | |
} | |
let mut authorities: Vec<ResourceRecord> = Vec::new(); | |
for _ in 0..header.nscount { | |
let (rest, authority) = ResourceRecord::read(orig_buf, buf)?; | |
buf = rest; | |
authorities.push(authority); | |
} | |
let mut additionals: Vec<ResourceRecord> = Vec::new(); | |
for _ in 0..header.arcount { | |
let (rest, additional) = ResourceRecord::read(orig_buf, buf)?; | |
buf = rest; | |
additionals.push(additional); | |
} | |
let message = Self { | |
header, | |
questions, | |
answers, | |
authorities, | |
additionals, | |
}; | |
Some(message) | |
} | |
} | |
impl Header { | |
fn read(buf: &[u8]) -> Option<(&[u8], Self)> { | |
let (buf, id) = read_u16(buf)?; | |
let (buf, value) = read_u16(buf)?; | |
let param = Param::from(value); | |
let (buf, qdcount) = read_u16(buf)?; | |
let (buf, ancount) = read_u16(buf)?; | |
let (buf, nscount) = read_u16(buf)?; | |
let (buf, arcount) = read_u16(buf)?; | |
let header = Self { | |
id, | |
param, | |
qdcount, | |
ancount, | |
nscount, | |
arcount, | |
}; | |
Some((buf, header)) | |
} | |
} | |
impl From<u16> for Param { | |
fn from(value: u16) -> Self { | |
Self { | |
qr: (value >> 15) & 0b1, | |
opcode: (value >> 11) & 0b1111, | |
aa: (value >> 10) & 0b1, | |
tc: (value >> 9) & 0b1, | |
rd: (value >> 8) & 0b1, | |
ra: (value >> 7) & 0b1, | |
z: (value >> 4) & 0b111, | |
rcode: value & 0b1111, | |
} | |
} | |
} | |
impl Question { | |
fn read<'a, 'b>(orig_buf: &'a [u8], buf: &'b [u8]) -> Option<(&'b [u8], Self)> { | |
let (buf, name) = read_domain_name(orig_buf, buf)?; | |
let (buf, qtype) = read_u16(buf)?; | |
let (buf, qclass) = read_u16(buf)?; | |
let question = Self { | |
name, | |
qtype, | |
qclass, | |
}; | |
Some((buf, question)) | |
} | |
} | |
impl ResourceRecord { | |
fn read<'a, 'b>(orig_buf: &'a [u8], buf: &'b [u8]) -> Option<(&'b [u8], Self)> { | |
let (buf, name) = read_domain_name(orig_buf, buf)?; | |
let (buf, rrtype) = read_u16(buf)?; | |
let (buf, rrclass) = read_u16(buf)?; | |
let (buf, ttl) = read_u32(buf)?; | |
let (buf, rdlength) = read_u16(buf)?; | |
let (rdata_buf, buf) = try_split_at(buf, rdlength as usize)?; | |
let rr = Self { | |
name, | |
rrtype, | |
rrclass, | |
ttl, | |
rdlength, | |
rdata: read_rdata(orig_buf, rrtype, rdata_buf)?, | |
}; | |
Some((buf, rr)) | |
} | |
} | |
fn read_rdata(orig_buf: &[u8], rrtype: u16, rdata_buf: &[u8]) -> Option<Box<RData>> { | |
let rdata = match rrtype { | |
1 => { | |
// A | |
if let Ok(address) = rdata_buf.try_into() { | |
RData::A { address } | |
} else { | |
return None; | |
} | |
} | |
2 => { | |
// NS | |
let (buf, nsdname) = read_domain_name(orig_buf, rdata_buf)?; | |
if buf.is_empty() { | |
RData::Ns { nsdname } | |
} else { | |
return None; | |
} | |
} | |
3 => { | |
// MD | |
let (buf, madname) = read_domain_name(orig_buf, rdata_buf)?; | |
if buf.is_empty() { | |
RData::Md { madname } | |
} else { | |
return None; | |
} | |
} | |
4 => { | |
// MF | |
let (buf, madname) = read_domain_name(orig_buf, rdata_buf)?; | |
if buf.is_empty() { | |
RData::Mf { madname } | |
} else { | |
return None; | |
} | |
} | |
5 => { | |
// CNAME | |
let (buf, cname) = read_domain_name(orig_buf, rdata_buf)?; | |
if buf.is_empty() { | |
RData::Cname { cname } | |
} else { | |
return None; | |
} | |
} | |
6 => { | |
// SOA | |
let (buf, nname) = read_domain_name(orig_buf, rdata_buf)?; | |
let (buf, rname) = read_domain_name(orig_buf, buf)?; | |
let (buf, serial) = read_u32(buf)?; | |
let (buf, refresh) = read_u32(buf)?; | |
let (buf, retry) = read_u32(buf)?; | |
let (buf, expire) = read_u32(buf)?; | |
let (buf, minimum) = read_u32(buf)?; | |
if buf.is_empty() { | |
RData::Soa { | |
nname, | |
rname, | |
serial, | |
refresh, | |
retry, | |
expire, | |
minimum, | |
} | |
} else { | |
return None; | |
} | |
} | |
7 => { | |
// MB | |
let (buf, madname) = read_domain_name(orig_buf, rdata_buf)?; | |
if buf.is_empty() { | |
RData::Mb { madname } | |
} else { | |
return None; | |
} | |
} | |
8 => { | |
// MG | |
let (buf, mgmname) = read_domain_name(orig_buf, rdata_buf)?; | |
if buf.is_empty() { | |
RData::Mg { mgmname } | |
} else { | |
return None; | |
} | |
} | |
9 => { | |
// MR | |
let (buf, newname) = read_domain_name(orig_buf, rdata_buf)?; | |
if buf.is_empty() { | |
RData::Mr { newname } | |
} else { | |
return None; | |
} | |
} | |
10 => RData::Null { | |
anything: rdata_buf.to_vec(), | |
}, | |
12 => { | |
// PTR | |
let (buf, ptrdname) = read_domain_name(orig_buf, rdata_buf)?; | |
if buf.is_empty() { | |
RData::Ptr { ptrdname } | |
} else { | |
return None; | |
} | |
} | |
14 => { | |
// MINFO | |
let (buf, rmailbx) = read_domain_name(orig_buf, rdata_buf)?; | |
let (buf, emailbx) = read_domain_name(orig_buf, buf)?; | |
if buf.is_empty() { | |
RData::Minfo { rmailbx, emailbx } | |
} else { | |
return None; | |
} | |
} | |
15 => { | |
// MX | |
let (buf, preference) = read_u16(rdata_buf)?; | |
let (buf, exchange) = read_domain_name(orig_buf, buf)?; | |
if buf.is_empty() { | |
RData::Mx { | |
preference, | |
exchange, | |
} | |
} else { | |
return None; | |
} | |
} | |
11 | 13 | 16 | 28 | 252..=255 => RData::NotImlemented { | |
raw: rdata_buf.to_vec(), | |
}, | |
_ => RData::Unknown { | |
raw: rdata_buf.to_vec(), | |
}, | |
}; | |
Some(Box::new(rdata)) | |
} | |
fn read_domain_name<'a, 'b>( | |
orig_buf: &'a [u8], | |
mut buf: &'b [u8], | |
) -> Option<(&'b [u8], Vec<String>)> { | |
let mut name: Vec<String> = Vec::new(); | |
loop { | |
let (rest, length) = read_u8(buf)?; | |
if length == 0 { | |
return Some((rest, name)); | |
} | |
if length & 0b1100_0000 == 0b1100_0000 { | |
// 不適切なオフセットが与えられると無限ループする可能性・・・ | |
let (rest, pointer) = read_u16(buf)?; | |
let offset = (pointer & 0b0011_1111_1111_1111) as usize; | |
if orig_buf.len() <= offset { | |
return None; | |
} | |
let new_buf = &orig_buf[offset..]; | |
let (_, expanded) = read_domain_name(orig_buf, new_buf)?; | |
name.extend(expanded); | |
return Some((rest, name)); | |
} | |
buf = rest; | |
let mut word = String::new(); | |
for _ in 0..length { | |
let (rest, ch) = read_u8(buf)?; | |
buf = rest; | |
word.push(ch as char); | |
} | |
name.push(word); | |
} | |
} | |
fn get_qr_text(qr: u16) -> &'static str { | |
match qr { | |
0 => "Query", | |
1 => "Response", | |
_ => "unknown", | |
} | |
} | |
fn get_opcode_text(opcode: u16) -> &'static str { | |
match opcode { | |
0 => "Query", | |
1 => "IQuery", | |
2 => "Status", | |
_ => "reserve (RCF1035)", | |
} | |
} | |
fn get_rcode_text(rcode: u16) -> &'static str { | |
match rcode { | |
0 => "NoError (No Error)", | |
1 => "FormErr (Format Error)", | |
2 => "ServFail (Server Failure)", | |
3 => "NXDomain (Non-Existent Domain)", | |
_ => "unknown", | |
} | |
} | |
fn get_type_text(type_id: u16) -> &'static str { | |
match type_id { | |
1 => "A (a host address)", | |
2 => "NS (an authoritative name server)", | |
3 => "MD (a mail destination)", | |
4 => "MF (a mail forwarder)", | |
5 => "CNAME (the canonical name for an alias)", | |
6 => "SOA (marks the start of a zone of authority)", | |
7 => "MB (a mailbox domain name)", | |
8 => "MG (a mail gropu member)", | |
9 => "MR (a mail rename domain name)", | |
10 => "NULL (a null RR)", | |
11 => "WKS (a well known service description)", | |
12 => "PTR (a domain name pointer)", | |
13 => "HINFO (host infomation)", | |
14 => "MINFO (mailbox or mail list infomation)", | |
15 => "MX (mail exchange)", | |
16 => "TXT (text strings)", | |
28 => "AAAA (IP6 Address)", | |
252 => "AXFR (A request for a transfer of an entire zone)", | |
253 => "MAILB (A request for mailbox-related records)", | |
254 => "MAILA (A request for mail agent RRs)", | |
255 => "* (A request for all records)", | |
_ => "unknown", | |
} | |
} | |
fn get_class_text(class_id: u16) -> &'static str { | |
match class_id { | |
1 => "IN (the Internet)", | |
2 => "CS (the CSNET class)", | |
3 => "CH (the CHAOS class)", | |
4 => "HS (Hesiod)", | |
255 => "* (any class)", | |
_ => "unknown", | |
} | |
} | |
fn try_split_at(buf: &[u8], pos: usize) -> Option<(&[u8], &[u8])> { | |
if pos <= buf.len() { | |
Some(buf.split_at(pos)) | |
} else { | |
None | |
} | |
} | |
fn read_u8(buf: &[u8]) -> Option<(&[u8], u8)> { | |
let (any_bytes, rest) = try_split_at(buf, std::mem::size_of::<u8>())?; | |
if let &[value] = any_bytes { | |
Some((rest, value)) | |
} else { | |
None | |
} | |
} | |
fn read_u16(buf: &[u8]) -> Option<(&[u8], u16)> { | |
let (any_bytes, rest) = try_split_at(buf, std::mem::size_of::<u16>())?; | |
if let Ok(two_bytes) = any_bytes.try_into() { | |
let value = u16::from_be_bytes(two_bytes); | |
Some((rest, value)) | |
} else { | |
None | |
} | |
} | |
fn read_u32(buf: &[u8]) -> Option<(&[u8], u32)> { | |
let (any_bytes, rest) = try_split_at(buf, std::mem::size_of::<u32>())?; | |
if let Ok(four_bytes) = any_bytes.try_into() { | |
let value = u32::from_be_bytes(four_bytes); | |
Some((rest, value)) | |
} else { | |
None | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment