diff --git a/docker_container_version/Dockerfile b/docker_container_version/Dockerfile new file mode 100644 index 0000000..5e78f75 --- /dev/null +++ b/docker_container_version/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.10-slim-bullseye + +COPY . /app +COPY templates /app/templates +WORKDIR /app + +RUN pip install flask ipaddress requests + +CMD ["python", "main.py"] + diff --git a/docker_container_version/main.py b/docker_container_version/main.py new file mode 100644 index 0000000..896ce17 --- /dev/null +++ b/docker_container_version/main.py @@ -0,0 +1,158 @@ +from flask import Flask, request, render_template +import ipaddress +import re +import requests +import json +import os + +template_dir = template_dir = os.path.join(os.path.dirname(__file__), 'templates') + +app = Flask(__name__, template_folder=template_dir) +def get_asn_from_as(asn): + # Remove the "AS" from the beginning of the ASN + if asn.startswith("AS") or asn.startswith("as"): + asn = asn[2:] + return asn +def get_more_specifics(data): + """ + :param data: json data from RIPEstat API + :return: list of more specific prefixes and their origins + + """ + prefixes = [] + origins = [] + for prefix_origin in data["data"]["more_specifics"]: + prefixes.append(prefix_origin['prefix']) + origins.append(prefix_origin['origin']) + return [prefixes, origins] +def get_prefix_roa_status(prefix, origin): + """ given a prefix, determine if it is covered by an existing ROA""" + url = f"https://stat.ripe.net/data/rpki-validation/data.json?resource={origin}&prefix={prefix}" + response = requests.get(url) + data = json.loads(response.text) + data_str = json.dumps(data, indent=4) + # print(data_str) + try: + validation_status = data["data"]["status"] + except KeyError: + return None + return(validation_status) +def get_prefix_info(prefix): + """ given a prefix, return the more specific prefixes and their origins as seen by RIPEstat""" + url = "https://stat.ripe.net/data/routing-status/data.json?resource=" + prefix + try: + response = requests.get(url) + except requests.exceptions.RequestException: + return None + data = json.loads(response.text) + try: + if "prefix" in data["data"]["last_seen"]: + seen_origin = data["data"]["last_seen"]["origin"] + more_specifics, more_specific_origins = get_more_specifics(data) + return [prefix, seen_origin, more_specifics, more_specific_origins] + except KeyError: + return None +def return_rov_status(roa_prefix, roa_maxlen, roa_asn, prefix, origin_asn): + """ given a prefix and an origin ASN, as well as a proposed ROA (prefix, maxlen, asn), + determine if the ROA covers the prefix and origin ASN + :param roa_prefix: prefix from the ROA + :param roa_maxlen: maximum length of the prefix from the ROA + :param roa_asn: ASN from the ROA + :param prefix: prefix from the routing table + :param origin_asn: origin ASN from the routing table + :return: ROV status of the prefix""" + + roa_ip_prefix = ipaddress.ip_network(roa_prefix) + ip_prefix = ipaddress.ip_network(prefix) + if not ip_prefix.subnet_of(roa_ip_prefix): + return "error: prefix not covered by ROA" + if ip_prefix.subnet_of(roa_ip_prefix) and roa_maxlen >= ip_prefix.prefixlen and roa_asn == origin_asn: + return "valid" + else: + return "invalid" + +def is_valid_prefix(prefix): + # Check if the prefix is a valid IPv4 or IPv6 prefix + try: + # Try to parse the prefix as an IPv4 or IPv6 prefix + ip_network = ipaddress.ip_network(prefix) + return True + except ValueError: + return False + +def is_valid_asn(asn): + # Check if the ASN is a string starting with "AS", followed by numbers + if re.match(r'^[Aa][Ss]\d+$', asn): + return True + # Check if the ASN is just numbers + elif asn.isdigit(): + return True + else: + return False + +def is_valid_prefix_maxlength(ip_prefix, prefix_maxlength): + try: + # Try to parse the prefix maxlength as an integer + prefix_maxlength = int(prefix_maxlength) + except ValueError: + return False + + # Check if the prefix maxlength is in the valid range based on the type of ip_prefix + if ipaddress.ip_network(ip_prefix).version == 4: + return 1 <= prefix_maxlength <= 32 and prefix_maxlength >= ipaddress.ip_network(ip_prefix).prefixlen + elif ipaddress.ip_network(ip_prefix).version == 6: + return 1 <= prefix_maxlength <= 128 and prefix_maxlength >= ipaddress.ip_network(ip_prefix).prefixlen + + +def check_list_of_prefixes_against_ROA(origin, prefixes, origins, roa_prefix, roa_maxlen, roa_asn): + + messages = [] + existing_roa_status = get_prefix_roa_status(roa_prefix, origin) + messages.append([roa_prefix, return_rov_status(roa_prefix, roa_maxlen, roa_asn, roa_prefix, origin), origin, + existing_roa_status]) + prefix_origin = zip(prefixes, origins) + for prefix, origin in prefix_origin: + existing_roa_status = get_prefix_roa_status(prefix, origin) + messages.append([prefix, return_rov_status(roa_prefix, roa_maxlen, roa_asn, prefix, origin), origin, + existing_roa_status]) + return messages + +@app.route('/', methods=['GET', 'POST']) +def index(): + if request.method == 'POST': + # Get the user input + roa_ip_prefix = request.form['ip_prefix'] + roa_ip_prefix = roa_ip_prefix.strip() + # Validate the IP prefix + if not is_valid_prefix(roa_ip_prefix): + return 'Invalid IP prefix' + + origin_asn = request.form['origin_asn'] + origin_asn = origin_asn.strip() + # Validate the origin ASN + if not is_valid_asn(origin_asn): + return 'Invalid origin ASN' + roa_prefix_maxlength = request.form['prefix_maxlength'] + roa_prefix_maxlength = roa_prefix_maxlength.strip() + # Validate the prefix maxlength + if not is_valid_prefix_maxlength(roa_ip_prefix, roa_prefix_maxlength): + return 'Invalid prefix maxlength' + + roa_ip_prefix = request.form['ip_prefix'] + roa_prefix_maxlength = int(request.form['prefix_maxlength']) + roa_origin_asn = request.form['origin_asn'] + prefix_info = get_prefix_info(roa_ip_prefix) + if prefix_info is None: + return "Prefix not found or problems with RIPEstat API" + prefix, seen_origin, more_specifics, more_specific_origins = prefix_info + items = check_list_of_prefixes_against_ROA(seen_origin, more_specifics, more_specific_origins, roa_ip_prefix, + roa_prefix_maxlength, roa_origin_asn) + roa_info = [roa_ip_prefix, roa_prefix_maxlength, roa_origin_asn] + + return render_template('render.html', items=items, roa_info=roa_info) + else: + return render_template('index.html') + +if __name__ == '__main__': + app.run(port=8000, host='0.0.0.0') + diff --git a/docker_container_version/templates/index.html b/docker_container_version/templates/index.html new file mode 100644 index 0000000..3ae2dc5 --- /dev/null +++ b/docker_container_version/templates/index.html @@ -0,0 +1,56 @@ + + +
+This app queries the stat.ripe.net to determine if a RPKI-ROA created with the following information would likely agree (i.e., not evaluate as invalid) for routes currently seen in the Internet
+ + + diff --git a/docker_container_version/templates/render.html b/docker_container_version/templates/render.html new file mode 100644 index 0000000..8a353b2 --- /dev/null +++ b/docker_container_version/templates/render.html @@ -0,0 +1,51 @@ + + + + +Proposed ROA Prefix = {{ roa_info[0] }}
+Max Length = {{ roa_info[1]}}
+and origin ASN = {{roa_info[2] }}
+| The first row is the prefix specified in the proposed ROA. + Subsequent lines list more specifics that are seen in the global routing table. | +If you create the proposed ROA, this is how it would be evaluated for each prefix. | +For this prefix, this is the origin ASN as seen in the global Internet. | +This is the status of this prefix due to an existing ROA. | +||||
|---|---|---|---|---|---|---|---|
| {{ item[0] }} | + + {% if item[1] == "invalid" %} +{{ item[1] }} | + {% elif item[1] =="valid" %} +{{ item[1] }} | + {% else %} +{{ item[1] }} | + {% endif %} +{{ item[2] }} | + {% if item[3] == "valid" %} +{{ item[3] }} | + {% elif item[3] == "invalid" %} +{{ item[3] }} | + {% else %} +{{ item[3] }} | + {% endif %} + +