มกราคม 26, 2019

ลองสั่ง DNS Query

ลองสั่ง DNS Query

เราน่าจะทราบกันดีอยู่แล้วในเครือข่ายอินเตอร์เน็ตอุปกรณ์ทุกชิ้นจะมีที่อยู่สำหรับอ้างถึงที่เรียกว่า IP Address แต่ทว่าที่อยู่ที่ว่านั้นประกอบไปด้วยชุดตัวเลขที่ยากต่อการจดจำ ก็เลยมีการคิดค้นสิ่งที่เรียกว่า Domain Name ขึ้นมา

Domain Name ที่ว่านี้ไม่ได้มาแทน IP Address แต่มาเป็นส่วนเสริม โดยเป็นการใช้ชื่อที่เป็นข้อความ (Domain Name) ที่มนุษย์สามารถจดจำได้ดีกว่า ในการอ้างถึง IP Address และชื่อที่ใช้นี้จำเป็นต้องไม่ซ้ำกันดังนั้นจึงต้องมีหน่วยงานเข้ามาดูแล ซึ่งนั่นก็คือ ICANN

แม้ว่าเราจะรู้ Domain Name ของปลายทางแล้ว แต่อุปกรณ์ก็ไม่ได้รู้เองได้ว่า Domain Name นั้นผูกกับ IP Address อะไร ดังนั้นจึงต้องมีตัวกลางที่เป็นคนบอกว่า Domain Name ไหน คือ IP Address ไหน เราเรียกตัวกลางนี้ว่า Domain Name System (DNS)

เดิม DNS ให้บริการผ่าน UDP Socket โดยใช้เลข Port 53 แต่ในบางกรณีก็จะใช้ TCP Socket แทน โดยทั่วไป Application หรือ OS จะเป็นคนจัดการเรื่องการสื่อสารกับ DNS ให้ทั้งหมด ผู้ใช้ทั่วไปแบบเราๆ ไม่จำเป็นต้องทำอะไร แต่ถ้าต้องการจะดูว่า request/response หน้าตาเป็นยังไงก็ดูได้ผ่านโปรแกรมจำพวก Network Sniffer เช่น Tcpdump หรือ Wireshark

dns-1

DNS Message Format

บทความนี้ส่วนใหญ่จะแปลจาก: Let's hand write DNS messages by James Routley
Format ของทั้ง DNS Request และ DNS Response จะแบ่งเป็น 5 ส่วน ตามแผนภาพด้านล่างนี้

+---------------------+
|        Header       |
+---------------------+
|       Question      | the question for the name server
+---------------------+
|        Answer       | Resource Records (RRs) answering the question
+---------------------+
|      Authority      | RRs pointing toward an authority
+---------------------+
|      Additional     | RRs holding additional information
+---------------------+

ในบทความนี้จะทำการ ส่ง Query เพื่อขอ Record A ของ johannotes.com

Header Section

ส่วน Header จะแบ่งเป็นช่องย่อยๆ ดังนี้

 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                      ID                       |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    QDCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ANCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    NSCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                    ARCOUNT                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  • ID (16 bits) เป็น ID ของ Message ใช้ในการจับคู่ Query และ Response
  • QR (1 bit) เป็น Flag สำหรับบอกว่า Message นี้เป็น Query(0) หรือ Response(1)
  • Opcode (4 bits) ใช้ระบุประเภทของ Query
    • 0: Standard query
    • 1: Inverse query
    • 2: Server status request
    • 3 - 15: Reserved for future use
  • AA (1 bit) (Authoritative Answer) ใช้ใน Response เท่านั้น ใช่ระบุว่าคำตอบที่ได้มา มาจาก Authoritative Name Server (server ที่ถือ record นั้นๆ) หรือมาจากทางอื่น เช่น cache ในฝั่ง Query ให้ใส่ 0
  • TC (1 bit) (TrunCation) เป็น Flag สำหรับบอกว่า Message นี้ถูก truncate หรือไม่ ปกติเป็น 0
  • RD (1 bit) (Recursion Desired) เป็น Flag สำหรับบอกให้ DNS server ทำ Recursive Query (ถาม server อื่นต่อ) ในกรณีที่ไม่เจอ Record ที่เราถาม
  • RA (1 bit) (Recursion Available) ใช้ใน Response เท่านั้น บอกว่า server รองรับการทำ recursive query หรือไม่
  • Z (3 bits) (Reserved for future use) สงวนไว้ ยังไม่มีการใช้งาน
  • RCODE (4 bits) (Response code) บอกสถานะของ Response
    • 0 ไม่มี error
    • 1 (Format error) Format ผิด server อ่านไม่ได้
    • 2 (Server failure) มีปัญหาที่ฝั่ง server เอง
    • 3 (Name Error) ไม่มี record ที่ต้องการหา (คนที่จะตอบแบบนี้ได้ต้องเป็น Authoritative Name Server)
    • 4 (Not Implemented) server ไม่รองรับ query รูปแบบนี้
    • 5 (Refused) server ปฏิเสธที่จะตอบกลับ เนื่องจาก policy หรืออะไรบางอย่าง
  • QDCOUNT (16 bit) ใช้ระบุจำนวนของ Question ปกติเป็น 1
  • ANCOUNT (16 bit) จำนวนของ record ในส่วน Answer Section
  • NSCOUNT (16 bit) จำนวนของ record ในส่วน Authority Section
  • ARCOUNT (16 bit) จำนวนของ record ในส่วน Additional Section

