Esempio n. 1
0
class RedirectionManager:
    """
    RedirectionManager

    The redirection manager handles subscribers who have redirection enabled,
    it adds the flows into ovs for redirecting user to the redirection server
    """
    DNS_TIMEOUT_SECS = 15

    REDIRECT_NOT_PROCESSED = REG_ZERO_VAL
    REDIRECT_PROCESSED = 0x1

    RedirectRequest = namedtuple(
        'RedirectRequest',
        ['imsi', 'ip_addr', 'rule', 'rule_num', 'rule_version', 'priority'],
    )

    def __init__(self, bridge_ip, logger, main_tbl_num, next_table,
                 scratch_table_num, session_rule_version_mapper):
        self._bridge_ip = bridge_ip
        self.logger = logger
        self.main_tbl_num = main_tbl_num
        self.next_table = next_table
        self._scratch_tbl_num = scratch_table_num
        self._redirect_dict = RedirectDict()
        self._dns_cache = Memoizer({})
        self._redirect_port = get_service_config_value('redirectd',
                                                       'http_port', 8080)
        self._session_rule_version_mapper = session_rule_version_mapper

        self._cwf_args_set = False
        self._mac_rewrite_scratch = None
        self._internal_ip_allocator = None
        self._arpd_controller_fut = None
        self._arp_contoller = None
        self._egress_table = None
        self._bridge_mac = None

    def set_cwf_args(self, internal_ip_allocator, arp, mac_rewrite,
                     bridge_name, egress_table):
        self._mac_rewrite_scratch = mac_rewrite
        self._internal_ip_allocator = internal_ip_allocator
        self._arpd_controller_fut = arp
        self._arp_contoller = None
        self._egress_table = egress_table

        def get_virtual_iface_mac(iface):
            virt_ifaddresses = netifaces.ifaddresses(iface)
            return virt_ifaddresses[netifaces.AF_LINK][0]['addr']

        self._bridge_mac = get_virtual_iface_mac(bridge_name)
        self._cwf_args_set = True
        return self

    def setup_lte_redirect(self, datapath, loop, redirect_request):
        """
        Depending on redirection server address type install redirection rules
        """
        imsi = redirect_request.imsi
        ip_addr = redirect_request.ip_addr
        rule = redirect_request.rule
        rule_num = redirect_request.rule_num
        rule_version = redirect_request.rule_version
        priority = redirect_request.priority

        # TODO IMPORTANT check that redirectd service is running, as its a
        # dynamic service its not on by default. Will save you some sanity :)

        # TODO figure out what to do with SIP_URI
        if rule.redirect.address_type == rule.redirect.SIP_URI:
            raise RedirectException("SIP_URIs redirection isn't setup")
        if rule.redirect.address_type == rule.redirect.IPv6:
            raise RedirectException("No ipv6 support, so no ipv6 redirect")

        self._save_redirect_entry(ip_addr, rule.redirect)
        self._install_redirect_flows(datapath, loop, imsi, ip_addr, rule,
                                     rule_num, rule_version, priority)
        return

    def _install_redirect_flows(self, datapath, loop, imsi, ip_addr, rule,
                                rule_num, rule_version, priority):
        """
        Add flows to forward traffic to the redirection server.

        1) Intercept tcp traffic to the web to the redirection server, which
            completes the tcp handshake. This is done by adding an OVS flow
            with a learn action (flow catches inbound tcp packets, while learn
            action creates another flow that sends packets back from server)

        2) Add flows to allow UDP traffic so DNS queries can go through.
            Finally add flows with a higher priority that allow traffic to and
            from the address provided in redirect rule.
        """

        if rule.redirect.address_type == rule.redirect.URL:
            self._install_url_bypass_flows(datapath,
                                           loop,
                                           imsi,
                                           rule,
                                           rule_num,
                                           rule_version,
                                           priority,
                                           ue_ip=ip_addr)
        elif rule.redirect.address_type == rule.redirect.IPv4:
            self._install_ipv4_bypass_flows(datapath,
                                            imsi,
                                            rule,
                                            rule_num,
                                            rule_version,
                                            priority,
                                            [rule.redirect.server_address],
                                            ue_ip=ip_addr)

        self._install_dns_flows(datapath, imsi, rule, rule_num, rule_version,
                                priority)
        self._install_server_flows(datapath, imsi, ip_addr, rule, rule_num,
                                   rule_version, priority)

    def _install_scratch_table_flows(self, datapath, imsi, rule, rule_num,
                                     rule_version, priority):
        """
        The flow action for subscribers that need to be redirected does 2 things
            * Forward requests from subscriber to the internal http server
            * Instantiate a flow that matches response packets from the server
              and sends them back to subscriber
        Match: incoming tcp traffic with port 80, direction out
        Action:
            1) Set reg2 to rule_num
            2) Set ip dst to server ip
            3) Output to table 20
            4) Apply LearnAction:
            LearnAction(adds new flow for every pkt flow that hits this rule)
                1) Match ip packets
                2) Match tcp protocol
                3) Match packets from LOCAL port
                4) Match ip src = server ip
                5) Match ip dst = current flow ip src
                6) Match tcp src = current flow tcp dst
                7) Match tcp dst = current flow tcp src
                8) Load ip src = current flow ip dst
                9) Output through gtp0
        """
        parser = datapath.ofproto_parser
        match_http = MagmaMatch(eth_type=ether_types.ETH_TYPE_IP,
                                ip_proto=IPPROTO_TCP,
                                tcp_dst=80,
                                imsi=encode_imsi(imsi),
                                direction=Direction.OUT)
        of_note = parser.NXActionNote(list(rule.id.encode()))

        actions = [
            parser.NXActionLearn(
                table_id=self.main_tbl_num,
                priority=priority,
                cookie=rule_num,
                specs=[
                    parser.NXFlowSpecMatch(src=ether_types.ETH_TYPE_IP,
                                           dst=('eth_type_nxm', 0),
                                           n_bits=16),
                    parser.NXFlowSpecMatch(src=IPPROTO_TCP,
                                           dst=('ip_proto_nxm', 0),
                                           n_bits=8),
                    parser.NXFlowSpecMatch(src=Direction.IN,
                                           dst=(DIRECTION_REG, 0),
                                           n_bits=32),
                    parser.NXFlowSpecMatch(src=int(
                        ipaddress.IPv4Address(self._bridge_ip)),
                                           dst=('ipv4_src_nxm', 0),
                                           n_bits=32),
                    parser.NXFlowSpecMatch(src=('ipv4_src_nxm', 0),
                                           dst=('ipv4_dst_nxm', 0),
                                           n_bits=32),
                    parser.NXFlowSpecMatch(src=('tcp_src_nxm', 0),
                                           dst=('tcp_dst_nxm', 0),
                                           n_bits=16),
                    parser.NXFlowSpecMatch(src=self._redirect_port,
                                           dst=('tcp_src_nxm', 0),
                                           n_bits=16),
                    parser.NXFlowSpecMatch(src=encode_imsi(imsi),
                                           dst=(IMSI_REG, 0),
                                           n_bits=64),
                    parser.NXFlowSpecLoad(src=('ipv4_dst_nxm', 0),
                                          dst=('ipv4_src_nxm', 0),
                                          n_bits=32),
                    parser.NXFlowSpecLoad(src=80,
                                          dst=('tcp_src_nxm', 0),
                                          n_bits=16),
                    # Learn doesn't support resubmit to table, so send directly
                    parser.NXFlowSpecOutput(src=('in_port', 0),
                                            dst="",
                                            n_bits=16),
                ]),
            parser.NXActionRegLoad2(dst=SCRATCH_REGS[0],
                                    value=self.REDIRECT_PROCESSED),
            parser.OFPActionSetField(ipv4_dst=self._bridge_ip),
            parser.OFPActionSetField(tcp_dst=self._redirect_port),
            of_note,
        ]
        actions += self._load_rule_actions(parser, rule_num, rule_version)

        flows.add_resubmit_current_service_flow(
            datapath,
            self._scratch_tbl_num,
            match_http,
            actions,
            priority=priority,
            cookie=rule_num,
            hard_timeout=rule.hard_timeout,
            resubmit_table=self.main_tbl_num)

        match = MagmaMatch(imsi=encode_imsi(imsi))
        action = []
        flows.add_drop_flow(datapath,
                            self._scratch_tbl_num,
                            match,
                            action,
                            priority=flows.MINIMUM_PRIORITY + 1,
                            cookie=rule_num)

    def _install_not_processed_flows(self, datapath, imsi, ip_addr, rule,
                                     rule_num, priority):
        """
        Redirect all traffic to the scratch table to only allow redirected
        http traffic to go through, the rest will be dropped. reg0 is used as
        a boolean to know whether the drop rule was processed.
        """
        parser = datapath.ofproto_parser
        of_note = parser.NXActionNote(list(rule.id.encode()))

        match = MagmaMatch(imsi=encode_imsi(imsi),
                           direction=Direction.OUT,
                           reg0=self.REDIRECT_NOT_PROCESSED,
                           eth_type=ether_types.ETH_TYPE_IP,
                           ipv4_src=ip_addr)
        action = [of_note]
        flows.add_resubmit_current_service_flow(
            datapath,
            self.main_tbl_num,
            match,
            action,
            priority=priority,
            cookie=rule_num,
            hard_timeout=rule.hard_timeout,
            resubmit_table=self._scratch_tbl_num)

        match = MagmaMatch(imsi=encode_imsi(imsi),
                           direction=Direction.OUT,
                           reg0=self.REDIRECT_PROCESSED,
                           eth_type=ether_types.ETH_TYPE_IP,
                           ipv4_src=ip_addr)
        action = [of_note]
        flows.add_resubmit_next_service_flow(datapath,
                                             self.main_tbl_num,
                                             match,
                                             action,
                                             priority=priority,
                                             cookie=rule_num,
                                             hard_timeout=rule.hard_timeout,
                                             resubmit_table=self.next_table)

    def setup_cwf_redirect(self, datapath, loop, redirect_request):
        """
        Add flows to forward traffic to the redirection server for cwf networks

        1) Intercept tcp traffic to the web to the redirection server, which
            completes the tcp handshake. Also overwrite UE src ip to match the
            subnet of the redirection server.
            This is done by assigning an internal IP per each subscriber.
            Add an OVS flow with a learn action (flow catches inbound tcp http
            packets, while learn action creates another flow that rewrites
            packet back to send to ue)

        2) Add flows to allow UDP traffic so DNS queries can go through.
            Add flows with a higher priority that allow traffic to and
            from the address provided in redirect rule.

        TODO we might want to track stats for these rules and report to sessiond
        """
        if not self._cwf_args_set:
            raise RedirectException("Can't install cwf redirection, missing"
                                    "cwf specific args, call set_cwf_args()")
        imsi = redirect_request.imsi
        rule = redirect_request.rule
        rule_num = redirect_request.rule_num
        rule_version = redirect_request.rule_version
        priority = redirect_request.priority
        if rule.redirect.address_type == rule.redirect.URL:
            self._install_url_bypass_flows(datapath, loop, imsi, rule,
                                           rule_num, rule_version, priority)
        elif rule.redirect.address_type == rule.redirect.IPv4:
            self._install_ipv4_bypass_flows(datapath, imsi, rule, rule_num,
                                            rule_version, priority,
                                            [rule.redirect.server_address])

        parser = datapath.ofproto_parser
        # TODO use subscriber ip_addr to generate internal IP and release
        # internal IP when subscriber disconnects or redirection flow is removed
        internal_ip = self._internal_ip_allocator.next_ip()

        self._save_redirect_entry(internal_ip, rule.redirect)
        #TODO check if we actually need this, dns might already be allowed
        self._install_dns_flows(datapath, imsi, rule, rule_num, rule_version,
                                priority)

        match_tcp_80 = MagmaMatch(imsi=encode_imsi(imsi),
                                  eth_type=ether_types.ETH_TYPE_IP,
                                  ip_proto=IPPROTO_TCP,
                                  direction=Direction.OUT,
                                  tcp_dst=80)
        match_tcp_8008 = MagmaMatch(imsi=encode_imsi(imsi),
                                    eth_type=ether_types.ETH_TYPE_IP,
                                    ip_proto=IPPROTO_TCP,
                                    direction=Direction.OUT,
                                    tcp_dst=8080)
        match_tcp_8080 = MagmaMatch(imsi=encode_imsi(imsi),
                                    eth_type=ether_types.ETH_TYPE_IP,
                                    ip_proto=IPPROTO_TCP,
                                    direction=Direction.OUT,
                                    tcp_dst=8008)
        actions = [
            parser.NXActionLearn(
                table_id=self._mac_rewrite_scratch,
                priority=flows.UE_FLOW_PRIORITY,
                cookie=rule_num,
                specs=[
                    parser.NXFlowSpecMatch(src=ether_types.ETH_TYPE_IP,
                                           dst=('eth_type_nxm', 0),
                                           n_bits=16),
                    parser.NXFlowSpecMatch(src=IPPROTO_TCP,
                                           dst=('ip_proto_nxm', 0),
                                           n_bits=8),
                    parser.NXFlowSpecMatch(src=int(
                        ipaddress.IPv4Address(self._bridge_ip)),
                                           dst=('ipv4_src_nxm', 0),
                                           n_bits=32),
                    parser.NXFlowSpecMatch(src=int(internal_ip),
                                           dst=('ipv4_dst_nxm', 0),
                                           n_bits=32),
                    parser.NXFlowSpecMatch(src=('tcp_src_nxm', 0),
                                           dst=('tcp_dst_nxm', 0),
                                           n_bits=16),
                    parser.NXFlowSpecMatch(src=self._redirect_port,
                                           dst=('tcp_src_nxm', 0),
                                           n_bits=16),
                    parser.NXFlowSpecLoad(src=('eth_src_nxm', 0),
                                          dst=('eth_dst_nxm', 0),
                                          n_bits=48),
                    parser.NXFlowSpecLoad(src=encode_imsi(imsi),
                                          dst=(IMSI_REG, 0),
                                          n_bits=64),
                    parser.NXFlowSpecLoad(src=('ipv4_src_nxm', 0),
                                          dst=('ipv4_dst_nxm', 0),
                                          n_bits=32),
                    parser.NXFlowSpecLoad(src=('ipv4_dst_nxm', 0),
                                          dst=('ipv4_src_nxm', 0),
                                          n_bits=32),
                    parser.NXFlowSpecLoad(src=('tcp_dst_nxm', 0),
                                          dst=('tcp_src_nxm', 0),
                                          n_bits=16),
                ]),
            parser.OFPActionSetField(ipv4_src=str(internal_ip)),
            parser.OFPActionSetField(ipv4_dst=self._bridge_ip),
            parser.OFPActionSetField(eth_dst=self._bridge_mac),
            parser.OFPActionSetField(tcp_dst=self._redirect_port),
        ]
        flows.add_output_flow(datapath,
                              self.main_tbl_num,
                              match_tcp_80,
                              actions,
                              priority=flows.UE_FLOW_PRIORITY,
                              cookie=rule_num,
                              output_port=OFPP_LOCAL)
        flows.add_output_flow(datapath,
                              self.main_tbl_num,
                              match_tcp_8008,
                              actions,
                              priority=flows.UE_FLOW_PRIORITY,
                              cookie=rule_num,
                              output_port=OFPP_LOCAL)
        flows.add_output_flow(datapath,
                              self.main_tbl_num,
                              match_tcp_8080,
                              actions,
                              priority=flows.UE_FLOW_PRIORITY,
                              cookie=rule_num,
                              output_port=OFPP_LOCAL)

        # Add flows for vlan traffic too (we need to pop vlan for flask server)
        # In ryu vlan_vid=(0x1000, 0x1000) matches all vlans
        match_tcp_80_vlan = MagmaMatch(imsi=encode_imsi(imsi),
                                       eth_type=ether_types.ETH_TYPE_IP,
                                       ip_proto=IPPROTO_TCP,
                                       direction=Direction.OUT,
                                       tcp_dst=80,
                                       vlan_vid=(0x1000, 0x1000))
        match_tcp_8008_vlan = MagmaMatch(imsi=encode_imsi(imsi),
                                         eth_type=ether_types.ETH_TYPE_IP,
                                         ip_proto=IPPROTO_TCP,
                                         direction=Direction.OUT,
                                         tcp_dst=8080,
                                         vlan_vid=(0x1000, 0x1000))
        match_tcp_8080_vlan = MagmaMatch(imsi=encode_imsi(imsi),
                                         eth_type=ether_types.ETH_TYPE_IP,
                                         ip_proto=IPPROTO_TCP,
                                         direction=Direction.OUT,
                                         tcp_dst=8008,
                                         vlan_vid=(0x1000, 0x1000))
        actions.append(parser.OFPActionPopVlan())
        flows.add_output_flow(datapath,
                              self.main_tbl_num,
                              match_tcp_80_vlan,
                              actions,
                              priority=flows.UE_FLOW_PRIORITY + 1,
                              cookie=rule_num,
                              output_port=OFPP_LOCAL)
        flows.add_output_flow(datapath,
                              self.main_tbl_num,
                              match_tcp_8008_vlan,
                              actions,
                              priority=flows.UE_FLOW_PRIORITY + 1,
                              cookie=rule_num,
                              output_port=OFPP_LOCAL)
        flows.add_output_flow(datapath,
                              self.main_tbl_num,
                              match_tcp_8080_vlan,
                              actions,
                              priority=flows.UE_FLOW_PRIORITY + 1,
                              cookie=rule_num,
                              output_port=OFPP_LOCAL)

        # TODO cleanup, make this a default rule in the ue_mac table
        ue_tbl = 0
        ue_next_tbl = 1
        # Allows traffic back from the flask server
        match = MagmaMatch(in_port=OFPP_LOCAL)
        actions = [
            parser.NXActionResubmitTable(table_id=self._mac_rewrite_scratch)
        ]
        flows.add_resubmit_next_service_flow(datapath,
                                             ue_tbl,
                                             match,
                                             actions=actions,
                                             priority=flows.DEFAULT_PRIORITY,
                                             resubmit_table=ue_next_tbl)
        match = MagmaMatch(imsi=encode_imsi(imsi),
                           eth_type=ether_types.ETH_TYPE_IP,
                           ip_proto=IPPROTO_TCP,
                           direction=Direction.IN,
                           in_port=OFPP_LOCAL)
        flows.add_resubmit_next_service_flow(datapath,
                                             self.main_tbl_num,
                                             match, [],
                                             priority=flows.DEFAULT_PRIORITY,
                                             cookie=rule_num,
                                             resubmit_table=self._egress_table)

        # Mac doesn't matter as we rewrite it anwyays
        mac_addr = '01:02:03:04:05:06'
        if self._arp_contoller or self._arpd_controller_fut.done():
            if not self._arp_contoller:
                self._arp_contoller = self._arpd_controller_fut.result()
            self._arp_contoller.set_incoming_arp_flows(datapath, internal_ip,
                                                       mac_addr)

        # Drop all other traffic that doesn't match
        match = MagmaMatch(imsi=encode_imsi(imsi))
        flows.add_drop_flow(datapath,
                            self.main_tbl_num,
                            match, [],
                            priority=flows.MINIMUM_PRIORITY + 1,
                            cookie=rule_num)

    def _install_server_flows(self, datapath, imsi, ip_addr, rule, rule_num,
                              rule_version, priority):
        """
        Install the redirect flows to redirect all HTTP traffic to the captive
        portal and to drop all of the rest.
        """
        self._install_scratch_table_flows(datapath, imsi, rule, rule_num,
                                          rule_version, priority)
        self._install_not_processed_flows(datapath, imsi, ip_addr, rule,
                                          rule_num, priority)

    def _install_url_bypass_flows(self,
                                  datapath,
                                  loop,
                                  imsi,
                                  rule,
                                  rule_num,
                                  rule_version,
                                  priority,
                                  ue_ip=None):
        """
        Resolve DNS queries to get the ip address of redirect url, this is done
        to allow traffic to safely pass through as we want subscribers to have
        full access to the url they are redirected to.

        First check cache for redirect url, if not in cache submit a DNS query
        """
        redirect_addr_host = urlsplit(rule.redirect.server_address).netloc
        cached_ips = self._dns_cache.get(redirect_addr_host)
        if cached_ips is not None:
            self.logger.debug(
                "DNS cache hit for {}, entry expires in {} sec".format(
                    redirect_addr_host,
                    self._dns_cache.ttl(redirect_addr_host)))
            self._install_ipv4_bypass_flows(datapath, imsi, rule, rule_num,
                                            rule_version, priority, cached_ips,
                                            ue_ip)
            return

        resolver = aiodns.DNSResolver(timeout=self.DNS_TIMEOUT_SECS, loop=loop)
        query = resolver.query(redirect_addr_host, 'A')

        def add_flows(dns_resolve_future):
            """
            Callback for when DNS query is resolved, adds the bypass flows
            """
            try:
                ips = [entry.host for entry in dns_resolve_future.result()]
                ttl = min(entry.ttl for entry in dns_resolve_future.result())
            except aiodns.error.DNSError as err:
                self.logger.error("Error: ip lookup for {}: {}".format(
                    redirect_addr_host, err))
                return
            self._dns_cache.get(redirect_addr_host, lambda: ips, max_age=ttl)
            self._install_ipv4_bypass_flows(datapath, imsi, rule, rule_num,
                                            rule_version, priority, ips, ue_ip)

        asyncio.ensure_future(query, loop=loop).add_done_callback(add_flows)

    def _install_ipv4_bypass_flows(self,
                                   datapath,
                                   imsi,
                                   rule,
                                   rule_num,
                                   rule_version,
                                   priority,
                                   ips,
                                   ue_ip=None):
        """
        Installs flows for traffic that is allowed to pass through for
        subscriber who has redirection enabled. Allow access to all passed ips.

        Allow UDP traffic(for DNS queries), traffic to/from redirection address
        """
        parser = datapath.ofproto_parser
        of_note = parser.NXActionNote(list(rule.id.encode()))
        actions = [
            of_note,
        ]
        actions += self._load_rule_actions(parser, rule_num, rule_version)

        matches = []
        uplink_ip_match = {}
        downlink_ip_match = {}
        if ue_ip != None:
            uplink_ip_match['ipv4_src'] = ue_ip
            downlink_ip_match['ipv4_dst'] = ue_ip
        for ip in ips:
            matches.append(
                MagmaMatch(eth_type=ether_types.ETH_TYPE_IP,
                           direction=Direction.OUT,
                           ipv4_dst=ip,
                           imsi=encode_imsi(imsi),
                           **uplink_ip_match))
            matches.append(
                MagmaMatch(eth_type=ether_types.ETH_TYPE_IP,
                           direction=Direction.IN,
                           ipv4_src=ip,
                           imsi=encode_imsi(imsi),
                           **downlink_ip_match))
        for match in matches:
            flows.add_resubmit_next_service_flow(
                datapath,
                self.main_tbl_num,
                match,
                actions,
                priority=priority + 1,
                cookie=rule_num,
                hard_timeout=rule.hard_timeout,
                resubmit_table=self.next_table)

    def _install_dns_flows(self, datapath, imsi, rule, rule_num, rule_version,
                           priority):
        """
        Installs flows that allow DNS queries to path through.
        """
        parser = datapath.ofproto_parser
        of_note = parser.NXActionNote(list(rule.id.encode()))
        actions = [
            of_note,
        ]
        actions += self._load_rule_actions(parser, rule_num, rule_version)
        matches = []
        # Install UDP flows for DNS
        matches.append(
            MagmaMatch(eth_type=ether_types.ETH_TYPE_IP,
                       ip_proto=IPPROTO_UDP,
                       udp_src=53,
                       direction=Direction.IN,
                       imsi=encode_imsi(imsi)))
        matches.append(
            MagmaMatch(eth_type=ether_types.ETH_TYPE_IP,
                       ip_proto=IPPROTO_UDP,
                       udp_dst=53,
                       direction=Direction.OUT,
                       imsi=encode_imsi(imsi)))
        # Install TCP flows for DNS
        matches.append(
            MagmaMatch(eth_type=ether_types.ETH_TYPE_IP,
                       ip_proto=IPPROTO_TCP,
                       tcp_src=53,
                       direction=Direction.IN,
                       imsi=encode_imsi(imsi)))
        matches.append(
            MagmaMatch(eth_type=ether_types.ETH_TYPE_IP,
                       ip_proto=IPPROTO_TCP,
                       tcp_dst=53,
                       direction=Direction.OUT,
                       imsi=encode_imsi(imsi)))
        for match in matches:
            flows.add_resubmit_next_service_flow(
                datapath,
                self.main_tbl_num,
                match,
                actions,
                priority=priority,
                cookie=rule_num,
                hard_timeout=rule.hard_timeout,
                resubmit_table=self.next_table)

    def _save_redirect_entry(self, ip_addr, redirect_info):
        """
        Saves the redirect entry in Redis.

        Throws:
            RedirectException: on error
        """
        try:
            # Verify if ip_addr is in the correct format, and also
            # normalize the variants of the address into a single format
            ip_str = str(ipaddress.ip_address(ip_addr))
        except ValueError as exp:
            raise RedirectException(exp)
        try:
            self._redirect_dict[ip_str] = redirect_info
        except RedisError as exp:
            raise RedirectException(exp)
        self.logger.info("Saved redirect rule for %s in Redis" % ip_str)

    def deactivate_flow_for_rule(self, datapath, imsi, rule_num):
        """
        Deactivate a specific rule using the flow cookie for a subscriber
        """
        cookie, mask = (rule_num, flows.OVS_COOKIE_MATCH_ALL)
        match = MagmaMatch(imsi=encode_imsi(imsi))
        flows.delete_flow(datapath,
                          self._scratch_tbl_num,
                          match,
                          cookie=cookie,
                          cookie_mask=mask)

    def deactivate_flows_for_subscriber(self, datapath, imsi):
        """
        Deactivate all rules for a subscriber
        """
        flows.delete_flow(datapath, self._scratch_tbl_num,
                          MagmaMatch(imsi=encode_imsi(imsi)))

    def _load_rule_actions(self, parser, rule_num, rule_version):
        return [
            parser.NXActionRegLoad2(dst='reg2', value=rule_num),
            parser.NXActionRegLoad2(dst=RULE_VERSION_REG, value=rule_version),
        ]
