Linode provides a service to their customers called DNS Manager. This allows you to create and modify name server records to be served from Linode’s DNS infrastructure, without having to maintain your own name server.
Deconstructing, analysing and reconstructing this API is the topic of this Technical.
As mentioned above, this API allows users to create and modify name server records. To achieve this, you should be able to do the following things:
Records may also have a type (for example A, the IPv4 record, or AAAA, the IPv6 record), and types have differing requirements, so the data for each needs to be captured.
The DNSManager API is a Single Endpoint API (see Layout for details) that is shared with Linode’s other APIs.
You send it requests to its endpoint (https://api.linode.com/) with query arguments that specify the function you wish to run, along with some arguments that match the specific function.
Authentication is done by either providing an API key as the password in a HTTP BASIC Authorization header, or putting it in the api_key query argument.
Transport security is provided via TLS, with support for ECDHE (and therefore forward secrecy).
The response to each request is a JSON object, consisting of three keys:
An API key is required to use the Linode API. One can be got from their web interface, using the user.getapikey function.
$ curl "https://api.linode.com/" \
-d "api_action=user.getapikey" \
-d "username=hawkowl" \
-d "password=7yId7UoGhsnYh1k"
This will respond with something similar to the following:
{
"ERRORARRAY": [],
"ACTION": "user.getapikey",
"DATA": {
"USERNAME": "hawkowl",
"API_KEY": "SECRETKEY"
}
}
Note
All instances of SECRET_KEY would be where a valid Linode API key would be. It’s much too long to display inline (60+ characters).
Linode provides an “echo” function for testing.
$ curl "https://api.linode.com/" \
-d "api_key=SECRETKEY" \
-d "api_action=test.echo" \
-d "foo=bar"
Since test.echo function simply responds with what it was given, that request will respond with this on success:
{
"ERRORARRAY": [],
"ACTION": "test.echo",
"DATA": {
"foo": "bar"
}
}
If something goes wrong, it will respond with an error instead:
{
"ERRORARRAY": [{
"ERRORCODE": 4,
"ERRORMESSAGE": "Authentication failed"
}],
"ACTION": "test.echo",
"DATA": {}
}
To create a domain, we need to use the domain.create method. This takes a number of arguments, but a working command is below.
Note
The API docs for Linode’s domain.create method say that CustomerID is required. This is wrong.
$ curl "https://api.linode.com/" \
-d "api_key=SECRETKEY" \
-d "api_action=domain.create" \
-d "Domain=mycoolawesomesite.net" \
-d "Type=master" \
-d "SOA_Email=hawkowl@atleastfornow.net"
{
"ERRORARRAY": [],
"ACTION": "domain.create"
"DATA": {
"DomainID": 12345
}
}
DomainID is what you want to hold onto. This is the ID of your new domain, and you will need it to query it, delete it, or add entries to it.
We can query it like this:
$ curl "https://api.linode.com/" \
-d "api_key=SECRETKEY" \
-d "api_action=domain.list" \
-d "DomainID=12345"
{
"ERRORARRAY": [],
"ACTION": "domain.list",
"DATA": [{
"DOMAINID": 12345,
"DESCRIPTION": "",
"EXPIRE_SEC": 0,
"RETRY_SEC": 0,
"STATUS": 1,
"LPM_DISPLAYGROUP": "",
"MASTER_IPS": "",
"REFRESH_SEC": 0,
"SOA_EMAIL": "hawkowl@atleastfornow.net",
"TTL_SEC": 0,
"DOMAIN": "mycoolawesomesite.net",
"AXFR_IPS": "none",
"TYPE": "master"
}]
}
Note
Not giving the DomainID key will make it return all domains under your account.
We can then add what Linode calls “resources” to this domain, such as subdomains.
$ curl "https://api.linode.com/" \
-d "api_key=SECRETKEY" \
-d "api_action=domain.resource.create" \
-d "DomainID=12345" \
-d "Type=A" \
-d "Name=www" \
-d "Target=203.0.113.27"
{
"ERRORARRAY": [],
"ACTION": "domain.resource.create",
"DATA": {
"ResourceID": 7654321
}
}
There are several kinds of types of resources – A, AAAA, TXT, MX, SRV, NS and CNAME. They all share the same resource creation function, and some of the meanings of the parameters are overloaded. None of the parameters other than Type or the DomainID are marked as universally required in the documentation, requiring you to read the description to see if it applies to the type you are creating.
For instance, the Target parameter has the following docs:
When Type=MX the hostname. When Type=CNAME the target of the alias. When Type=TXT the value of the record. When Type=A or AAAA the token of ‘[remote_addr]’ will be substituted with the IP address of the request.
The full documentation for this function can be found on Linode’s site.
Listing resources works more or less the same as domain.list. A DomainID is given to domain.resources.list, with an optional ResourceID to display only a single resource. Otherwise, all resources under that domain are given.
$ curl "https://api.linode.com/" \
-d "api_key=SECRETKEY" \
-d "api_action=domain.resource.list" \
-d "DomainID=12345"
{
"ERRORARRAY": [],
"ACTION": "domain.resource.list",
"DATA": [{
"DOMAINID": 12345,
"PORT": 80,
"RESOURCEID": 7654321,
"NAME": "www",
"WEIGHT": 5,
"TTL_SEC": 0,
"TARGET": "203.0.113.27",
"PRIORITY": 10,
"PROTOCOL": "",
"TYPE": "A"
}]
}
As I see it, the current Linode API has the following shortfalls:
Now that we have analysed how the API works and used it in context, I will now re-engineer it from the ground up, providing a proof in concept using the Twisted asynchonous networking framework and the Saratoga API development framework.
The API needs to handle a few particular data models:
I these can be better termed as domains, zone mirrors, and records, respectively.
The API will be in the RFC-3986 Style, with an explicit version in the path. The whole API for this example will be dedicated to the DNSManager API. An example of the root URI for v1 would be something like dns.api.linode.com/v1/.
Since we have two top level models, we should have them at the root:
/domains
/zonemirrors
You can then refer to individual domains and mirrors with an ID:
/domains
/domains/<ID>
/zonemirrors
/zonemirrors/<ID>
As domains can have records, we need to be able to refer to them too:
/domains
/domains/<ID>
/domains/<ID>/records
/domains/<ID>/records/<ID>
/zonemirrors
/zonemirrors/<ID>
But since different records have incredibly disparate data models depending on the type, it might be good to keep them seperate:
/domains
/domains/<ID>
/domains/<ID>/A
/domains/<ID>/A/<ID>
/domains/<ID>/MX
/domains/<ID>/MX/<ID>
/domains/<ID>/NS
/domains/<ID>/NS/<ID>
/domains/<ID>/AAAA
/domains/<ID>/AAAA/<ID>
/domains/<ID>/TXT
/domains/<ID>/TXT/<ID>
/domains/<ID>/SRV
/domains/<ID>/SRV/<ID>
/domains/<ID>/CNAME
/domains/<ID>/CNAME/<ID>
/domains/<ID>/records
/zonemirrors
/zonemirrors/<ID>
This lets us get all of the records of a domain in one go, or all the records of a specific type on the domain. Accessing a record individually has to be done through the correct type.
This map looks a bit complicated. However, since every record type has different parameters, it makes a lot more sense to split them up. It also makes it easier to document and use, as you don’t have overloaded meanings of each option.
Domains (“master zones”) are core to the API – they are what everything else sits under.
Adapting from the Linode API docs, this is the domain ‘model’:
From this, we can develop the following JSON Schema for creating a Domain:
{
"description": "Domain -- Create",
"type": "object",
"required": ["domain", "soa"],
"properties": {
"domain": {
"title": "The base for this domain.",
"type": "string",
"format": "hostname"
},
"soa": {
"title": "Start Of Authority Email.",
"type": "string",
"format": "email"
},
"default_ttl": {
"title": "Default TTL for records, in seconds.",
"type": "integer"
},
"status": {
"title": "The status of the domain.",
"type": "string",
"enum": ["active", "inactive"]
},
"axfr": {
"title": "IP addresses which may AXFR the domain.",
"oneOf": [
{
"type": "array",
"uniqueItems": true,
"items": {
"anyOf": [
{
"type": "string",
"format": "ipv4"
},
{
"type": "string",
"format": "ipv6"
}
]
}
},
{
"type": "string",
"length": 0
}
]
}
}
}
To put it simply, this means that a domain is an object (dict), and can have these properties. Out of those properties, domain and soa must be given. The rest are optional, and have defaults if they are not provided.
But since we also want to validate outputs as well as inputs, lets also write a JSON Schema for the response. (It’s generally good to respond as if they immediately did a GET request on the new resource.)
{
"description": "Domain -- Create Response",
"type": "object",
"properties": {
"id": {
"title": "The ID of this domain.",
"type": "integer"
},
"domain": {
"title": "The base for this domain.",
"type": "string",
"format": "hostname"
},
"soa": {
"title": "Start Of Authority Email.",
"type": "string",
"format": "email"
},
"default_ttl": {
"title": "Default TTL for records, in seconds.",
"type": "integer"
},
"status": {
"title": "The status of the domain.",
"type": "string",
"enum": ["active", "inactive"]
},
"axfr": {
"title": "IP addresses which may AXFR the domain.",
"type": "array",
"uniqueItems": true,
"items": {
"oneOf": [
{ "format": "ipv4" },
{ "format": "ipv6" }
]
}
}
}
}
They are nearly exactly similar, barring the inclusion of id in the response. By checking both the input and output, it is less likely that a bug will cause the API to return incorrect data or data that it shouldn’t.