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 @@ + + + + IP Prefix Form + + + +

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 @@ + + + + + Title + + +
+

Proposed ROA Prefix = {{ roa_info[0] }}

+

Max Length = {{ roa_info[1]}}

+

and origin ASN = {{roa_info[2] }}

+
+ + + + + + + + + + + + {% for item in items %} + + + + {% if item[1] == "invalid" %} + + {% elif item[1] =="valid" %} + + {% else %} + + {% endif %} + + {% if item[3] == "valid" %} + + {% elif item[3] == "invalid" %} + + {% else %} + + {% endif %} + + + {% endfor %} + +
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] }}{{ item[1] }}{{ item[1] }}{{ item[1] }}{{ item[2] }}{{ item[3] }}{{ item[3] }}{{ item[3] }}
+ + + \ No newline at end of file diff --git a/main.py b/main.py index c94e014..a9f3c82 100644 --- a/main.py +++ b/main.py @@ -122,18 +122,21 @@ 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' + return f"\"{roa_ip_prefix}\" is an 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' + return f"\"{origin_asn}\" is an 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' + return f"\"{roa_prefix_maxlength}\" is an Invalid prefix maxlength" roa_ip_prefix = request.form['ip_prefix'] roa_prefix_maxlength = int(request.form['prefix_maxlength'])