Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dns helper v2 #104

Open
wants to merge 5 commits into
base: dart
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions pox/misc/knowledgebase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Copyright (c) 2013 Felician Nemeth
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
A simple knowledgebase for querying apriori configuration.

The knowledgebase module stores some aspects of the intended
configuration of or other apriori information about the controlled
network. (Whereas, for example, the topology module collects the
current/actual view of the network.)

It takes one "file" commandline argument, e.g.,
knowledgebase --file=db.csv

Example of db.csv:
hostname,ip,mac,dpid,routing
h1,10.0.0.1,,,
h2,10.0.0.2,22:22:22:22:22:22,,
,,3,forwarding.l2_learning
,,4,forwarding.hub

Knowledgebase stores values as strings. Should type be specified in
the header row, e.g., "mac:EthAddr,ip:IPAddr,name:str"?

The file format is disputable (json, yaml, ...), but it might be a
good idea to be somewhat compatible with mininet (once mininet defines
its own format). See: https://github.com/mininet/mininet/pull/157

In your code, you can query Knowledgebase like this:
if hasattr(core, 'Knowledgebase'):
result = core.Knowledgebase.query(ip='10.0.0.1')
else:
log.warn('knowledgebase is not loaded')
See Knowledgebase.query for details.

Knowledgebase is potentially used by:
proto.dns_responder
"""

from csv import DictReader
from pox.core import core

log = core.getLogger()

class Knowledgebase ():
def __init__ (self, file=None):
self._filename=file
self._db = {}
self._read_db()

def _read_db (self):
"""Read database from a CSV file."""
try:
with open(self._filename) as f:
self._db = [d for d in DictReader(f)]
except IOError as e:
log.error('IOError:%s' % e)
log.debug('db:%s' % self._db)

def query (self, **kw):
"""
Query the knowledgebase. Returns a list of dictionaries that
contains all the key-vaule pairs of 'kw'. Examples:

# simple query
core.Knowledgebase.query(ip='10.0.0.1')

# select one of many interfaces
core.Knowledgebase.query(hostname='h1', ip='10.0.0.1')

# or
i = {'hostname': 'h1', 'ip': '10.0.0.1'}
core.Knowledgebase.query(**i)
"""
result = []
for item in self._db:
for k, v in kw.iteritems():
if v != item.get(k):
break
else:
result.append(item)

# For the sake of extensibility, here we could emit a
# KnowledgebaseQuery event asking other modules to append their
# answers if they can.

return result

def launch (file=None):
core.registerNew(Knowledgebase, file)
182 changes: 182 additions & 0 deletions pox/proto/dns_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Copyright 2011-2012 James McCauley
# Copyright 2013 Felician Nemeth
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
dns_helper hides the details of DNS communication from other modules.

This module follows the logic of arp_helper by hiding the wire format
details of DNS packets and turning the into general events.
Therefore, higher layer modules can answer DNS lookups and process DNS
answers without understanding DNS packet formats. Although,
additional low level details are exposed in the events raised by
dns_helper.

TODO: option to eat_packets?

Modules relying on dns_helper:
dns_responder
dns_spy
"""

from pox.core import core
from pox.lib.revent import *
import pox.openflow.libopenflow_01 as of
from pox.lib.addresses import IPAddr, EthAddr
import pox.lib.packet as pkt
from pox.lib.packet.dns import rrtype_to_str, rrclass_to_str

log = core.getLogger()

class DNSAnswer (Event):
def __init__ (self, item):
super(DNSUpdate, self).__init__()
self.item = item

class DNSLookup (Event):
"""
Fired with one DNS question.

When dns_helper receives a DNS packet, it fires a DNSLookup event
for each question it founds in the packet. Event handlers may
answer this question by setting rr_answers or simple_answers.

rr_answers is a list of full "rr"s, while simple_answers is a list
of IP addresses for A lookups, fully qualified domain names for PTR
lookups, etc.
"""
def __init__ (self, rr):
super(DNSLookup, self).__init__()

self.name = rr.name
self.qtype = rr.qtype

self.rr = rr
for t in rrtype_to_str.values():
setattr(self, t, False)
t = rrtype_to_str.get(rr.qtype)
if t is not None:
setattr(self, t, True)
setattr(self, "UNKNOWN", False)
self.qtype_name = t
else:
setattr(self, "UNKNOWN", True)
self.qtype_name = "UNKNOWN"
self.rr_answers = []
self.simple_answers = []

class DNSHelper (EventMixin):
_eventMixin_events = set([ DNSAnswer, DNSLookup ])

def __init__ (self, install_flow = True):
self._install_flow = install_flow
core.openflow.addListeners(self)

def _send_response (self, event, answers):
q_eth = event.parsed.find('ethernet')
q_ip = event.parsed.find('ipv4')
q_udp = event.parsed.find('udp')
if not (q_eth and q_ip and q_udp):
return

if q_udp.dstport == pkt.dns.MDNS_PORT:
# respose will appear to be sent from here:
r_ip_src = IPAddr('192.0.2.1')
r_eth_src = EthAddr('11:22:33:44:55:66')
# TODO random address is a bad idea. We should dig up an
# address from the answers.

# multicast destination:
r_ip_dst = q_ip.dstip
r_eth_dst = q_eth.dst
else:
r_ip_src = q_ip.dstip
r_eth_src = q_eth.dst
r_ip_dst = q_ip.srcip
r_eth_dst = q_eth.src

