mirror of
https://github.com/vee1e/mitmproxy.git
synced 2026-09-03 11:17:24 +00:00
[dns] move resolve code into addon
This commit is contained in:
parent
c821d02e09
commit
99bcfb7f55
4 changed files with 168 additions and 162 deletions
|
|
@ -1,8 +1,116 @@
|
|||
import asyncio
|
||||
import ipaddress
|
||||
import socket
|
||||
from typing import Callable, Iterable, List, Tuple, Union
|
||||
from mitmproxy import ctx, dns
|
||||
|
||||
IP4_PTR_SUFFIX = ".in-addr.arpa"
|
||||
IP6_PTR_SUFFIX = ".ip6.arpa"
|
||||
|
||||
|
||||
class ResolveError(Exception):
|
||||
"""Exception thrown by different resolve methods."""
|
||||
def __init__(self, response_code: int) -> None:
|
||||
assert response_code != dns.response_codes.NOERROR
|
||||
self.response_code = response_code
|
||||
|
||||
|
||||
async def resolve_question_by_name(
|
||||
question: dns.Question,
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
family: socket.AddressFamily,
|
||||
ip: Callable[[str], Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]
|
||||
) -> Iterable[dns.ResourceRecord]:
|
||||
try:
|
||||
addrinfos = await loop.getaddrinfo(host=question.name, port=0, family=family)
|
||||
except socket.gaierror as e:
|
||||
if e.errno == socket.EAI_NONAME:
|
||||
raise ResolveError(dns.response_codes.NXDOMAIN)
|
||||
else:
|
||||
# NOTE might fail on Windows for IPv6 queries:
|
||||
# https://stackoverflow.com/questions/66755681/getaddrinfo-c-on-windows-not-handling-ipv6-correctly-returning-error-code-1
|
||||
raise ResolveError(dns.response_codes.SERVFAIL)
|
||||
return map(lambda addrinfo: dns.ResourceRecord(
|
||||
name=question.name,
|
||||
type=question.type,
|
||||
class_=question.class_,
|
||||
ttl=dns.ResourceRecord.DEFAULT_TTL,
|
||||
data=ip(addrinfo[4][0]).packed,
|
||||
), addrinfos)
|
||||
|
||||
|
||||
async def resolve_question_by_addr(
|
||||
question: dns.Question,
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
suffix: str,
|
||||
sockaddr: Callable[[List[str]], Union[Tuple[str, int], Tuple[str, int, int, int]]]
|
||||
) -> Iterable[dns.ResourceRecord]:
|
||||
try:
|
||||
addr = sockaddr(question.name[:-len(suffix)].split(".")[::-1])
|
||||
except ValueError:
|
||||
raise ResolveError(dns.response_codes.FORMERR)
|
||||
try:
|
||||
name, _ = await loop.getnameinfo(addr, flags=socket.NI_NAMEREQD)
|
||||
except socket.gaierror as e:
|
||||
raise ResolveError(dns.response_codes.NXDOMAIN if e.errno == socket.EAI_NONAME else dns.response_codes.SERVFAIL)
|
||||
return [dns.ResourceRecord(
|
||||
name=question.name,
|
||||
type=question.type,
|
||||
class_=question.class_,
|
||||
ttl=dns.ResourceRecord.DEFAULT_TTL,
|
||||
data=dns.domain_names.pack(name),
|
||||
)]
|
||||
|
||||
|
||||
async def resolve_question(question: dns.Question) -> Iterable[dns.ResourceRecord]:
|
||||
"""Resolve the question into resource record(s), throwing ResolveError if an error condition occurs."""
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
if question.class_ != dns.classes.IN:
|
||||
raise ResolveError(dns.response_codes.NOTIMP)
|
||||
if question.type == dns.types.A:
|
||||
return await resolve_question_by_name(question, loop, socket.AddressFamily.AF_INET, ipaddress.IPv4Address)
|
||||
elif question.type == dns.types.AAAA:
|
||||
return await resolve_question_by_name(question, loop, socket.AddressFamily.AF_INET6, ipaddress.IPv6Address)
|
||||
elif question.type == dns.types.PTR:
|
||||
name_lower = question.name.lower()
|
||||
if name_lower.endswith(IP4_PTR_SUFFIX):
|
||||
return await resolve_question_by_addr(
|
||||
question=question,
|
||||
loop=loop,
|
||||
suffix=IP4_PTR_SUFFIX,
|
||||
sockaddr=lambda x: (str(ipaddress.IPv4Address(".".join(x))), 0)
|
||||
)
|
||||
elif name_lower.endswith(IP6_PTR_SUFFIX):
|
||||
return await resolve_question_by_addr(
|
||||
question=question,
|
||||
loop=loop,
|
||||
suffix=IP6_PTR_SUFFIX,
|
||||
sockaddr=lambda x: (str(ipaddress.IPv6Address(bytes.fromhex("".join(x)))), 0, 0, 0)
|
||||
)
|
||||
else:
|
||||
raise ResolveError(dns.response_codes.FORMERR)
|
||||
else:
|
||||
raise ResolveError(dns.response_codes.NOTIMP)
|
||||
|
||||
|
||||
async def resolve_message(message: dns.Message) -> dns.Message:
|
||||
try:
|
||||
if not message.query:
|
||||
raise ResolveError(dns.response_codes.REFUSED) # we cannot resolve an answer
|
||||
if message.op_code != dns.op_codes.QUERY:
|
||||
raise ResolveError(dns.response_codes.NOTIMP) # inverse queries and others are not supported
|
||||
rrs: List[dns.ResourceRecord] = []
|
||||
for q in message.questions:
|
||||
rrs.extend(await resolve_question(q))
|
||||
except ResolveError as e:
|
||||
return message.fail(e.response_code)
|
||||
else:
|
||||
return message.succeed(rrs)
|
||||
|
||||
|
||||
class DnsResolver:
|
||||
async def dns_request(self, flow: dns.DNSFlow) -> None:
|
||||
# handle regular mode requests here to not block the layer
|
||||
if ctx.options.dns_mode == "regular":
|
||||
flow.response = await flow.request.resolve()
|
||||
flow.response = await resolve_message(flow.request)
|
||||
|
|
|
|||
103
mitmproxy/dns.py
103
mitmproxy/dns.py
|
|
@ -1,14 +1,11 @@
|
|||
from __future__ import annotations
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
import ipaddress
|
||||
import itertools
|
||||
import random
|
||||
import socket
|
||||
import struct
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
import time
|
||||
from typing import Callable, Iterable, List, Optional, Tuple, Union
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
from mitmproxy import connection, flow, stateobject
|
||||
from mitmproxy.net.dns import classes, domain_names, op_codes, response_codes, types
|
||||
|
|
@ -16,18 +13,9 @@ from mitmproxy.net.dns import classes, domain_names, op_codes, response_codes, t
|
|||
# DNS parameters taken from https://www.iana.org/assignments/dns-parameters/dns-parameters.xml
|
||||
|
||||
|
||||
class ResolveError(Exception):
|
||||
"""Exception thrown by different resolve methods."""
|
||||
def __init__(self, response_code: int) -> None:
|
||||
assert response_code != response_codes.NOERROR
|
||||
self.response_code = response_code
|
||||
|
||||
|
||||
@dataclass
|
||||
class Question(stateobject.StateObject):
|
||||
HEADER = struct.Struct("!HH")
|
||||
IP4_PTR_SUFFIX = ".in-addr.arpa"
|
||||
IP6_PTR_SUFFIX = ".ip6.arpa"
|
||||
|
||||
name: str
|
||||
type: int
|
||||
|
|
@ -42,80 +30,6 @@ class Question(stateobject.StateObject):
|
|||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
async def _resolve_by_name(
|
||||
self,
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
family: socket.AddressFamily,
|
||||
ip: Callable[[str], Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]
|
||||
) -> Iterable[ResourceRecord]:
|
||||
try:
|
||||
addrinfos = await loop.getaddrinfo(host=self.name, port=0, family=family)
|
||||
except socket.gaierror as e:
|
||||
if e.errno == socket.EAI_NONAME:
|
||||
raise ResolveError(response_codes.NXDOMAIN)
|
||||
else:
|
||||
# NOTE might fail on Windows for IPv6 queries:
|
||||
# https://stackoverflow.com/questions/66755681/getaddrinfo-c-on-windows-not-handling-ipv6-correctly-returning-error-code-1
|
||||
raise ResolveError(response_codes.SERVFAIL)
|
||||
return map(lambda addrinfo: ResourceRecord(
|
||||
name=self.name,
|
||||
type=self.type,
|
||||
class_=self.class_,
|
||||
ttl=ResourceRecord.DEFAULT_TTL,
|
||||
data=ip(addrinfo[4][0]).packed,
|
||||
), addrinfos)
|
||||
|
||||
async def _resolve_by_addr(
|
||||
self,
|
||||
loop: asyncio.AbstractEventLoop,
|
||||
suffix: str,
|
||||
sockaddr: Callable[[List[str]], Union[Tuple[str, int], Tuple[str, int, int, int]]]
|
||||
) -> Iterable[ResourceRecord]:
|
||||
try:
|
||||
addr = sockaddr(self.name[:-len(suffix)].split(".")[::-1])
|
||||
except ValueError:
|
||||
raise ResolveError(response_codes.FORMERR)
|
||||
try:
|
||||
name, _ = await loop.getnameinfo(addr, flags=socket.NI_NAMEREQD)
|
||||
except socket.gaierror as e:
|
||||
raise ResolveError(response_codes.NXDOMAIN if e.errno == socket.EAI_NONAME else response_codes.SERVFAIL)
|
||||
return [ResourceRecord(
|
||||
name=self.name,
|
||||
type=self.type,
|
||||
class_=self.class_,
|
||||
ttl=ResourceRecord.DEFAULT_TTL,
|
||||
data=domain_names.pack(name),
|
||||
)]
|
||||
|
||||
async def resolve(self) -> Iterable[ResourceRecord]:
|
||||
"""Resolve the question into resource record(s), throwing ResolveError if an error condition occurs."""
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
if self.class_ != classes.IN:
|
||||
raise ResolveError(response_codes.NOTIMP)
|
||||
if self.type == types.A:
|
||||
return await self._resolve_by_name(loop, socket.AddressFamily.AF_INET, ipaddress.IPv4Address)
|
||||
elif self.type == types.AAAA:
|
||||
return await self._resolve_by_name(loop, socket.AddressFamily.AF_INET6, ipaddress.IPv6Address)
|
||||
elif self.type == types.PTR:
|
||||
name_lower = self.name.lower()
|
||||
if name_lower.endswith(Question.IP4_PTR_SUFFIX):
|
||||
return await self._resolve_by_addr(
|
||||
loop=loop,
|
||||
suffix=Question.IP4_PTR_SUFFIX,
|
||||
sockaddr=lambda x: (str(ipaddress.IPv4Address(".".join(x))), 0)
|
||||
)
|
||||
elif name_lower.endswith(Question.IP6_PTR_SUFFIX):
|
||||
return await self._resolve_by_addr(
|
||||
loop=loop,
|
||||
suffix=Question.IP6_PTR_SUFFIX,
|
||||
sockaddr=lambda x: (str(ipaddress.IPv6Address(bytes.fromhex("".join(x)))), 0, 0, 0)
|
||||
)
|
||||
else:
|
||||
raise ResolveError(response_codes.FORMERR)
|
||||
else:
|
||||
raise ResolveError(response_codes.NOTIMP)
|
||||
|
||||
def to_json(self) -> dict:
|
||||
"""
|
||||
Converts the question into json for mitmweb.
|
||||
|
|
@ -350,21 +264,6 @@ class Message(stateobject.StateObject):
|
|||
additionals=[],
|
||||
)
|
||||
|
||||
async def resolve(self) -> Message:
|
||||
"""Resolves the message and return the result in form of a response message."""
|
||||
try:
|
||||
if not self.query:
|
||||
raise ResolveError(response_codes.REFUSED) # we cannot resolve an answer
|
||||
if self.op_code != op_codes.QUERY:
|
||||
raise ResolveError(response_codes.NOTIMP) # inverse queries and others are not supported
|
||||
rrs: List[ResourceRecord] = []
|
||||
for q in self.questions:
|
||||
rrs.extend(await q.resolve())
|
||||
except ResolveError as e:
|
||||
return self.fail(e.response_code)
|
||||
else:
|
||||
return self.succeed(rrs)
|
||||
|
||||
@classmethod
|
||||
def unpack(cls, buffer: bytes) -> Message:
|
||||
"""Converts the entire given buffer into a DNS message."""
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
import asyncio
|
||||
import ipaddress
|
||||
import platform
|
||||
from typing import Callable
|
||||
|
||||
import pytest
|
||||
|
||||
from mitmproxy import dns
|
||||
from mitmproxy.addons import dns_resolver, proxyserver
|
||||
from mitmproxy.test import taddons, tflow
|
||||
from mitmproxy.test import taddons, tflow, tutils
|
||||
|
||||
|
||||
async def test_simple(monkeypatch):
|
||||
monkeypatch.setattr(dns.Message, "resolve", lambda _: asyncio.sleep(0, "resp"))
|
||||
monkeypatch.setattr(dns_resolver, "resolve_message", lambda _: asyncio.sleep(0, "resp"))
|
||||
|
||||
dr = dns_resolver.DnsResolver()
|
||||
with taddons.context(dr, proxyserver.Proxyserver()) as tctx:
|
||||
|
|
@ -18,3 +23,54 @@ async def test_simple(monkeypatch):
|
|||
f = tflow.tdnsflow()
|
||||
await dr.dns_request(f)
|
||||
assert not f.response
|
||||
|
||||
|
||||
@pytest.mark.skip("requires internet connection")
|
||||
async def test_resolve():
|
||||
async def fail_with(question: dns.Question, code: int):
|
||||
with pytest.raises(dns_resolver.ResolveError) as ex:
|
||||
await dns_resolver.resolve_question(question)
|
||||
assert ex.value.response_code == code
|
||||
|
||||
async def succeed_with(question: dns.Question, check: Callable[[dns.ResourceRecord], bool]):
|
||||
assert any(map(check, await dns_resolver.resolve_question(question)))
|
||||
|
||||
await fail_with(dns.Question("dns.google", dns.types.A, dns.classes.CH), dns.response_codes.NOTIMP)
|
||||
await fail_with(dns.Question("not.exists", dns.types.A, dns.classes.IN), dns.response_codes.NXDOMAIN)
|
||||
await fail_with(dns.Question("dns.google", dns.types.SOA, dns.classes.IN), dns.response_codes.NOTIMP)
|
||||
await fail_with(dns.Question("totally.invalid", dns.types.PTR, dns.classes.IN), dns.response_codes.FORMERR)
|
||||
await fail_with(dns.Question("invalid.in-addr.arpa", dns.types.PTR, dns.classes.IN), dns.response_codes.FORMERR)
|
||||
await fail_with(dns.Question("0.0.0.1.in-addr.arpa", dns.types.PTR, dns.classes.IN), dns.response_codes.NXDOMAIN)
|
||||
|
||||
await succeed_with(
|
||||
dns.Question("dns.google", dns.types.A, dns.classes.IN),
|
||||
lambda rr: rr.ipv4_address == ipaddress.IPv4Address("8.8.8.8")
|
||||
)
|
||||
if platform.system() == "Linux": # will fail on Windows, apparently returns empty on Mac
|
||||
await succeed_with(
|
||||
dns.Question("dns.google", dns.types.AAAA, dns.classes.IN),
|
||||
lambda rr: rr.ipv6_address == ipaddress.IPv6Address("2001:4860:4860::8888")
|
||||
)
|
||||
await succeed_with(
|
||||
dns.Question("8.8.8.8.in-addr.arpa", dns.types.PTR, dns.classes.IN),
|
||||
lambda rr: rr.domain_name == "dns.google"
|
||||
)
|
||||
await succeed_with(
|
||||
dns.Question("8.8.8.8.in-addr.arpa", dns.types.PTR, dns.classes.IN),
|
||||
lambda rr: rr.domain_name == "dns.google"
|
||||
)
|
||||
await succeed_with(
|
||||
dns.Question("8.8.8.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.6.8.4.0.6.8.4.1.0.0.2.ip6.arpa", dns.types.PTR, dns.classes.IN),
|
||||
lambda rr: rr.domain_name == "dns.google"
|
||||
)
|
||||
|
||||
req = tutils.tdnsreq()
|
||||
req.query = False
|
||||
assert (await dns_resolver.resolve_message(req)).response_code == dns.response_codes.REFUSED
|
||||
req.query = True
|
||||
req.op_code = dns.op_codes.IQUERY
|
||||
assert (await dns_resolver.resolve_message(req)).response_code == dns.response_codes.NOTIMP
|
||||
req.op_code = dns.op_codes.QUERY
|
||||
resp = await dns_resolver.resolve_message(req)
|
||||
assert resp.response_code == dns.response_codes.NOERROR
|
||||
assert filter(lambda rr: str(rr.ipv4_address) == "8.8.8.8", resp.answers)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import ipaddress
|
||||
import platform
|
||||
import struct
|
||||
from typing import Callable
|
||||
import pytest
|
||||
|
||||
from mitmproxy import dns
|
||||
|
|
@ -35,63 +33,8 @@ class TestResourceRecord:
|
|||
assert rr.text == "sample text"
|
||||
|
||||
|
||||
class TestQuestion:
|
||||
|
||||
@pytest.mark.skip("requires internet connection")
|
||||
async def test_resolve(self):
|
||||
async def fail_with(question: dns.Question, code: int):
|
||||
with pytest.raises(dns.ResolveError) as ex:
|
||||
await question.resolve()
|
||||
assert ex.value.response_code == code
|
||||
|
||||
async def succeed_with(question: dns.Question, check: Callable[[dns.ResourceRecord], bool]):
|
||||
assert any(map(check, await question.resolve()))
|
||||
|
||||
await fail_with(dns.Question("dns.google", dns.types.A, dns.classes.CH), dns.response_codes.NOTIMP)
|
||||
await fail_with(dns.Question("not.exists", dns.types.A, dns.classes.IN), dns.response_codes.NXDOMAIN)
|
||||
await fail_with(dns.Question("dns.google", dns.types.SOA, dns.classes.IN), dns.response_codes.NOTIMP)
|
||||
await fail_with(dns.Question("totally.invalid", dns.types.PTR, dns.classes.IN), dns.response_codes.FORMERR)
|
||||
await fail_with(dns.Question("invalid.in-addr.arpa", dns.types.PTR, dns.classes.IN), dns.response_codes.FORMERR)
|
||||
await fail_with(dns.Question("0.0.0.1.in-addr.arpa", dns.types.PTR, dns.classes.IN), dns.response_codes.NXDOMAIN)
|
||||
|
||||
await succeed_with(
|
||||
dns.Question("dns.google", dns.types.A, dns.classes.IN),
|
||||
lambda rr: rr.ipv4_address == ipaddress.IPv4Address("8.8.8.8")
|
||||
)
|
||||
if platform.system() == "Linux": # will fail on Windows, apparently returns empty on Mac
|
||||
await succeed_with(
|
||||
dns.Question("dns.google", dns.types.AAAA, dns.classes.IN),
|
||||
lambda rr: rr.ipv6_address == ipaddress.IPv6Address("2001:4860:4860::8888")
|
||||
)
|
||||
await succeed_with(
|
||||
dns.Question("8.8.8.8.in-addr.arpa", dns.types.PTR, dns.classes.IN),
|
||||
lambda rr: rr.domain_name == "dns.google"
|
||||
)
|
||||
await succeed_with(
|
||||
dns.Question("8.8.8.8.in-addr.arpa", dns.types.PTR, dns.classes.IN),
|
||||
lambda rr: rr.domain_name == "dns.google"
|
||||
)
|
||||
await succeed_with(
|
||||
dns.Question("8.8.8.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.6.8.4.0.6.8.4.1.0.0.2.ip6.arpa", dns.types.PTR, dns.classes.IN),
|
||||
lambda rr: rr.domain_name == "dns.google"
|
||||
)
|
||||
|
||||
|
||||
class TestMessage:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resolve(self):
|
||||
req = tutils.tdnsreq()
|
||||
req.query = False
|
||||
assert (await req.resolve()).response_code == dns.response_codes.REFUSED
|
||||
req.query = True
|
||||
req.op_code = dns.op_codes.IQUERY
|
||||
assert (await req.resolve()).response_code == dns.response_codes.NOTIMP
|
||||
req.op_code = dns.op_codes.QUERY
|
||||
resp = await req.resolve()
|
||||
assert resp.response_code == dns.response_codes.NOERROR
|
||||
assert filter(lambda rr: str(rr.ipv4_address) == "8.8.8.8", resp.answers)
|
||||
|
||||
def test_responses(self):
|
||||
req = tutils.tdnsreq()
|
||||
resp = tutils.tdnsresp()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue