Recently, we have been working on a feature that involves dealing with IP addresses. We came across some unusual IP addresses in our logs. Specifically, the addresses followed this pattern: ::ffff:x.x.x.x
. Such IP addresses are called IPv4-mapped IPv6 addresses and in this article, we are going to discuss why they are used and show how to handle them in Node.js. Handling IPv4-mapped IPv6 addresses properly is useful for many applications, e.g. IP address blocking or applications working with both IPv4 and IPv6 addresses. First, we will look at some necessary background and reasons why these IP addresses exist, then we will discuss some Node.js code to handle this type of IP address properly.
Internet Protocol
At the dawn of the universe internet, the addressing infrastructure was handled by Internet Protocol version 4 (IPv4). IPv4 address is a 32-bit value, so it can hold about 4,294,967,296 (232) different addresses (note: the actual number is somewhat lower as there are some addresses reserved for multicasts and local networks). An IPv4 address generally looks like this: x.x.x.x
, where x
is a number between 0 and 255. With the massive growth of the internet, the IPv4 addresses started to run out and there was a need for a new protocol that would have a larger address space.
Internet Protocol version 6 (IPv6) is the successor to IPv4, and the most notable change is the size of the actual value, an IPv6 address has 128 bits. The specific format consists of 8 groups of 4 hexadecimal digits separated by :
instead of .
. A concrete example of an IPv6 address is 10e1:0000:0000:c69b:c32d:22b7:7c85:083a
. It is possible to shorten this address via ::
, which can replace consecutive groups of zeros. The resulting address looks like this: 10e1::c69b:c32d:22b7:7c85:083a
.
The IPv6 protocol can hold around 340,282,366,920,938,463,463,374,607,431,768,211,456 (2128) distinct addresses. As you can see, this number is quite future-proof. Nevertheless, the introduction of a new protocol creates a major headache - transitioning between the old and the new protocol. This is where IPv4-mapped IPv6 addresses come into play.
An IPv4-mapped IPv6 address is an IPv6 address that is used to store addresses from IPv4. The syntax is quite simple, it’s just a normal IPv4 address prefixed by ::ffff:
, e.g. ::ffff:1.2.3.4
, this corresponds to the IPv4 address 1.2.3.4
.
The prefix is 80 bits of zeros (they are hidden by the ::
shorthand), 16 bits of ones (the ffff
part) and the last 32 bits are the IPv4 address. According to Wikipedia, the prefix was chosen to preserve compatibility:
The prefix was chosen to yield a zero-valued checksum to avoid changes to the transport protocol header checksum.
How to find out if Node.js listens on IPv6
Let’s look at some JavaScript code now. First, we need to find out if the Node.js process listens on IPv4 or IPv6.
For this, we will use a simple Node.js application using the Express framework for the server itself. We will call it demo.js
. The basic code looks like this:
const express = require('express');
const app = express();
const port = 4321;
app.get('/', (req, res) => {
res.json({
remoteAddress: req.ip,
});
});
app.listen(port, () => {
console.log(`Demo app listening at http://localhost:${port}`);
});
First, we install express and then run the demo code:
npm i express
node demo.js
Second, we keep the demo code running, open up another terminal and run:
sudo lsof -i -P -n | grep LISTEN
This command will give us the list of all processes listening on certain ports, but the most important information will be whether the process uses IPv4 or IPv6 (note: this command is only for Linux and Mac, try this for Windows). Sample output will look like this:
We can see that Node.js will use IPv6 by default. This will also work with IPv4 addresses, but we need to be aware of the ::ffff:
prefix described in the previous section.
Testing both IPv4 and IPv6 connections
You might have noticed that demo.js
contains code that will return a request's IP address. Let's try to call the demo app with three different URLs and see if we notice anything interesting. The URLs will be:
http://localhost:4321/
http://127.0.0.1:4321/
http://[::1]:4321/
We will be using cURL, so use this in the terminal (just change the URL):
curl http://localhost:4321/
The response for http://[::1]:4321/
is:
{
"remoteAddress": "::1"
}
We expected this response. Note: ::1
represents the local loopback in IPv6. The square brackets in http://[::1]:4321/
are necessary to distinguish the IPv6 address in the URL.
However, for http://localhost:4321/
and http://127.0.0.1:4321/
, the response is:
{
"remoteAddress": "::ffff:127.0.0.1"
}
This corresponds to the fact that our Node.js app is using IPv6. A side note about localhost
: the reason localhost behaves like 127.0.0.1
is the fact that the machine running the code has localhost
bound to 127.0.0.1
in the /etc/hosts
file.
The IP address with the prefixed ::ffff:
is not very friendly. Fortunately, there is an open-source package that is perfect for handling this: ipaddr.js.
ipaddr.js
ipaddr.js is a small JavaScript library for IP address manipulation. In our case, we're going to use it for IP address validation and parsing.
Let's install ipaddr.js:
npm i ipaddr.js
Here's an updated version of demo.js
with ipaddr.js used:
const express = require('express');
const ipaddr = require('ipaddr.js');
const app = express();
const port = 4321;
app.get('/', (req, res) => {
let remoteAddress = req.ip;
if (ipaddr.isValid(req.ip)) {
remoteAddress = ipaddr.process(req.ip).toString();
}
res.json({
remoteAddress,
});
});
app.listen(port, () => {
console.log(`Demo app listening at http://localhost:${port}`);
});
Let's discuss the usage of ipaddr.js:
The first thing we need to do is to validate the incoming IP address. It's important to note that the content of req.ip
is not always a valid IP address, so that's why we have to do it. ipaddr.isValid
will validate both IPv4 and IPv6 addresses.
Next, we want to convert any IPv4-mapped IPv6 address to plain IPv4. Luckily, ipaddr.js has a method for this purpose: ipaddr.process
. This method parses the IP address and converts IPv4-mapped IPv6 address to IPv4. The return value of this function is an internal ipaddr.js representation of the IP address, so we need to call .toString()
to get the expected string value.
If you feel that the process
method is somewhat non-transparent and a bit too much like "magic", you can instead use this code:
let remoteAddress = req.ip;
if (ipaddr.isValid(remoteAddress)) {
const addr = ipaddr.parse(remoteAddress);
if (addr.kind() === 'ipv6' && addr.isIPv4MappedAddress()) {
remoteAddress = addr.toIPv4Address().toString();
}
}
Now, let's see our code in action. We're going to repeat the three requests from the previous section.
The response for http://[::1]:4321/
is:
{
"remoteAddress": "::1"
}
For http://localhost:4321/
and http://127.0.0.1:4321/
, we get:
{
"remoteAddress": "127.0.0.1"
}
We managed to achieve our goal - get a regular IPv4 address without the prefix.
...and one last thing
ipaddr.js is an open-source library and we encourage you to explore its code - the main source file is only about 1,000 lines long, the code is very readable and you can learn a lot from it, e.g. regular expressions for IP address parsing.
We love open source at Apify - feel free to explore our GitHub account, which is full of interesting web scraping tools and packages.