Skip to content

Commit

Permalink
Add dns_resolve and conf.nameservers
Browse files Browse the repository at this point in the history
  • Loading branch information
gpotter2 committed Jul 18, 2023
1 parent e0f2323 commit 7f406c8
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 29 deletions.
9 changes: 8 additions & 1 deletion scapy/arch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"get_if_raw_hwaddr",
"get_working_if",
"in6_getifaddr",
"read_nameservers",
"read_routes",
"read_routes6",
"SIOCGIFHWADDR",
Expand Down Expand Up @@ -119,14 +120,20 @@ def get_if_raw_addr6(iff):
# def get_if_raw_addr(iff)
# def get_if_raw_hwaddr(iff)
# def in6_getifaddr()
# def read_nameservers()
# def read_routes()
# def read_routes6()
# def set_promisc(s,iff,val=1)

if LINUX:
from scapy.arch.linux import * # noqa F403
elif BSD:
from scapy.arch.unix import read_routes, read_routes6, in6_getifaddr # noqa: E501
from scapy.arch.unix import ( # noqa F403
read_nameservers,
read_routes,
read_routes6,
in6_getifaddr,
)
from scapy.arch.bpf.core import * # noqa F403
if not conf.use_pcap:
# Native
Expand Down
3 changes: 3 additions & 0 deletions scapy/arch/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
from scapy.pton_ntop import inet_ntop
from scapy.supersocket import SuperSocket

# re-export
from scapy.arch.unix import read_nameservers # noqa: F401