ตัวอย่าง Header สำหรับ Query A record ของ johannotes.com

AA AA       - ID ของ Message

2 bytes ต่อมาจะประกอบด้วยข้อมูลหลายตัว ดังนั้นเราจะลองเขียนเป็นเลขฐาน 2 กันก่อน

0           - QR เป็น query
0000        - Opcode เป็น Standard query
0           - AA (ไม่ใช้ในฝั่ง Query)
0           - TC ไม่ truncate
1           - RD ให้ถามที่อื่นต่อ
0           - RA (ไม่ใช้ในฝั่ง Query)
000         - Z (สงวนไว้)
0000        - RCODE (ไม่ใช้ในฝั่ง Query)

เมื่อนำมาต่อกันจะได้เป็น 0000 0001 0000 0000 แปลงเป็นฐาน 16 ได้

01 00

ส่วนจำนวน record ต่างๆ

00 01       - QDCOUNT มี 1 คำถาม
00 00       - ANCOUNT (ไม่ใช้ในฝั่ง Query)
00 00       - NSCOUNT (ไม่ใช้ในฝั่ง Query)
00 00       - ARCOUNT (ไม่ใช้ในฝั่ง Query)

รวมทั้งหมดในส่วนนี้จะได้

AA AA 01 00 00 01 00 00 00 00 00 00

Question Section

ส่วนต่มาคือส่วน Question ในส่วนนี้เราจะต้องระบุ Domain Name ของปลายทางที่เราต้องการรู้ลงไป

 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                                               |
|                     QNAME                     |
|                                               |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                     QTYPE                     |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                     QCLASS                    |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
  • QNAME (n bits) เป็นส่วนสำหรับใส่ Domain Name ที่ต้องการ ตาม Format DNS Name Notation (ด้านล่าง)
  • QTYPE (2 bytes) ชนิดของ record ที่ต้องการ เช่น A(1) CNAME(5) AAAA(28) ปกติจะเป็น A หรือ AAAA ดูเต็มๆ ได้จาก IANA
  • QCLASS (2 bytes) คลาสของ Records ที่ต้องการ ปกติใส่ 1 หมายถึง IN หรือ Internet ดูเต็มๆ ได้จาก IANA

DNS Name Notation

DNS Name Notation เป็น Format ของการเขียน Name โดยเราจะต้องแบ่ง Domain Name ออกเป็นส่วนๆ โดยแบ่งจากเครื่องหมาย . (dot) หลังจากนั้นเราตจะเขียนลงไปทีละส่วน โดยใส่เลขระบุความยาวของส่วนนั้นๆ (ขนาด 1 bytes) ก่อน แล้วใส่ตัวอักษรของส่วนนั้นๆ ลงไปทีละตัว

 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|          10           |           j           |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|          o            |           h           |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|          a            |           n           |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|          n            |           o           |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|          t            |           e           |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|          s            |           3           |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|          c            |           o           |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|          m            |
+--+--+--+--+--+--+--+--+

จากตารางเอามาเขียนเป็น HEX จะได้