r_dns = pkt.dns()
r_dns.qr = True
r_dns.aa = True
r_dns.answers = answers

r_udp = pkt.udp()
r_udp.srcport = q_udp.dstport
r_udp.dstport = q_udp.srcport
r_udp.payload = r_dns

r_ip = pkt.ipv4(srcip=r_ip_src, dstip=r_ip_dst)
r_ip.protocol = r_ip.UDP_PROTOCOL
r_ip.payload = r_udp

r_eth = pkt.ethernet(src=r_eth_src, dst=r_eth_dst)
r_eth.type = pkt.ethernet.IP_TYPE
r_eth.payload = r_ip

r_pkt = of.ofp_packet_out(data=r_eth.pack())
r_pkt.actions.append(of.ofp_action_output(port=event.port))

log.debug('response: %s' % r_dns)
event.connection.send(r_pkt)

def _handle_ConnectionUp (self, event):
if self._install_flow:
def install_one_flow (event, port):
msg = of.ofp_flow_mod()
msg.match = of.ofp_match()
msg.match.dl_type = pkt.ethernet.IP_TYPE
msg.match.nw_proto = pkt.ipv4.UDP_PROTOCOL
msg.match.tp_src = port
msg.actions.append(of.ofp_action_output(port = of.OFPP_CONTROLLER))
event.connection.send(msg)

install_one_flow(event, pkt.dns.SERVER_PORT)
install_one_flow(event, pkt.dns.MDNS_PORT)

def _handle_PacketIn (self, event):
p = event.parsed.find('dns')

if p is None or not p.parsed:
return
log.debug(p)

answers = []
for q in p.questions:
if rrclass_to_str.get(q.qclass, '') != "IN":
continue # Internet only
ev = DNSLookup(q)
self.raiseEvent(ev)
for rr in ev.rr_answers:
answers.append(rr)
for rrdata in ev.simple_answers:
ttl = 120 # 2 minutes
length = 0 # should be calculated at 'packing'
rr = pkt.dns.rr(q.name, q.qtype, q.qclass, ttl, length, rrdata)
answers.append(rr)
if answers:
self._send_response(event, answers)

def process_q (entry):
if rrclass_to_str.get(entry.qclass, '') != "IN":
return # Internet only
self.raiseEvent(DNSAnswer, entry)

for answer in p.answers:
process_q(answer)
for addition in p.additional:
process_q(addition)


def launch (no_flow = False):
core.registerNew(DNSHelper, not no_flow)
108 changes: 108 additions & 0 deletions pox/proto/dns_responder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Copyright (c) 2013 Felician Nemeth
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at:
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Dns_responder answers mDNS queries.

TODO: rename dsn_responder to mdsn_responder, mdsn_server, dsn_local,
or something?

Why? Simple routing modules might not handle multicast traffic.
Additionally, it is sometimes not practical to set up a real DNS
server either.

On the other hand, we might take care of the lookups using out-of-band
information. Add dns entries on commandline like:
dns_responder --<IP>=<hostname> --<IP>=<hostname>
e.g.,
dns_responder --10.0.0.2=h2 --10.0.0.3=h3
Or use the knowledgebase module like:
dns_responder knowledgebase --file=dns.csv
with "dns.csv" containing
hostname,ip
h2,10.0.0.2
h3,10.0.0.3

Note, libnss-mdns doesn't work in mininet: running "getent hosts
h2.local" on h1 doesn't send ipv4 packets out on interface h1-eth0.
"""

from pox.core import core
from pox.lib.addresses import IPAddr

log = core.getLogger()

class DnsResponder (object):
def __init__ (self, db=[]):
self._db = db
core.listen_to_dependencies(self)

def _answer_A (self, q):
if not q.name.endswith('.local'):
log.debug('ignoring question: %s' % q)
return None
name = q.name[:-len('.local')]

if hasattr(core, 'Knowledgebase'):
kb = core.Knowledgebase.query(hostname=name)
else:
kb = []

for item in (self._db + kb):
if (item.get('hostname', '').lower() == name.lower()):
# TODO multiple addresses in the response?
ip_str = item.get('ip')
log.info('answering: %s with %s' % (q.name, ip_str))
return IPAddr(ip_str)
return None

def _answer_PTR (self, q):
if not q.name.endswith('.in-addr.arpa'):
log.debug('ignoring question: %s' % q)
return None
name = q.name[:-len('.in-addr.arpa')]
q_ip_str = name.split('.')
q_ip_str.reverse()
q_ip_str = '.'.join(q_ip_str)
q_ip = IPAddr(q_ip_str)

if hasattr(core, 'Knowledgebase'):
kb = core.Knowledgebase.query(ip=q_ip_str)
else:
kb = []

for item in (self._db + kb):
if (IPAddr(item.get('ip')) == q_ip):
# TODO multiple hostnames in the response?
hostname = item.get('hostname')
if hostname:
log.info('answering: %s with %s' % (q.name, hostname))
return hostname + '.local'
return None

def _answer_UNKNOWN (self, q):
log.debug('ignoring question: %s' % q)
return None

def _handle_DNSHelper_DNSLookup (self, event):
log.debug('q: (%s,%s)' % (event.name, event.qtype))
attr = getattr(self, "_answer_" + event.qtype_name, self._answer_UNKNOWN)
answer = attr(event)
if answer:
event.simple_answers.append(answer)
return

def launch (**kw):
db = [{"ip": k, "hostname": v} for k,v in kw.iteritems()]
core.registerNew(DnsResponder, db)
Loading