# Typing imports
from typing import (
Any,
Expand Down
16 changes: 16 additions & 0 deletions scapy/arch/unix.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"""

import os
import re
import socket
import struct
from fcntl import ioctl
Expand Down Expand Up @@ -409,3 +410,18 @@ def read_routes6():

fd_netstat.close()
return routes


#######
# DNS #
#######

def read_nameservers() -> List[str]:
"""Return the nameservers configured by the OS
"""
try:
with open('/etc/resolv.conf', 'r') as fd:
return re.findall(r"nameserver\s+([^\s]+)", fd.read())
except FileNotFoundError:
warning("Could not retrieve the OS's nameserver !")
return []
72 changes: 46 additions & 26 deletions scapy/arch/windows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@

import winreg

from scapy.arch.windows.structures import _windows_title, \
GetAdaptersAddresses, GetIpForwardTable, GetIpForwardTable2, \
get_service_status
from scapy.arch.windows.structures import (
_windows_title,
GetAdaptersAddresses,
GetIpForwardTable,
GetIpForwardTable2,
get_service_status,
)
from scapy.consts import WINDOWS, WINDOWS_XP
from scapy.config import conf, ProgPath
from scapy.error import (
Expand Down Expand Up @@ -263,33 +267,33 @@ def _get_mac(x):
data = bytearray(x["physical_address"])
return str2mac(bytes(data)[:size])

def _resolve_ips(y):
# type: (List[Dict[str, Any]]) -> List[str]
if not isinstance(y, list):
return []
ips = []
for ip in y:
addr = ip['address']['address'].contents
if addr.si_family == socket.AF_INET6:
ip_key = "Ipv6"
si_key = "sin6_addr"
else:
ip_key = "Ipv4"
si_key = "sin_addr"
data = getattr(addr, ip_key)
data = getattr(data, si_key)
data = bytes(bytearray(data.byte))
# Build IP
if data:
ips.append(inet_ntop(addr.si_family, data))
return ips

def _get_ips(x):
# type: (Dict[str, Any]) -> List[str]
unicast = x['first_unicast_address']
anycast = x['first_anycast_address']
multicast = x['first_multicast_address']

def _resolve_ips(y):
# type: (List[Dict[str, Any]]) -> List[str]
if not isinstance(y, list):
return []
ips = []
for ip in y:
addr = ip['address']['address'].contents
if addr.si_family == socket.AF_INET6:
ip_key = "Ipv6"
si_key = "sin6_addr"
else:
ip_key = "Ipv4"
si_key = "sin_addr"
data = getattr(addr, ip_key)
data = getattr(data, si_key)
data = bytes(bytearray(data.byte))
# Build IP
if data:
ips.append(inet_ntop(addr.si_family, data))
return ips

ips = []
ips.extend(_resolve_ips(unicast))
if extended:
Expand All @@ -306,7 +310,8 @@ def _resolve_ips(y):
"mac": _get_mac(x),
"ipv4_metric": 0 if WINDOWS_XP else x["ipv4_metric"],
"ipv6_metric": 0 if WINDOWS_XP else x["ipv6_metric"],
"ips": _get_ips(x)
"ips": _get_ips(x),
"nameservers": _resolve_ips(x["first_dns_server_address"])
} for x in GetAdaptersAddresses()
]

Expand All @@ -329,6 +334,7 @@ def __init__(self, provider, data=None):
self.cache_mode = None # type: Optional[bool]
self.ipv4_metric = None # type: Optional[int]
self.ipv6_metric = None # type: Optional[int]
self.nameservers = [] # type: List[str]
self.guid = None # type: Optional[str]
self.raw80211 = None # type: Optional[bool]
super(NetworkInterface_Win, self).__init__(provider, data)
Expand All @@ -344,6 +350,7 @@ def update(self, data):
self.guid = data['guid']
self.ipv4_metric = data['ipv4_metric']
self.ipv6_metric = data['ipv6_metric']
self.nameservers = data['nameservers']

try:
# Npcap loopback interface
Expand Down Expand Up @@ -640,7 +647,8 @@ def load(self, NetworkInterface_Win=NetworkInterface_Win):
'ipv4_metric': 0,
'ipv6_metric': 0,
'ips': ips,
'flags': flags
'flags': flags,
'nameservers': [],
}
# No KeyError will happen here, as we get it from cache
results[netw] = NetworkInterface_Win(self, data)
Expand Down Expand Up @@ -1016,3 +1024,15 @@ def __init__(self, *args, **kargs):
"winpcap is not installed. You may use conf.L3socket or"
"conf.L3socket6 to access layer 3"
)


#######
# DNS #
#######

def read_nameservers() -> List[str]:
"""Return the nameservers configured by the OS (on the default interface)
"""
# Windows has support for different DNS servers on each network interface,
# but to be cross-platform we only return the servers for the default one.
return cast(NetworkInterface_Win, conf.iface).nameservers
5 changes: 4 additions & 1 deletion scapy/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -785,8 +785,10 @@ class Conf(ConfClass):
ifaces = None # type: 'scapy.interfaces.NetworkInterfaceDict'
#: holds the cache of interfaces loaded from Libpcap
cache_pcapiflist = {} # type: Dict[str, Tuple[str, List[str], Any, str]]
neighbor = None # type: 'scapy.layers.l2.Neighbor'
# `neighbor` will be filed by scapy.layers.l2
neighbor = None # type: 'scapy.layers.l2.Neighbor'
#: holds the name servers IP/hosts used for custom DNS resolution
nameservers = None # type: str
#: holds the Scapy IPv4 routing table and provides methods to
#: manipulate it
route = None # type: 'scapy.route.Route'
Expand Down Expand Up @@ -828,6 +830,7 @@ class Conf(ConfClass):
stats_classic_protocols = [] # type: List[Type[Packet]]
stats_dot11_protocols = [] # type: List[Type[Packet]]
temp_files = [] # type: List[str]
#: netcache holds time-based caches for net operations
netcache = NetCache()
geoip_city = None
# can, tls, http and a few others are not loaded by default
Expand Down
1 change: 1 addition & 0 deletions scapy/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ def _add_fake_iface(self, ifname):
'guid': "{%s}" % uuid.uuid1(),
'ipv4_metric': 0,
'ipv6_metric': 0,
'nameservers': [],
}
if WINDOWS:
from scapy.arch.windows import NetworkInterface_Win, \
Expand Down
77 changes: 76 additions & 1 deletion scapy/layers/dns.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
import time
import warnings

from scapy.arch import get_if_addr, get_if_addr6
from scapy.arch import (
get_if_addr,
get_if_addr6,
read_nameservers,
)
from scapy.ansmachine import AnsweringMachine
from scapy.base_classes import Net
from scapy.config import conf
Expand All @@ -26,7 +30,9 @@
PacketListField, ShortEnumField, ShortField, StrField, \
StrLenField, MultipleTypeField, UTCTimeField, I
from scapy.sendrecv import sr1
from scapy.supersocket import StreamSocket
from scapy.pton_ntop import inet_ntop, inet_pton
from scapy.volatile import RandShort

from scapy.layers.inet import IP, DestIPField, IPField, UDP, TCP
from scapy.layers.inet6 import IPv6, DestIP6Field, IP6Field
Expand Down Expand Up @@ -284,6 +290,7 @@ class DNSStrField(StrLenField):
def h2i(self, pkt, x):
if not x:
return b"."
x = bytes_encode(x)
if x[-1:] != b"." and not _is_ptr(x):
return x + b"."
return x
Expand Down Expand Up @@ -1046,6 +1053,74 @@ def pre_dissect(self, s):
bind_layers(TCP, DNS, dport=53)
bind_layers(TCP, DNS, sport=53)

# Nameserver config
conf.nameservers = read_nameservers()
_dns_cache = conf.netcache.new_cache("dns_cache", 300)


@conf.commands.register
def dns_resolve(qname, qtype="A", verbose=1, **kwargs):
"""
Perform a simple DNS resolution using conf.nameservers with caching
"""
answer = _dns_cache.get("_".join([qname, qtype]))
if answer:
return answer

kwargs.setdefault("timeout", 5)
kwargs.setdefault("verbose", 0)
for nameserver in conf.nameservers:
# Try all nameservers
try:
# Spawn a UDP socket, connect to the nameserver on port 53
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(kwargs["timeout"])
sock.connect((nameserver, 53))
# Connected. Wrap it with DNS
sock = StreamSocket(sock, DNS)
# I/O
res = sock.sr1(
DNS(qd=[DNSQR(qname=qname, qtype=qtype)], id=RandShort()),
**kwargs,
)
except IOError as ex:
if verbose:
log_runtime.warning(str(ex))
continue
finally:
sock.close()
if res:
# We have a response ! Check for failure
if res[DNS].rcode == 2: # server failure
res = None
if verbose:
log_runtime.info(
"DNS: %s answered with failure for %s" % (
nameserver,
qname,
)
)
else:
break
if res is not None:
# Calc expected qname and qtype
eqname = DNSQR.qname.h2i(None, qname)
eqtype = DNSQR.qtype.any2i_one(None, qtype)
try:
# Find answer
answer = next(
x.rdata
for x in res.an
if x.type == eqtype and x.rrname == eqname
)
# Cache it
_dns_cache["_".join([qname, qtype])] = answer
return answer
except StopIteration:
# No answer
pass
return None


@conf.commands.register
def dyndns_add(nameserver, name, rdata, type="A", ttl=10):
Expand Down
14 changes: 14 additions & 0 deletions test/scapy/layers/dns.uts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ def _test():

dns_ans = retry_test(_test)

= DNS request using dns_resolve
~ netaccess DNS
* this is not using a raw socket so should also work without root

val = dns_resolve(qname="google.com", qtype="A")
assert val
assert inet_pton(socket.AF_INET, val)
assert val == conf.netcache.dns_cache["google.com_A"]

val = dns_resolve(qname="google.com", qtype="AAAA")
assert val
assert inet_pton(socket.AF_INET6, val)
assert val == conf.netcache.dns_cache["google.com_AAAA"]

= DNS labels
~ DNS
query = DNSQR(qname=b"www.secdev.org")
Expand Down

0 comments on commit 7f406c8

Please sign in to comment.