0A 6A 6F 68 61 6E 6E 6F 74 65 73 03 63 6F 6D

แล้วให้ใส่ 00 เพื่อเป็นการบอกว่าจบแล้ว

0A 6A 6F 68 61 6E 6E 6F 74 65 73 03 63 6F 6D 00

ตัวอย่าง

QNAME สำหรับ johannotes คือ

0A 6A 6F 68 61 6E 6E 6F 74 65 73 03 63 6F 6D 00

QTYPE ต้องการเป็น A Record จะได้

00 01

QCLASS เป็น Internet จะได้

00 01

รวมแล้วได้

0A 6A 6F 68 61 6E 6E 6F 74 65 73 03 63 6F 6D 00 00 01 00 01

ลองส่ง Query

เนื่องจากส่วนที่เหลือจะเป็นส่วนของ Response ทั้งหมด ดังนั้นเราจะมาลองส่ง Query กับก่อน โดยเราจะเปิด Socket ไปที่ 8.8.8.8 port 53 ที่เป็น DNS Server ของ Google หลังจากนั้นก็จะทำการเขียนข้อมูลที่เตรียมไว้เป็น byte array แล้วอ่าน Response ที่ได้

Data ที่จะส่ง :

AA AA 01 00 00 01 00 00 00 00 00 00 0A 6A 6F 68 61 6E 6E 6F 74 65 73 03 63 6F 6D 00 00 01 00 01

จะได้ผลลัพธ์ดังนี้

AA-AA-81-80-00-01-00-01-00-00-00-00-0A-6A-6F-68-61-6E-6E-6F-74-65-73-03-63-6F-6D-00-00-01-00-01-C0-0C-00-01-00-01-00-00-0E-0F-00-04-8B-3B-DC-0E

12 bytes แรกที่เป็น Header คือ

AA-AA-81-80-00-01-00-01-00-00-00-00

2 bytes แรก

AA-AA       - เป็น ID ที่เราส่งไปตอน Query

2 bytes ต่อมา (81-80) ต้องแปลงเป็น binary ก่อนอ่าน จะได้ 1000 0001 1000 0000

1           - QR เป็น reqponse
0000        - Opcode เป็น Standard query
0           - AA ไม่ได้มาจาก Authoritative Name Server
0           - TC ไม่ truncate
1           - RD ถามต่อ
1           - RA รองรับ recursive query
000         - Z (สงวนไว้)
0000        - RCODE ไม่มี error

ต่อมาจะเป็นจำนวน Record ต่างๆ

00-01       - QDCOUNT จำนวนคำถามเป็น 1
00-01       - ANCOUNT จำนวน record ใน Answer Section เป็น 1
00-00       - ANCOUNT จำนวน record ใน Authority Section เป็น 0
00-00       - ANCOUNT จำนวน record ใน Additional Section เป็น 0

ส่วน Question

ส่วนนี้จะเหมือนกับข้อมูล Question ที่เราส่งไปใน Query ซึงก็คือ

0A-6A-6F-68-61-6E-6E-6F-74-65-73-03-63-6F-6D-00-00-01-00-01

Answer/Authority/Additional Sections

  • Answer คือส่วนที่ตอบคำถามของ Query เรา
  • Authority ใช้ใน Iterative Query สำหรับบอกว่า Authoritative zone servers คือที่ไหน สำหรับให้เรานำไป Query ต่อ
  • Additional เป็นข้อมูลเพิ่มเติมของ Authority เช่น A, AAAA

ใน่ส่วนนี้จะประกอบไปด้วย resource record ต่างๆ ซึ่งใช้ format เดียวกันหมดตามแผนภาพข้างล่าง

 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                                               |
|                NAME (n bytes)                 |
|                                               |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                TYPE (2 bytes)                 |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                CLASS (2 bytes)                |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|                 TTL (4 bytes)                 |
|                                               |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|               RDLENGTH (2 bytes)              |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
|                                               |
|                RDATA (n bytes)                |
|                                               |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

NAME

Name คือส่วนที่ระบุว่า record นี้เป็นข้อมูลของ URL ไหน โดยส่วนนี้จะมีขนาดไม่แน่นอน อีกทั้งยังมีอยู่ 2 format คือ แบบ DNS Name Notation เหมือนกับในส่วน Question กับแบบ Compression label ที่ไม่ได้เก็บ Name จริงๆ แต่ชี้ไปยังตำแหน่งของ DNS Name Notation อื่น เพื่อลดการซ้ำซ้อน

