IRR, RPSL, ARIN, RADb, and API, oh my!
Our organization has used RADb for publishing IRR information for the last 5 or 6 years. We chose RADb because it is or is one of the largest routing databases, has the most features, and is generally easy to use.
Over the time I have be following ARINs progress in the IRR world and watched some API announcements flow through my email inbox. I had some downtime this week and decided to give it a try!
ARIN has some documentation that they publish but I haven’t found much more information beyond this so I figured I would share what I found here.
1st – You need an ARIN account (maintainer), ORG id, and prefixes that your ORG has control over in ARIN. The API throws ambiguous errors if you start trying to publish test information.
2nd – Objects which you HAVE NOT already created entries for either through the web interface or email. This will cause errors.
3rd – You need to generate and API key for your ARIN maintainer.
4th – A basic understanding of RPSL to begin with.
I decided the easiest ways to do this would be to make a text document for each object, have python read that document then push the information to ARIN via the requests module. Here is what I started with. My API key, our ORG ID and some empty lists that would eventually contains the names of the files for each object.
import requests
import pprint
# Disable SSL Warnings
requests.urllib3.disable_warnings()
api_key = 'API-KEYKEYKEYKEYKEY'
org_id = 'ORDID-123'
aut_nums = []
routes = []
routes6 = []
as_sets = []
route_sets = []
Our autonomous system IRR object looks like the below text. I then saved this file as arin-irr-aut-num-AS395176.txt
aut-num: AS395176
as-name: SJN-AS395176
descr: SJN DATA CENTER DBA ENCORE TECHNOLOGIES
import: from AS10796 action pref = 100; accept ANY
import: from AS6181 action pref = 100; accept ANY
import: from AS3356 action pref = 100; accept ANY
import: from AS30152 action pref = 120; accept AS30152
import: from AS394927 action pref = 120; accept AS394927
mp-import: afi ipv6.unicast from AS10796 action pref = 100; accept ANY
mp-import: afi ipv6.unicast from AS6181 action pref = 100; accept ANY
mp-import: afi ipv6.unicast from AS3356 action pref = 100; accept ANY
mp-import: afi ipv6.unicast from AS30152 action pref = 120; accept AS30152
export: to AS3356 announce AS395176 AS-Encore-Technologies AS-Encore-Technologies-Customers
export: to AS6181 announce AS395176 AS-Encore-Technologies AS-Encore-Technologies-Customers
export: to AS19796 announce AS395176 AS-Encore-Technologies AS-Encore-Technologies-Customers
export: to AS30152 announce {0.0.0.0/0}
export: to AS394927 announce {0.0.0.0/0}
mp-export: afi ipv6.unicast to AS30152 announce {::/0}
remarks: ========================================================
remarks: SJN Data Center D/B/A Encore Technologies
remarks: ========================================================
remarks: Route received will be tagged with the following communities
and be assigned a local preference as outlined.
remarks: community CUSTOMERS members 65534:123; - LOCAL_PREF 120
community PEERS members 65534:777; - LOCAL_PREF 110
community TRANSIT members 65534:124; - LOCAL_PREF 100
remarks: Routes received with the following communities will have the following actions applied.
remarks: community LOCAL_PREF_110 members 65534:110; - Set local pref 110
community LOCAL_PREF_120 members 65534:120; - Set local pref 120
community LOCAL_PREF_130 members 65534:130; - Set local pref 130
community LOCAL_PREF_70 members 65534:70; - Set local pref 70
community LOCAL_PREF_80 members 65534:80; - Set local pref 80
community LOCAL_PREF_90 members 65534:90; - Set local pref 90
community BLACKHOLE members 65535:666; - Set next-hop to discard, will be carried upstream as well
community NO-EXPORT members [ no-export 65535:65281] - Will not be announced outside local ASN
remarks: Routes announced with the following communities will not be
announced to the associated ASN.
remarks: community DO_NOT_ANNOUNCE_TO_AS10796 members 65534:10796;
community DO_NOT_ANNOUNCE_TO_AS3356 members 65534:3356;
community DO_NOT_ANNOUNCE_TO_AS6181 members 65534:6181;
remarks: Routes announced with the following communities will be carried upstream.
remarks: community CENTURYLINK_3356_BLACKHOLE members 3356:9999;
community CENTURYLINK_3356_LOCAL_PREF_70 members 3356:70;
community CENTURYLINK_3356_LOCAL_PREF_80 members 3356:80;
community CENTURYLINK_3356_LOCAL_PREF_90 members 3356:90;
remarks: BOGON ASNs will be filtered on ingress at all peering sessions.
remarks: as-path AS_ZERO ".* 0 .*";
as-path AS_TRANS ".* 23456 .*";
as-path EXAMPLE_AS_1 ".* [64496-64511] .*";
as-path EXAMPLE_AS_2 ".* [65536-65551] .*";
as-path RESERVED_AS_1 ".* [64512-65534] .*";
as-path RESERVED_AS_2 ".* [4200000000-4294967294] .*";
as-path LAST_16_AS ".* 65535 .*";
as-path LAST_32_AS ".* 4294967295 .*";
as-path RESERVED_AS ".* [65552-131071] .*";
remarks: BOGON Prefixes will be filtered on ingress at all peering sessions.
remarks: 0.0.0.0/8;
10.0.0.0/8;
100.64.0.0/10;
127.0.0.0/8;
127.0.53.53/32;
169.254.0.0/16;
172.16.0.0/12;
192.0.0.0/24;
192.0.2.0/24;
192.168.0.0/16;
198.18.0.0/15;
198.51.100.0/24;
203.0.113.0/24;
224.0.0.0/4;
240.0.0.0/4;
255.255.255.255/32;
remarks: ::/8;
0100::/64;
2001:2::/48;
2001:10::/28;
2001:db8::/32;
2002::/16;
2d00:0000::/8;
2e00:0000::/7;
3000:0000::/4;
3ffe::/16;
fc00::/7;
fe80::/10;
fec0::/10;
ff00::/8;
admin-c: ADMIN6147-ARIN
tech-c: NOC32519-ARIN
mnt-by: MNT-SDCL-12
source: ARIN
Now update the python lists to include this file.
import requests
import pprint
# Disable SSL Warnings
requests.urllib3.disable_warnings()
api_key = 'API-KEYKEYKEYKEYKEY'
org_id = 'ORDID-123'
aut_nums = ['arin-irr-aut-num-AS395176.txt',]
routes = []
routes6 = []
as_sets = []
route_sets = []
Each IRR object has a slightly different API URL format depending on if you are adding or updating. For that we build a function which tries to build the object as new (POST) and if ARIN returns an HTTP error we try the other method which updates the existing object (PUT). In the case of the aut-num object, the URL format is the same. In the cast of other object such as an as-set, we have to include the orgHandle in the POST URL but not in the PUT url. Both examples are below.
def aut_num_func(as_num, data, api_key):
url = 'https://reg.arin.net/rest/irr/aut-num/' + as_num + '?apikey=' + api_key
try:
#Attempt to post if doesnt exist
response = requests.post(url, data=contents, headers={'Accept': 'application/rpsl','Content-Type': 'application/rpsl'}, verify=False)
response.raise_for_status()
except requests.HTTPError:
#Put if does exist
response = requests.put(url, data=contents, headers={'Accept': 'application/rpsl','Content-Type': 'application/rpsl'}, verify=False)
pprint.pprint(response.text)
def as_set_func(as_set, data, api_key, org_id):
#URL CHANGES
try:
#Attempt to post if doesnt exist
url = 'https://reg.arin.net/rest/irr/as-set?apikey=' + api_key + '&orgHandle=' + org_id
response = requests.post(url, data=contents, headers={'Accept': 'application/rpsl','Content-Type': 'application/rpsl'}, verify=False)
response.raise_for_status()
except requests.HTTPError:
#Put if does exist
url = 'https://reg.arin.net/rest/irr/as-set/' + as_set + '?apikey=' + api_key
response = requests.put(url, data=contents, headers={'Accept': 'application/rpsl','Content-Type': 'application/rpsl'}, verify=False)
pprint.pprint(response.text)
If you are familiar with HTTP calls or the requests module in general you’ll notice the headers for these requests are their own application. Where we generally see application/xml or application/json we see application/rpsl. Best I can tell, and how I was able to get it to work is that application/rpsl is simply a multiline string. So if you did not want to read from a file you could include something like this in your python script.
aut_num = """aut-num: AS395176
as-name: SJN-AS395176
descr: SJN DATA CENTER DBA ENCORE TECHNOLOGIES
...
...
..."""
Lastly we call we call these functions for each entry in our lists. In this example I am simple showing an aut-num object to keep it short. The for loop in my main section could probably be a function as well, but that is a problem for another day.
import requests
import pprint
# Disable SSL Warnings
requests.urllib3.disable_warnings()
api_key = 'API-KEYKEYKEYKEYKEY'
org_id = 'ORDID-123'
aut_nums = ['arin-irr-aut-num-AS395176.txt',]
def aut_num_func(as_num, data, api_key):
url = 'https://reg.arin.net/rest/irr/aut-num/' + as_num + '?apikey=' + api_key
try:
#Attempt to post if doesnt exist
response = requests.post(url, data=contents, headers={'Accept': 'application/rpsl','Content-Type': 'application/rpsl'}, verify=False)
response.raise_for_status()
except requests.HTTPError:
#Put if does exist
response = requests.put(url, data=contents, headers={'Accept': 'application/rpsl','Content-Type': 'application/rpsl'}, verify=False)
pprint.pprint(response.text)
if __name__ == "__main__":
for aut_num in aut_nums:
with open(aut_num) as f:
contents = str(f.read())
as_num = contents.splitlines()[0]
as_num = as_num.strip('aut-num: ')
aut_num_func(as_num=as_num, data=contents, api_key=api_key)
print('')
In the for loop we extract the first line of the file which contains the name of the object. We have to do this because the URL needs the name of the object as well. This is easier than statically setting the URL and having to manually update it for each object you want to update or create.
Overall I am somewhere in the middle. On one hand, ARIN doesn’t do any syntax validation on import and export statements, they throw ambiguous errors if the object already exists or is not an object you own and you cannot update objects via the API that were created using the web interface. On the other, it’s free, unlike RADb. But even RADb is only 600 dollars per year.
For now, I’ll stick with RADb but will continue to keep my eye on ARIN.