diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/check_roa.iml b/.idea/check_roa.iml new file mode 100644 index 0000000..74d515a --- /dev/null +++ b/.idea/check_roa.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..cad27e3 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..8ace5c9 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..6a3e608 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..d712186 --- /dev/null +++ b/main.py @@ -0,0 +1,174 @@ +from flask import Flask, request, render_template +import ipaddress +import re +import requests +import json +import datetime + +# given a time string, return the hours between now and that time + +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_time_delta(time_string): + # get the current time + current_time = datetime.datetime.now() + # get the time of the post + post_time = datetime.datetime.strptime(time_string, "%Y-%m-%dT%H:%M:%S") + # get the difference between the two times + delta = current_time - post_time + # return the number of hours between the two times + if delta.total_seconds() < 3600: + return "less than an hour ago" + return (f"{int(delta.total_seconds() / 3600)} hours ago") + +# using RIPEstat API, given an AS number, return the set of prefixes announced by that AS + +def get_less_specifics(data): + """ + :param data: json data from RIPEstat API + :return: list of less specific prefixes and their origins + + """ + prefixes = [] + origins = [] + for prefix_origin in data["data"]["less_specifics"]: + prefixes.append(prefix_origin['prefix']) + origins.append(prefix_origin['origin']) + return [prefixes, origins] + +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_info(prefix): + """ given a prefix, return the prefix information from RIPEstat API + :param prefix: prefix to query + :return: json data from RIPEstat API""" + url = "https://stat.ripe.net/data/routing-status/data.json?resource=" + prefix + response = requests.get(url) + print(response.text) + data = json.loads(response.text) + if data["messages"] != []: + if "error" in data["messages"][0]: + return_type = -1 + error_message = data["messages"][0][1] + return [return_type, error_message] + + if "prefix" in data["data"]["last_seen"]: + return_type = 1 + prefix_last_seen = data["data"]["last_seen"]["prefix"] + last_seen_date = data["data"]["last_seen"]["time"] + last_seen_since_hours = get_time_delta(last_seen_date) + origin_asn = data["data"]["last_seen"]["origin"] + less_specifics, less_specific_origins = get_less_specifics(data) + more_specifics, more_specific_origins = get_more_specifics(data) + return [return_type, prefix_last_seen, last_seen_since_hours, origin_asn, less_specifics, less_specific_origins, more_specifics, more_specific_origins] + + if "prefix" in data["data"]["less_specifics"][0]: + return_type = 2 + less_specifics, less_specific_origins = get_less_specifics(data) + more_specifics, more_specific_origins = get_more_specifics(data) + return [return_type, less_specifics, less_specific_origins, more_specifics, more_specific_origins] + + +def return_rov_status(roa_prefix, roa_maxlen, roa_asn, prefix, origin_asn): + """ given a prefix and an origin ASN, determine if the prefix is ROV compliant + :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 "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" +app = Flask(__name__) + +def is_valid_prefix(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 + elif ipaddress.ip_network(ip_prefix).version == 6: + return 1 <= prefix_maxlength <= 128 + +@app.route('/', methods=['GET', 'POST']) +def index(): + if request.method == 'POST': + # Get the user input + ip_prefix = request.form['ip_prefix'] + + # Validate the IP prefix + if not is_valid_prefix(ip_prefix): + return 'Invalid IP prefix' + + prefix_maxlength = request.form['prefix_maxlength'] + origin_asn = request.form['origin_asn'] + + # Validate the origin ASN + if not is_valid_asn(origin_asn): + return 'Invalid origin ASN' + + # Validate the prefix maxlength + if not is_valid_prefix_maxlength(ip_prefix, prefix_maxlength): + return 'Invalid prefix maxlength' + + # Process the user input (e.g. validate the input, store it in a database, etc.) + + ip_prefix = request.form['ip_prefix'] + prefix_maxlength = request.form['prefix_maxlength'] + origin_asn = request.form['origin_asn'] + + return get_prefix_info(ip_prefix) + + else: + return render_template('index.html') + + + +if __name__ == '__main__': + app.run() + + #print(get_prefix_info("199.109.0.0/21")) diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..3ae2dc5 --- /dev/null +++ b/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

+
+ + + + + + + +
+ +