| # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import dpkt |
| import socket |
| import struct |
| |
| from lansim import tools |
| |
| |
| # An initial set of Protocol to Hardware address mappings. |
| _ARP_INITIAL_CACHE = { |
| # Broadcast address: |
| socket.inet_aton('255.255.255.255'): tools.inet_hwton('FF:FF:FF:FF:FF:FF'), |
| } |
| |
| |
| class SimpleHostError(Exception): |
| """A SimpleHost generic error.""" |
| |
| |
| class SimpleHost(object): |
| """A simple host supporting IPv4. |
| |
| This class is useful as a base clase to implement other hosts. It supports |
| a single IPv4 address. |
| """ |
| def __init__(self, sim, hw_addr, ip_addr): |
| """Creates the host and associates it with the given NetworkBridge. |
| |
| @param sim: The Simulator interface where this host lives. |
| @param hw_addr: Hex or binary representation of the Ethernet address. |
| @param ip_addr: The IPv4 address. For example: "10.0.0.1". |
| """ |
| self._sim = sim |
| self._hw_addr = hw_addr |
| self._ip_addr = ip_addr |
| self._bin_hw_addr = tools.inet_hwton(hw_addr) |
| self._bin_ip_addr = socket.inet_aton(ip_addr) |
| # arp cache: Protocol to Hardware address resolution cache. |
| self._arp_cache = dict(_ARP_INITIAL_CACHE) |
| # Reply to broadcast ARP requests. |
| rule = { |
| "dst": "\xff" * 6, # Broadcast HW addr. |
| "arp.pln": 4, # Protocol Addres Length is 4 (IP v4). |
| "arp.op": dpkt.arp.ARP_OP_REQUEST, |
| "arp.tpa": self._bin_ip_addr} |
| sim.add_match(rule, self.arp_request) |
| |
| # Reply to unicast ARP requests. |
| rule["dst"] = self._bin_hw_addr |
| sim.add_match(rule, self.arp_request) |
| |
| # Mappings used for TCP traffic forwarding. |
| self._tcp_fwd_in = {} |
| self._tcp_fwd_out = {} |
| self._tcp_fwd_ports = {} |
| |
| @property |
| def ip_addr(self): |
| """Returns the host IPv4 address.""" |
| return self._ip_addr |
| |
| |
| @property |
| def simulator(self): |
| """Returns the Simulator instance where this host runs on.""" |
| return self._sim |
| |
| |
| def arp_request(self, pkt): |
| """Sends the ARP_REPLY matching the request. |
| |
| @param pkt: a dpkt.Packet with the ARP_REQUEST. |
| """ |
| # Update the local ARP cache whenever we get a request. |
| self.add_arp(hw_addr=pkt.arp.sha, ip_addr=pkt.arp.spa) |
| |
| arp_resp = dpkt.arp.ARP( |
| op = dpkt.arp.ARP_OP_REPLY, |
| pln = 4, |
| tpa = pkt.arp.spa, # Target Protocol Address. |
| tha = pkt.arp.sha, # Target Hardware Address. |
| spa = self._bin_ip_addr, # Source Protocol Address. |
| sha = self._bin_hw_addr) # Source Hardware Address. |
| eth_resp = dpkt.ethernet.Ethernet( |
| dst = pkt.arp.sha, |
| src = self._bin_hw_addr, |
| type = dpkt.ethernet.ETH_TYPE_ARP, |
| data = arp_resp) |
| self._sim.write(eth_resp) |
| |
| |
| def add_arp(self, hw_addr, ip_addr): |
| """Maps the ip_addr to a given hw_addr. |
| |
| This is useful to send IP packets with send_ip() to hosts that haven't |
| comunicate with us yet. |
| |
| @param hw_addr: The network encoded corresponding Ethernet address. |
| @param ip_addr: The network encoded IPv4 address. |
| """ |
| self._arp_cache[ip_addr] = hw_addr |
| |
| |
| def _resolve_mac_address(self, ip_addr): |
| """Resolves the hw_addr of an IP address locally when it is known. |
| |
| This method uses the information gathered from received ARP requests and |
| locally added mappings with add_arp(). It also knows how to resolve |
| multicast addresses. |
| |
| @param ip_addr: The IP address to resolve encoded in network format. |
| @return: The Hardware address encoded in network format or None |
| if unknown. |
| @raise SimpleHostError if the MAC address for ip_addr is unknown. |
| """ |
| # From RFC 1112 6.4: |
| # An IP host group address is mapped to an Ethernet multicast address |
| # by placing the low-order 23-bits of the IP address into the low-order |
| # 23 bits of the Ethernet multicast address 01-00-5E-00-00-00 (hex). |
| # Because there are 28 significant bits in an IP host group address, |
| # more than one host group address may map to the same Ethernet |
| # multicast address. |
| int_ip_addr, = struct.unpack('!I', ip_addr) |
| if int_ip_addr & 0xF0000000 == 0xE0000000: # Multicast IP address |
| int_hw_ending = int_ip_addr & ((1 << 23) - 1) | 0x5E000000 |
| return '\x01\x00' + struct.pack('!I', int_hw_ending) |
| if ip_addr in self._arp_cache: |
| return self._arp_cache[ip_addr] |
| # No address found. |
| raise SimpleHostError("Unknown destination IP host.") |
| |
| |
| def send_ip(self, pkt): |
| """Sends an IP packet. |
| |
| The source IP address and the hardware layer is automatically filled. |
| @param pkt: A dpkg.ip.IP packet. |
| @raise SimpleHostError if the MAC address for ip_addr is unknown. |
| """ |
| hw_dst = self._resolve_mac_address(pkt.dst) |
| |
| pkt.src = self._bin_ip_addr |
| # Set the packet length and force to recompute the checksum. |
| pkt.len = len(pkt) |
| pkt.sum = 0 |
| hw_pkt = dpkt.ethernet.Ethernet( |
| dst = hw_dst, |
| src = self._bin_hw_addr, |
| type = dpkt.ethernet.ETH_TYPE_IP, |
| data = pkt) |
| return self._sim.write(hw_pkt) |
| |
| |
| def tcp_forward(self, port, dest_addr, dest_port): |
| """Forwards all the TCP/IP traffic on a given port to another host. |
| |
| This method makes all the incoming traffic for this host on a particular |
| port be redirected to dest_addr:dest_port. This allows us to use the |
| kernel's network stack to handle that traffic. |
| |
| @param port: The TCP port on this simulated host. |
| @param dest_addr: A host IP address on the same network in plain text. |
| @param dest_port: The TCP port on the destination host. |
| """ |
| if not self._tcp_fwd_ports: |
| # Lazy initialization. |
| self._sim.add_match({ |
| 'ip.dst': self._bin_ip_addr, |
| 'ip.p': dpkt.ip.IP_PROTO_TCP}, self._handle_tcp_forward) |
| |
| self._tcp_fwd_ports[port] = socket.inet_aton(dest_addr), dest_port |
| |
| |
| def _tcp_pick_port(self, dhost, dport): |
| """Picks a new unused source TCP port on the host.""" |
| for p in range(1024, 65536): |
| if (dhost, dport, p) in self._tcp_fwd_out: |
| continue |
| if p in self._tcp_fwd_ports: |
| continue |
| return p |
| raise SimpleHostError("Too many connections.") |
| |
| |
| def _handle_tcp_forward(self, pkt): |
| # Source from: |
| shost = pkt.ip.src |
| sport = pkt.ip.tcp.sport |
| dport = pkt.ip.tcp.dport |
| |
| ### Handle responses from forwarded traffic back to the sender (out). |
| if (shost, sport, dport) in self._tcp_fwd_out: |
| fhost, fport, oport = self._tcp_fwd_out[(shost, sport, dport)] |
| # Redirect the packet |
| pkt.ip.tcp.sport = oport |
| pkt.ip.tcp.dport = fport |
| pkt.ip.dst = fhost |
| pkt.ip.tcp.sum = 0 # Force checksum |
| self.send_ip(pkt.ip) |
| return |
| |
| ### Handle incoming traffic to a local forwarded port (in). |
| if dport in self._tcp_fwd_ports: |
| # Forward to: |
| fhost, fport = self._tcp_fwd_ports[dport] |
| |
| ### Check if it is an existing connection. |
| # lport: The port from where we send data out. |
| if (shost, sport, dport) in self._tcp_fwd_in: |
| lport = self._tcp_fwd_in[(shost, sport, dport)] |
| else: |
| # Pick a new local port on our side. |
| lport = self._tcp_pick_port(fhost, fport) |
| self._tcp_fwd_in[(shost, sport, dport)] = lport |
| self._tcp_fwd_out[(fhost, fport, lport)] = (shost, sport, dport) |
| |
| # Redirect the packet |
| pkt.ip.tcp.sport = lport |
| pkt.ip.tcp.dport = fport |
| pkt.ip.dst = fhost |
| pkt.ip.tcp.sum = 0 # Force checksum |
| self.send_ip(pkt.ip) |
| return |
| |
| |
| def socket(self, family, sock_type): |
| """Creates an asynchronous socket on the simulated host. |
| |
| This method creates an asynchronous socket object that can be used to |
| receive and send packets. This module only supports UDP sockets. |
| |
| @param family: The socket family, only AF_INET is supported. |
| @param sock_type: The socket type, only SOCK_DGRAM is supported. |
| @return: an UDPSocket object. See UDPSocket documentation for details. |
| @raise SimpleHostError if socket family and type is not supported. |
| """ |
| if family != socket.AF_INET: |
| raise SimpleHostError("socket family not supported.") |
| if sock_type != socket.SOCK_DGRAM: |
| raise SimpleHostError("socket type not supported.") |
| |
| return UDPSocket(self) |
| |
| |
| class UDPSocket(object): |
| """An asynchronous UDP socket interface. |
| |
| This UDP socket interface provides a way to send and received UDP messages |
| on an asynchronous way. This means that the socket doesn't have a recv() |
| method as a normal socket would have, since the simulation is event driven |
| and a callback should not block its execution. See the listen() method to |
| see how to receive messages from this socket. |
| |
| This interface is used by modules outside lansim to interact with lansim |
| in a way that can be ported to other different backends. For example, this |
| same interface could be implemented using the Python's socket module and |
| the real kernel stack. |
| """ |
| def __init__(self, host): |
| """Initializes the UDP socket. |
| |
| To be used for receiving packets, listen() must be called. |
| |
| @param host: A SimpleHost object. |
| """ |
| self._host = host |
| self._sim = host.simulator |
| self._port = None |
| |
| |
| def __del__(self): |
| self.close() |
| |
| |
| def listen(self, ip_addr, port, recv_callback): |
| """Bind and listen on the ip_addr:port. |
| |
| Calls recv_callback(pkt, src_addr, src_port) every time an UDP frame |
| is received. src_addr and src_port are passed with the source IPv4 |
| (as in '192.168.0.2') and the sender port number. |
| |
| This function can only be called once, since the socket can't be |
| reassigned. |
| |
| @param ip_addr: Local destination ip_addr. If None, the Host's IPv4 |
| address is used, for example '224.0.0.251' or '192.168.0.1'. |
| @param port: Local destination port number. |
| @param recv_callback: A callback function that accepts three |
| arguments, the received string, the sender IPv4 address and the |
| sender port number. |
| """ |
| if ip_addr is None: |
| ip_addr = self._host.ip_addr() |
| self._port = port |
| |
| # Binds all the traffic to the provided callback converting the |
| # single argument callback to the multiple argument. |
| self._sim.add_match({ |
| "ip.dst": socket.inet_aton(ip_addr), |
| "ip.udp.dport": port}, |
| lambda pkt: recv_callback(pkt.ip.udp.data, |
| socket.inet_ntoa(pkt.ip.src), |
| pkt.ip.udp.sport)) |
| |
| |
| def send(self, data, ip_addr, port): |
| """Send an UDP message with the data string to ip_addr:port. |
| |
| @param data: Any string small enough to fit in a single UDP packet. |
| @param ip_addr: Destination IPv4 address. |
| @param port: Destination UDP port number. |
| """ |
| pkt_udp = dpkt.udp.UDP( |
| dport = port, |
| sport = self._port if self._port != None else 0, |
| data = data |
| ) |
| # dpkt doesn't set the Length field on UDP packets according to RFC 768. |
| pkt_udp.ulen = len(pkt_udp.pack_hdr()) + len(str(pkt_udp.data)) |
| |
| pkt_ip = dpkt.ip.IP( |
| dst = socket.inet_aton(ip_addr), |
| ttl = 255, # The comon value for IP packets. |
| off = dpkt.ip.IP_DF, # Don't frag. |
| p = dpkt.ip.IP_PROTO_UDP, |
| data = pkt_udp |
| ) |
| self._host.send_ip(pkt_ip) |
| |
| |
| def close(self): |
| """Closes the socket and disconnects the bound callback.""" |
| #TODO(deymo): Remove the add_match rule added on listen(). |