loopback(127.0.0.1, ::1), localhost, and 0.0.0.0 in Local Development
Why I Wrote This
I found an issue where my backend server was listening on 127.0.0.1:1234
, but I couldn't access to it using localhost:1234
in my browser or curl
. Initially I thought it’s a backend issue, I found no errors in the logs. This led me to check whether 127.0.0.1 and localhost were truly equivalent.
Short answer: In NodeJs 17, localhost is primarily resolved to an IPv6 address. My backend was only listening on IPv4, causing client requests to fail without ever reaching the server.
In This Article
We will cover the following topics:
- Loopback: Understanding how the loopback interface works.
- IPv4 Range: A brief overview of the IPv4 address range and its significance.
- IPv6 fe80: An introduction to link-local addresses in IPv6.
- 0.0.0.0 and localhost: Clarifying the meanings and uses of these addresses.
Loopback
Loopback is a virtual interface without physical meaning.
You can think of it as a virtual door, like Doraemon's Anywhere Door, but this door only leads to your own machine when opened. In other words, it's an Anywhere Door that only works within your own computer.
Common loopback addresses:
address | format |
127.0.0.1 | IPv4 |
::1 | IPv6 (=0000:0000:0000:0000:0000:0000:0000:0001) |
IPv4 Loopback Range
IPv4 sets aside a whole bunch of addresses from 127.0.0.0
to 127.255.255.255
for loopback. It's like having a ton of virtual doors all leading back to your own computer.
This comes in handy when you want to test multiple programs on one machine.
For example, you could have:
- Server A using 127.0.0.4
- Server B using 127.0.5.4
- And so on...
IPv6 has only Loopback ::1
, no Loopback Range
Back on the design decisions of IPv4, it’s clear that IPv6 has learned from those lessons by avoiding the need for a reserved range like 127.0.0.0/8. With the vast address space available in IPv6, there’s no need for such concerns.
In the past, IPv4 users had to be careful about setting up private addresses, and forgetting to properly configure a private address as public could lead to security risks.
IPv6 provides ample space, allowing for more straightforward and safer address allocation.
IPv6 has something called link-local addresses (fe80::/10
).
These kind of make up for not having a loopback range.
Why: it is better than IPv4?
Using the fe80::/10 link-local addresses allows devices to operate flexibly across multiple interfaces while avoiding the potential issues of address configuration found in IPv4. This results in greater efficiency and convenience.
The IPv6 standard: says addresses from fe80::/10 to febf::/10 let devices on the same network talk to each other without needing routers or DHCP. It's pretty neat!
Here's a quick breakdown of how it works:
- fe80 to febf (ignore the rest)
- In numbers, that's
fe 8 0
tofe 11 16
- In binary:
fe 1000 0000
tofe 1011 1111
- After the first 10 bits, you've got 2^118 possible addresses!
BUT In practice: we only use fe80::/64
for link-local stuff. The rest is saved for later. IPv6 devices automatically give themselves a link-local address. With so many possible addresses (about 18 quintillion!), the chances of two devices picking the same address are basically zero.
Also, these link-local addresses in IPv6 are different from private network addresses in IPv4 (like 10.x.x.x or 192.168.x.x). They serve different purposes, even if they might seem similar at first glance. Link-local addresses in IPv6 DO NOT NEED INTERNET.
fe80: The Helper You're Already Using
- Routers chat using NDP (Neighbor Discovery Protocol) for IPv6
- For IPv4, they use ARP to match IP and MAC addresses
- IPv6 is huge, so NAT isn't usually needed (but NAT66 exists for transition)
- Newbies use a fe80 (link-local) address to introduce themselves to the router and get information about the network
When the New Kid Joins the Network Classroom
- no name tag (IP) yet.
- It makes up a
fe80
nickname to introduce itself. Kinda like "Hey router"
- IPv6 trick called SLAAC. It uses that
fe80
nickname to set things up.
- If it needs more info, DHCP's got its back - like getting the class schedule and cafeteria menu.
Three MUSTs for communication within fe80::/10
- Same subnet
- Same medium (like the same WiFi access point)
- Same protocol (WiFi and Ethernet are completely different)
From the OSI perspective, they ONLY works in the physical layer and the data link layer.
Hey, all network cards on the same computer start with fe80
, so they can talk to each other freely, right?
using ifconfig
, you can see so many interface and their ipv6 addresses started with fe80::/10
.
ifconfig
No! Even if these three conditions are met, communication can still be limited due to virtual network interfaces on the same computer.
Example: Vrtual interfaces (like utun1, utun2, utun3, etc.):
These are virtual network interfaces created dynamically by apps. VPNs are a common example. They create a virtual tunnel on your computer, allowing you to communicate with "restricted remote" locations through this virtual network.
Example: Physical interfaces (like en0, en1, en2, en3, etc.):
These are your actual, physical network interfaces. Usually, en0 is for Ethernet, en1 for WiFi, and the rest for other connections.
But here's the catch: even these can't freely communicate because they're on different physical links - basically, different network environments.
If you want them to talk, you'll need to set up a bridge (like bridge0) to connect them.
Isn't this similar to how 192.168.0.[x|y]
communicates in local development?
- LANs have multiple subnets
- Subnets have multiple links
The key difference is that subnets are routed while fe80::/10
only needs the link conditions to be met for communication.
Example: When you're running a server on your computer connected to WiFi, and you want to access it from your phone on the same WiFi:
- Method 1: Using 192.168.0.[x|y]
- You type 192.168.0.x on your phone
- The data goes through the router to reach your computer
- Method 2: Using
http://[ipv6address%yourNetworkInterface]:3000
- For example:
curl http://[fe80::xxxx:xxxx:xxxx:4c27%en0]:3000
- For example:
Method 2? It doesn't go through the router at all! It's a direct link between your devices.
0.0.0.0
0.0.0.0 | IPv4 |
:: | IPv6 |
Think of 0.0.0.0 as a wildcard or placeholder. It can represent any address, but no single address can fully represent it.
It's kind of deep when you think about it - like a divine IP address saying, "I represent everything, but nothing specific can represent me."
Pretty philosophical for a bunch of zeros, right?
Let's break it down from different perspectives:
- Server's View: When a server listens on 0.0.0.0, it's basically saying,
"I'm all ears on every available network interface.".
- Regular Client's View: As a destination, 0.0.0.0 is a no-go.
It's like trying to mail a letter to "Anywhere" - it just doesn't work.
If you try to
ping 0.0.0.0
, your computer will look at you and say, "I can’t"
- Browser's View: Now, this is where it gets interesting. Remember how
pinging 0.0.0.0
was useless? Well, browsers are a bit smarter (or maybe just more humanity).Most modern browsers see 0.0.0.0 and think, "Oh, you probably mean localhost or 127.0.0.1." It's like they're trying to read your mind. But not all browsers agree on how to handle this:
- Firefox: "I'll let you access it, but I'm going to warn you something."
- Chrome: "Sure, go right ahead. I don’t care"
- Safari: "Nope, not happening. Don't even try."
browser backend server frontend fetch(…) result chrome
128.0.6613.1390.0.0.0 0.0.0.0 🟢 200 OK safari17.4 0.0.0.0 0.0.0.0 ❌ failed to load resource: bad url (no response) firefox 130 0.0.0.0 0.0.0.0 🔺said “not secure”, but still get 200
Firefox special behavior
Firefox warns you it's not secure, probably because it's not HTTPS. But wait a minute - local development often uses HTTP, and that's normal, right? And Firefox doesn't warn you when accessing localhost, even though that's HTTP too. What gives?
backend server | frontend fetch(…) | |
0.0.0.0 | localhost | secure |
0.0.0.0 | 0.0.0.0 | not secure |
The likely reason:
Localhost is special. It's designed from the get-go to be a trusted domain on your machine.
… But when you use an IP address like 0.0.0.0, the browser isn't sure if you're staying local or venturing out into the wild internet. So it plays it safe and gives you a heads up.
localhost
localhost is a special hostname, but it's not a shortcut. When a browser receives "localhost", it still goes through DNS resolution. It doesn't think, "Oh, it's the local machine, I'll just send it to 127.0.0.1 without DNS resolution.”
DNS resolution behavior needs to stay consistent. Plus, you might point localhost to a different address, like 127.0.1.4.
Example 1: You might point localhost to a different address, like 127.0.1.4
.
# cat /etc/hosts
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
# this will go to 127.0.1.4 not 127.0.0.1
# 127.0.0.1 is overwritten.
127.0.1.4 localhost
If you add 127.0.0.1 aapple.com
to your hosts file, both aapple.com
and localhost
will go to 127.0.0.1
.
# cat /etc/hosts
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
127.0.0.1 aapple.com
Back to Issue Testing
$ cat /etc/hosts
##
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
If a Node.js 17 frontend
tries to access a normal backend:
Backend host | Frontend access to | Result |
localhost | 127.0.0.1 | 🟢 |
localhost | localhost | 🟢 |
localhost | 0.0.0.0 | 🟢 |
0.0.0.0 | 127.0.0.1 | 🟢 |
0.0.0.0 | localhost | ❌ |
0.0.0.0 | 0.0.0.0 | 🟢 |
127.0.0.1 | 127.0.0.1 | 🟢 |
127.0.0.1 | localhost | ❌ |
127.0.0.1 | 0.0.0.0 | 🟢 |
The issue occurring in Node.js 17 version seems to be that if /etc/hosts is set with "::1 localhost", it will resolve your localhost to IPv6. If your backend is listening on an IPv4 address, the client side won't connect at all, and there won't even be an error log.
Let's look at it the other way? Start a Node.js 17 backend
with a normal Node.js frontend:
Backend host | Firefox access to | Result | using curl |
localhost | 127.0.0.1 | ❌ | ❌ |
localhost | localhost | 🟢 | 🟢 |
localhost | 0.0.0.0 | ❌ | ❌ |
0.0.0.0 | 127.0.0.1 | 🟢 | 🟢 |
0.0.0.0 | localhost | 🟢 | 🟢 |
0.0.0.0 | 0.0.0.0 | 🟢 (not secure flag) | 🟢 |
127.0.0.1 | 127.0.0.1 | 🟢 | 🟢 |
127.0.0.1 | localhost | 🟢 | 🟢 |
127.0.0.1 | 0.0.0.0 | 🟢 (not secure flag) | 🟢 |
Some people suggest not using localhost, but instead using a specific IPv4 address because localhost might behave differently on different systems.
You shouldn't be usinglocalhost
but127.0.0.1
instead. Reason is that name resolution for localhost varies a lot between different systems and implementations of their network stacks.
This is reasonable, although in most development scenarios we don't encounter such extreme system differences.
Resources
https://thiscute.world/posts/linux-virtual-network-interfaces/