Compression label
0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| 1  1|                OFFSET                   |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

สำหรับรูปแบบ Compression label แล้ว จะมีขนาดแน่นอนที่ 2 bytes และ 2 bits แรกมีค่าเป็น 1 1 เสมอ 14 bits ถัดมาเป็นตัวเลขที่บอกตำแหน่งของ DNS Name Notation อื่น โดยนับจาก bytes แรกของ DNS Message คือ 0

TYPE

เหมือนกับ QTYPE ในส่วน Question คือ บอกว่าเป็น Record ประเภทไหน

CLASS

เหมือนกับ QCLASS ในส่วน Question

Time To Live (TTL)

TTL มีขนาด 4 bytes ใช้บอกว่าเราควรจะเก็บผมลัพทธ์ที่ได้นี้ไว้ใช้ซ้ำได้ (cache) อีกกี่วินาที โดย 0 หมายถึงอย่าเก็บ

Resource Data Length (RDLENGTH)

RDLENGTH มีขนาด 2 bytes ใช้บอกว่า RDATA มีขนาดกี่ bytes

Resource Data (RDATA)

RDATA มีขนาดตามที่ RDLENGTH บอก ส่วนนี้จะเป็นส่วนคำตอบจริงๆ ของสิ่งที่เราขอ ในบทคามนี้เราจะลองดูแค่ Address, CName, และ NS

Address (A)

IPv4 Address มีขนาด 4 bytes โดยแต่ละ byte จะเป็นตัวเลข 0 - 255 แทนแต่ละส่วนใน IP Address ตัวอย่างเช่น 7F 00 00 01 คือ 127.0.0.1

Canonical Name (CName)

Canonical Name คือ Alias ของ Name ที่เราถามในส่วน Question เขียนอยู่ใน Format ของ DNS Name Notation

Name Server Domain Name (NS)

Name Server Domain Name คือโดเมนของ Authoritative Name Server ของ record ที่เราถาม เขียนอยู่ใน Format ของ DNS Name Notation

ตัวอย่าง

Reacord แรกใน Answer ของเราคือ

C0-0C-00-01-00-01-00-00-0E-0F-00-04-8B-3B-DC-0E

อ่านได้ดังนี้

C0-0C         - Name แปลงเป็น binary จะได้ 1100 0000 0000 1100 เนื่องจาก 2 bit แรกเป็น 1 1  แสดงว่า Name ของเราเป็นแบบ Compression label โดยอยู่ในตำแหน่งที่ 12 เมื่อนับจาก byte แรก
00-01         - TYPE เป็น Record A
00-01         - CLASS เป็น Internet
00-00-0E-0F   - Time To Live ให้ cache ไว้ 3599 วินาที
00-04         - Resource Data มีขนาด 4 bytes
8B-3B-DC-0E   - Resource Data เป็น IPv4 เนื่องจากเราขอ Record A จะได้ 139.59.220.14

สรุปได้ว่า johannotes.com นั้นชี้ไปที่ IP Address 139.59.220.14

Make DNS more secure

จากข้างต้น เราจะเห็นว่าแค่เราสร้าง Query Message ที่ถูกต้อง แล้วก็ส่งไปยัง DNS server ผ่าน port 53 ก็สามารถ Query ได้แล้ว และใน DNS message นั้น มิได้มีการเข้ารหัสใดๆ เลย ซึ่งนั่นอาจจะทำให้การเชื่อมต่อของเราไม่ปลอดภัย เช่นมีคน Hijack DNS server ที่เราใช้ เพื่อหลอกให้เราเข้าไปยัง malicious server หรือแม้กระทั่งเรื่องความเป็นส่วนตัวที่ Query Message ของเราถูกอ่านได้จาก Node ต่างๆ บน network โดยไม่มีอะไรป้องกัน ด้วยความกังวลเหล่านี้จึงได้มีผู้คิดค้นวิธี Query ใหม่ๆ ขึ้นมา ไม่ว่าจะเป็น DNSCrypt ที่มีการออกแบบ Message ใหม่แล้วเปลี่ยนไปใช้ port 443 DNS over TLS ที่เอา DNS เดิมไปรันบน TLS ที่ port 853 และน้องใหม่มาแรง DNS over HTTPs ที่ย้าย DNS มารันบน HTTPS ตรงๆ