Esempio n. 2
0
class RedirectionManager:
    """
    RedirectionManager

    The redirection manager handles subscribers who have redirection enabled,
    it adds the flows into ovs for redirecting user to the redirection server
    """
    DNS_TIMEOUT_SECS = 15

    REDIRECT_NOT_PROCESSED = REG_ZERO_VAL
    REDIRECT_PROCESSED = 0x1

    RedirectRequest = namedtuple(
        'RedirectRequest',
        ['imsi', 'ip_addr', 'rule', 'rule_num', 'priority'],
    )

    def __init__(self, bridge_ip, logger, main_tbl_num, next_table,
                 scratch_table_num, session_rule_version_mapper):
        self._bridge_ip = bridge_ip
        self.logger = logger
        self.main_tbl_num = main_tbl_num
        self.next_table = next_table
        self._scratch_tbl_num = scratch_table_num
        self._redirect_dict = RedirectDict()
        self._dns_cache = Memoizer({})
        self._redirect_port = get_service_config_value(
            'redirectd', 'http_port', 8080)
        self._session_rule_version_mapper = session_rule_version_mapper

    def handle_redirection(self, datapath, loop, redirect_request):
        """
        Depending on redirection server address type install redirection rules
        """
        imsi = redirect_request.imsi
        ip_addr = redirect_request.ip_addr
        rule = redirect_request.rule
        rule_num = redirect_request.rule_num
        priority = redirect_request.priority

        # TODO IMPORTANT check that redirectd service is running, as its a
        # dynamic service its not on by default. Will save you some sanity :)

        # TODO figure out what to do with SIP_URI
        if rule.redirect.address_type == rule.redirect.SIP_URI:
            raise RedirectException("SIP_URIs redirection isn't setup")
        if rule.redirect.address_type == rule.redirect.IPv6:
            raise RedirectException("No ipv6 support, so no ipv6 redirect")

        self._save_redirect_entry(ip_addr, rule.redirect)
        self._install_redirect_flows(datapath, loop, imsi, rule, rule_num,
                                     priority)
        return

    def _install_redirect_flows(self, datapath, loop, imsi, rule, rule_num,
                                priority):
        """
        Add flows to forward traffic to the redirection server.

        1) Intercept tcp traffic to the web to the redirection server, which
            completes the tcp handshake. This is done by adding an OVS flow
            with a learn action (flow catches inbound tcp packets, while learn
            action creates another flow that sends packets back from server)

        2) Add flows to allow UDP traffic so DNS queries can go through.
            Finally add flows with a higher priority that allow traffic to and
            from the address provided in redirect rule.
        """

        if rule.redirect.address_type == rule.redirect.URL:
            self._install_url_bypass_flows(datapath, loop, imsi, rule,
                                           rule_num, priority)
        elif rule.redirect.address_type == rule.redirect.IPv4:
            self._install_ipv4_bypass_flows(datapath, imsi, rule,
                                            rule_num, priority,
                                            [rule.redirect.server_address])

        self._install_dns_flows(datapath, imsi, rule, rule_num, priority)
        self._install_server_flows(datapath, imsi, rule, rule_num, priority)

    def _install_scratch_table_flows(self, datapath, imsi, rule, rule_num,
                                     priority):
        """
        The flow action for subscribers that need to be redirected does 2 things
            * Forward requests from subscriber to the internal http server
            * Instantiate a flow that matches response packets from the server
              and sends them back to subscriber

        Match: incoming tcp traffic with port 80, direction out

        Action:
            1) Set reg2 to rule_num
            2) Set ip dst to server ip
            3) Output to table 20

            4) Apply LearnAction:

            LearnAction(adds new flow for every pkt flow that hits this rule)
                1) Match ip packets
                2) Match tcp protocol
                3) Match packets from LOCAL port
                4) Match ip src = server ip
                5) Match ip dst = current flow ip src
                6) Match tcp src = current flow tcp dst
                7) Match tcp dst = current flow tcp src

                8) Load ip src = current flow ip dst

                9) Output through gtp0
        """
        parser = datapath.ofproto_parser
        match_http = MagmaMatch(
            eth_type=ether_types.ETH_TYPE_IP, ip_proto=IPPROTO_TCP,
            tcp_dst=80, imsi=encode_imsi(imsi), direction=Direction.OUT)
        of_note = parser.NXActionNote(list(rule.id.encode()))

        actions = [
            parser.NXActionLearn(
                table_id=self.main_tbl_num,
                priority=priority,
                cookie=rule_num,
                specs=[
                    parser.NXFlowSpecMatch(
                        src=ether_types.ETH_TYPE_IP, dst=('eth_type_nxm', 0),
                        n_bits=16
                    ),
                    parser.NXFlowSpecMatch(
                        src=IPPROTO_TCP, dst=('ip_proto_nxm', 0), n_bits=8
                    ),
                    parser.NXFlowSpecMatch(
                        src=Direction.IN,
                        dst=(DIRECTION_REG, 0),
                        n_bits=32
                    ),
                    parser.NXFlowSpecMatch(
                        src=int(ipaddress.IPv4Address(self._bridge_ip)),
                        dst=('ipv4_src_nxm', 0),
                        n_bits=32
                    ),
                    parser.NXFlowSpecMatch(
                        src=('ipv4_src_nxm', 0),
                        dst=('ipv4_dst_nxm', 0),
                        n_bits=32
                    ),
                    parser.NXFlowSpecMatch(
                        src=('tcp_src_nxm', 0),
                        dst=('tcp_dst_nxm', 0),
                        n_bits=16
                    ),
                    parser.NXFlowSpecMatch(
                        src=self._redirect_port,
                        dst=('tcp_src_nxm', 0),
                        n_bits=16
                    ),
                    parser.NXFlowSpecMatch(
                        src=encode_imsi(imsi),
                        dst=(IMSI_REG, 0),
                        n_bits=64
                    ),
                    parser.NXFlowSpecLoad(
                        src=('ipv4_dst_nxm', 0),
                        dst=('ipv4_src_nxm', 0),
                        n_bits=32
                    ),
                    parser.NXFlowSpecLoad(
                        src=80,
                        dst=('tcp_src_nxm', 0),
                        n_bits=16
                    ),
                    # Learn doesn't support resubmit to table, so send directly
                    parser.NXFlowSpecOutput(
                        src=('in_port', 0), dst="", n_bits=16
                    ),
                ]
            ),
            parser.NXActionRegLoad2(dst=SCRATCH_REGS[0],
                                    value=self.REDIRECT_PROCESSED),
            parser.OFPActionSetField(ipv4_dst=self._bridge_ip),
            parser.OFPActionSetField(tcp_dst=self._redirect_port),
            of_note,
        ]
        actions += self._load_rule_actions(parser, rule_num, imsi, rule.id)

        flows.add_resubmit_current_service_flow(
            datapath, self._scratch_tbl_num, match_http, actions,
            priority=priority, cookie=rule_num, hard_timeout=rule.hard_timeout,
            resubmit_table=self.main_tbl_num)

        match = MagmaMatch(imsi=encode_imsi(imsi))
        action = []
        flows.add_drop_flow(datapath, self._scratch_tbl_num, match, action,
                            priority=flows.MINIMUM_PRIORITY + 1,
                            cookie=rule_num)

    def _install_not_processed_flows(self, datapath, imsi, rule, rule_num,
                                     priority):
        """
        Redirect all traffic to the scratch table to only allow redirected
        http traffic to go through, the rest will be dropped. reg0 is used as
        a boolean to know whether the drop rule was processed.
        """
        parser = datapath.ofproto_parser
        of_note = parser.NXActionNote(list(rule.id.encode()))

        match = MagmaMatch(imsi=encode_imsi(imsi),
                           direction=Direction.OUT,
                           reg0=self.REDIRECT_NOT_PROCESSED,
                           eth_type=ether_types.ETH_TYPE_IP)
        action = [of_note]
        flows.add_resubmit_current_service_flow(
            datapath, self.main_tbl_num, match, action, priority=priority,
            cookie=rule_num, hard_timeout=rule.hard_timeout,
            resubmit_table=self._scratch_tbl_num)

        match = MagmaMatch(imsi=encode_imsi(imsi),
                           direction=Direction.OUT,
                           reg0=self.REDIRECT_PROCESSED)
        action = [of_note]
        flows.add_resubmit_next_service_flow(
            datapath, self.main_tbl_num, match, action, priority=priority,
            cookie=rule_num, hard_timeout=rule.hard_timeout,
            resubmit_table=self.next_table)

    def _install_server_flows(self, datapath, imsi, rule, rule_num, priority):
        """
        Install the redirect flows to redirect all HTTP traffic to the captive
        portal and to drop all of the rest.
        """
        self._install_scratch_table_flows(datapath, imsi, rule, rule_num,
                                          priority)
        self._install_not_processed_flows(datapath, imsi, rule, rule_num,
                                          priority)

    def _install_url_bypass_flows(self, datapath, loop, imsi, rule, rule_num,
                                  priority):
        """
        Resolve DNS queries to get the ip address of redirect url, this is done
        to allow traffic to safely pass through as we want subscribers to have
        full access to the url they are redirected to.

        First check cache for redirect url, if not in cache submit a DNS query
        """
        redirect_addr_host = urlsplit(rule.redirect.server_address).netloc
        cached_ips = self._dns_cache.get(redirect_addr_host)
        if cached_ips is not None:
            self.logger.debug(
                "DNS cache hit for {}, entry expires in {} sec".format(
                    redirect_addr_host, self._dns_cache.ttl(redirect_addr_host)
                )
            )
            self._install_ipv4_bypass_flows(datapath, imsi, rule, rule_num,
                                            priority, cached_ips)
            return

        resolver = aiodns.DNSResolver(timeout=self.DNS_TIMEOUT_SECS, loop=loop)
        query = resolver.query(redirect_addr_host, 'A')

        def add_flows(dns_resolve_future):
            """
            Callback for when DNS query is resolved, adds the bypass flows
            """
            try:
                ips = [entry.host for entry in dns_resolve_future.result()]
                ttl = min(entry.ttl for entry in dns_resolve_future.result())
            except aiodns.error.DNSError as err:
                self.logger.error("Error: ip lookup for {}: {}".format(
                                  redirect_addr_host, err))
                return
            self._dns_cache.get(redirect_addr_host, lambda: ips, max_age=ttl)
            self._install_ipv4_bypass_flows(datapath, imsi, rule, rule_num,
                                            priority, ips)

        asyncio.ensure_future(query, loop=loop).add_done_callback(add_flows)

    def _install_ipv4_bypass_flows(self, datapath, imsi, rule, rule_num,
                                   priority, ips):
        """
        Installs flows for traffic that is allowed to pass through for
        subscriber who has redirection enabled. Allow access to all passed ips.

        Allow UDP traffic(for DNS queries), traffic to/from redirection address
        """
        parser = datapath.ofproto_parser
        of_note = parser.NXActionNote(list(rule.id.encode()))
        actions = [
            of_note,
        ]
        actions += self._load_rule_actions(parser, rule_num, imsi, rule.id)

        matches = []
        for ip in ips:
            matches.append(MagmaMatch(
                eth_type=ether_types.ETH_TYPE_IP, direction=Direction.OUT,
                ipv4_dst=ip, imsi=encode_imsi(imsi)
            ))
            matches.append(MagmaMatch(
                eth_type=ether_types.ETH_TYPE_IP, direction=Direction.IN,
                ipv4_src=ip, imsi=encode_imsi(imsi)
            ))
        for match in matches:
            flows.add_resubmit_next_service_flow(
                datapath, self.main_tbl_num, match, actions,
                priority=priority + 1, cookie=rule_num,
                hard_timeout=rule.hard_timeout, resubmit_table=self.next_table)

    def _install_dns_flows(self, datapath, imsi, rule, rule_num, priority):
        """
        Installs flows that allow DNS queries to path through.
        """
        parser = datapath.ofproto_parser
        of_note = parser.NXActionNote(list(rule.id.encode()))
        actions = [
            of_note,
        ]
        actions += self._load_rule_actions(parser, rule_num, imsi, rule.id)
        matches = []
        # Install UDP flows for DNS
        matches.append(MagmaMatch(eth_type=ether_types.ETH_TYPE_IP,
                                  ip_proto=IPPROTO_UDP,
                                  udp_src=53,
                                  direction=Direction.IN,
                                  imsi=encode_imsi(imsi)))
        matches.append(MagmaMatch(eth_type=ether_types.ETH_TYPE_IP,
                                  ip_proto=IPPROTO_UDP,
                                  udp_dst=53,
                                  direction=Direction.OUT,
                                  imsi=encode_imsi(imsi)))
        # Install TCP flows for DNS
        matches.append(MagmaMatch(eth_type=ether_types.ETH_TYPE_IP,
                                  ip_proto=IPPROTO_TCP,
                                  tcp_src=53,
                                  direction=Direction.IN,
                                  imsi=encode_imsi(imsi)))
        matches.append(MagmaMatch(eth_type=ether_types.ETH_TYPE_IP,
                                  ip_proto=IPPROTO_TCP,
                                  tcp_dst=53,
                                  direction=Direction.OUT,
                                  imsi=encode_imsi(imsi)))
        for match in matches:
            flows.add_resubmit_next_service_flow(
                datapath, self.main_tbl_num, match, actions, priority=priority,
                cookie=rule_num, hard_timeout=rule.hard_timeout,
                resubmit_table=self.next_table)

    def _save_redirect_entry(self, ip_addr, redirect_info):
        """
        Saves the redirect entry in Redis.

        Throws:
            RedirectException: on error
        """
        try:
            # Verify if ip_addr is in the correct format, and also
            # normalize the variants of the address into a single format
            ip_str = str(ipaddress.ip_address(ip_addr))
        except ValueError as exp:
            raise RedirectException(exp)
        try:
            self._redirect_dict[ip_str] = redirect_info
        except RedisError as exp:
            raise RedirectException(exp)
        self.logger.info("Saved redirect rule for %s in Redis" % ip_str)

    def deactivate_flow_for_rule(self, datapath, imsi, rule_num):
        """
        Deactivate a specific rule using the flow cookie for a subscriber
        """
        cookie, mask = (rule_num, flows.OVS_COOKIE_MATCH_ALL)
        match = MagmaMatch(imsi=encode_imsi(imsi))
        flows.delete_flow(datapath, self._scratch_tbl_num, match,
                          cookie=cookie, cookie_mask=mask)

    def deactivate_flows_for_subscriber(self, datapath, imsi):
        """
        Deactivate all rules for a subscriber
        """
        flows.delete_flow(datapath, self._scratch_tbl_num,
                          MagmaMatch(imsi=encode_imsi(imsi)))

    def _load_rule_actions(self, parser, rule_num, imsi, rule_id):
        version = self._session_rule_version_mapper.get_version(imsi, rule_id)
        return [
            parser.NXActionRegLoad2(dst='reg2', value=rule_num),
            parser.NXActionRegLoad2(dst=RULE_VERSION_REG, value=version),
        ]