Init commit for standalone enodebd

Change-Id: I88eeef5135dd7ba8551ddd9fb6a0695f5325337b
diff --git a/enodebd_iptables_rules.py b/enodebd_iptables_rules.py
new file mode 100644
index 0000000..39ffc02
--- /dev/null
+++ b/enodebd_iptables_rules.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python3
+
+"""
+Copyright 2020 The Magma Authors.
+
+This source code is licensed under the BSD-style license found in the
+LICENSE file in the root directory of this source tree.
+
+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.
+"""
+
+
+import asyncio
+import re
+import shlex
+import subprocess
+from typing import List
+
+from common.misc_utils import (
+    IpPreference,
+    get_if_ip_with_netmask,
+    get_ip_from_if,
+)
+from configuration.service_configs import load_service_config
+from logger import EnodebdLogger as logger
+
+IPTABLES_RULE_FMT = """sudo iptables -t nat
+    -{add} PREROUTING
+    -d {public_ip}
+    -p tcp
+    --dport {port}
+    -j DNAT --to-destination {private_ip}"""
+
+EXPECTED_IP4 = ('192.168.60.142', '10.0.2.1')
+EXPECTED_MASK = '255.255.255.0'
+
+
+def get_iptables_rule(port, enodebd_public_ip, private_ip, add=True):
+    return IPTABLES_RULE_FMT.format(
+        add='A' if add else 'D',
+        public_ip=enodebd_public_ip,
+        port=port,
+        private_ip=private_ip,
+    )
+
+
+def does_iface_config_match_expected(ip: str, netmask: str) -> bool:
+    return ip in EXPECTED_IP4 and netmask == EXPECTED_MASK
+
+
+def _get_prerouting_rules(output: str) -> List[str]:
+    prerouting_rules = output.split('\n\n')[0]
+    prerouting_rules = prerouting_rules.split('\n')
+    # Skipping the first two lines since it contains only column names
+    prerouting_rules = prerouting_rules[2:]
+    return prerouting_rules
+
+
+async def check_and_apply_iptables_rules(
+    port: str,
+    enodebd_public_ip: str,
+    enodebd_ip: str,
+) -> None:
+    command = 'sudo iptables -t nat -L'
+    output = subprocess.run(command, shell=True, stdout=subprocess.PIPE, check=True)
+    command_output = output.stdout.decode('utf-8').strip()
+    prerouting_rules = _get_prerouting_rules(command_output)
+    if not prerouting_rules:
+        logger.info('Configuring Iptables rule')
+        await run(
+            get_iptables_rule(
+                port,
+                enodebd_public_ip,
+                enodebd_ip,
+                add=True,
+            ),
+        )
+    else:
+        # Checks each rule in PREROUTING Chain
+        check_rules(prerouting_rules, port, enodebd_public_ip, enodebd_ip)
+
+
+def check_rules(
+    prerouting_rules: List[str],
+    port: str,
+    enodebd_public_ip: str,
+    private_ip: str,
+) -> None:
+    unexpected_rules = []
+    pattern = r'DNAT\s+tcp\s+--\s+anywhere\s+{pub_ip}\s+tcp\s+dpt:{dport} to:{ip}'.format(
+                pub_ip=enodebd_public_ip,
+                dport=port,
+                ip=private_ip,
+    )
+    for rule in prerouting_rules:
+        match = re.search(pattern, rule)
+        if not match:
+            unexpected_rules.append(rule)
+    if unexpected_rules:
+        logger.warning('The following Prerouting rule(s) are unexpected')
+        for rule in unexpected_rules:
+            logger.warning(rule)
+
+
+async def run(cmd):
+    """Fork shell and run command NOTE: Popen is non-blocking"""
+    cmd = shlex.split(cmd)
+    proc = await asyncio.create_subprocess_shell(" ".join(cmd))
+    await proc.communicate()
+    if proc.returncode != 0:
+        # This can happen because the NAT prerouting rule didn't exist
+        logger.error(
+            'Possible error running async subprocess: %s exited with '
+            'return code [%d].', cmd, proc.returncode,
+        )
+    return proc.returncode
+
+
+async def set_enodebd_iptables_rule():
+    """
+    Remove & Set iptable rules for exposing public IP
+    for enobeb instead of private IP..
+    """
+    # Remove & Set iptable rules for exposing public ip
+    # for enobeb instead of private
+    cfg = load_service_config('enodebd')
+    port, interface = cfg['tr069']['port'], cfg['tr069']['interface']
+    enodebd_public_ip = cfg['tr069']['public_ip']
+    # IPv4 only as iptables only works for IPv4. TODO: Investigate ip6tables?
+    enodebd_ip = get_ip_from_if(interface, preference=IpPreference.IPV4_ONLY)
+    # Incoming data from 192.88.99.142 -> enodebd address (eg 192.168.60.142)
+    enodebd_netmask = get_if_ip_with_netmask(
+        interface,
+        preference=IpPreference.IPV4_ONLY,
+    )[1]
+    verify_config = does_iface_config_match_expected(
+        enodebd_ip,
+        enodebd_netmask,
+    )
+    if not verify_config:
+        logger.warning(
+            'The IP address of the %s interface is %s. The '
+            'expected IP addresses are %s',
+            interface, enodebd_ip, str(EXPECTED_IP4),
+        )
+    await check_and_apply_iptables_rules(
+        port,
+        enodebd_public_ip,
+        enodebd_ip,
+    )
+
+
+if __name__ == '__main__':
+    set_enodebd_iptables_rule()