DNS over HTTPS (DoH)

DNS over HTTPS ดูเป็นตัวเลือกที่น่าจะมีหวังที่สุด เนื่องจากรันบนของที่มีอยู่แล้ว และใช้กันอย่างแพร่หลาย ไม่จำเป็นต้องแก้ config network ทั้งระบบเพื่อเปิด port พิเศษ

DNS over HTTPS มี implementation 2 แบบคือแบบ POST ที่ใช้ DNS message เดิม กับแบบ GET ที่ใช้ Query params สำหรับ Query และ Response เป็น JSON แทน

POST method

เราจะลองนำ DNS Query Message มาส่งผ่าน HTTPS กันดู

  • เริ่มจากเราต้องหา DNS server ที่รองรับ DoH กันก่อนจาก Publicly available servers (มีบาง server ที่รองรับแค่ GET หรือ POST อย่างเดียว) ในตัวอย่างนี้จะใช้ https://cloudflare-dns.com/dns-query

  • หลังจากนั้นให้สร้าง HTTP POST request ใส่ URL ของ DNS server ที่เลือก ใส่ Header ดังนี้

    • Content-Type: application/dns-udpwireformat
    • Accept: application/dns-udpwireformat
  • เตรียม DNS Query Message ในรูปแบบ binary แล้วใส่เข้าไปใน body ของ Request เป็น raw bytes

  • ส่ง Request

จากการลอง Qeury record A ของ johannotes.com ได้ response ดังนี้

AA-AA-81-80-00-01-00-01-00-00-00-01-0A-6A-6F-68-61-6E-6E-6F-74-65-73-03-63-6F-6D-00-00-01-00-01-C0-0C-00-01-00-01-00-00-06-3E-00-04-8B-3B-DC-0E-00-00-29-05-AC-00-00-00-00-00-00

ของเดิมที่ Query ผ่านวิธีปกติ
AA-AA-81-80-00-01-00-01-00-00-00-00-0A-6A-6F-68-61-6E-6E-6F-74-65-73-03-63-6F-6D-00-00-01-00-01-C0-0C-00-01-00-01-00-00-0E-0F-00-04-8B-3B-DC-0E

จะเห็นว่าครั้งนี้มี Additional Section เพิ่มมา 1 record เรามาลองอ่านกันดูดีกว่า

00-00-29-05-AC-00-00-00-00-00-00

00          - ไม่มี Name
00-29       - Type เป็น OPT
05-AC       - CLASS???
00-00-00-00 - ไม่ cache
00          - ไม่มี Resource Data

สรุปคือไม่เข้าใจเลยสักนิด = =" เอาเป็นว่า Answer เหมือนกันก็ใช้ได้แล้ว

GET method

  • เริ่มจากเราต้องหา DNS server ที่รองรับ DoH กันก่อนจาก Publicly available servers (มีบาง server ที่รองรับแค่ GET หรือ POST อย่างเดียว) ในตัวอย่างนี้จะใช้ https://cloudflare-dns.com/dns-query

  • หลังจากนั้นให้สร้าง HTTP GET request ใส่ URL ของ DNS server ที่เลือก ใส่ Header ดังนี้

    • Accept: application/dns-json
  • ใส่ Query Parameters

    • name: johannotes.com
    • type: A
  • ส่ง Request

จะได้ response ดังนี้

{
   "Status":0,
   "TC":false,
   "RD":true,
   "RA":true,
   "AD":false,
   "CD":false,
   "Question":[
      {
         "name":"johannotes.com.",
         "type":1
      }
   ],
   "Answer":[
      {
         "name":"johannotes.com.",
         "type":1,
         "TTL":3600,
         "data":"139.59.220.14"
      }
   ]
}

สรุปว่าแบบนี้ง่ายกว่าเยอะเลย 5555

อ้างอิง

Let's hand write DNS messages - James Routley

DNS Name Notation and Message Compression Technique - The TCP/IP Guide

DNS name compression - Keyboard Banger

DNS Parameters - IANA

DNS over HTTPS - IETF

JSON format to represent DNS data - IETF

DNS Using JSON - Cloudflare