Yet another Mastodon-inspired post. In this toot the author reports that downloading python packages is slow, and the Internet said that disabling IPv6 is the solution.

Slow can mean two different things here. If the host I’m using has a globally unique IPv6 address, but my connection to the outside is broken somehow, most software would try IPv6 first and then, after a timeout would fall back to IPv4 and try again. It can also mean that the IPv6 connection is working, but the download is actually slow.

Here is how I would debug this problem.

Do you have IPv6?#

Let’s check if we have a globally unique IPv6 address:

$ ip -6 addr show enp1s0
2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    inet6 2001:db8:aaaa::22/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::5054:ff:fe07:bdd7/64 scope link
       valid_lft forever preferred_lft forever

And if we have a default route:

$ ip -6 route show | grep default
default via fe80::f827:91ff:fe6e:4283 dev wlp3s0 proto ra metric 600 pref high

If you have an address but no route then something in your network is broken. Fix it.

Now let’s see if we can reach something on the Internet:

$ ping -c 3 2600::
PING 2600::(2600::) 56 data bytes
64 bytes from 2600::: icmp_seq=1 ttl=53 time=111 ms
64 bytes from 2600::: icmp_seq=2 ttl=53 time=111 ms
64 bytes from 2600::: icmp_seq=3 ttl=53 time=111 ms

--- 2600:: ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2001ms
rtt min/avg/max/mdev = 111.108/111.124/111.144/0.014 ms

You can also use sites like https://testipv6.com/ to test your connectivity. If you don’t have IPv6, disabling it won’t help unless your OS is broken in a very strange way.

Got working IPv6?#

Good, let’s continue. Let’s download something not to big, not to small via IPv4 and IPv6. I’m using a Linux Kernel in this example:

$ wget -4 https://git.kernel.org/torvalds/t/linux-6.14-rc5.tar.gz
[...]
2025-03-08 17:44:13 (18.4 MB/s) - ‘linux-6.14-rc5.tar.gz’ saved [247403222]
$
$ wget -6 https://git.kernel.org/torvalds/t/linux-6.14-rc5.tar.gz
[...]
2025-03-08 17:44:30 (27.3 MB/s) - ‘linux-6.14-rc5.tar.gz.1’ saved [247403222]

In this case IPv6 is faster than IPv4 so no problem at all.

Does the other side have IPv6?#

Python packages are not downloaded from pypi.org but from files.pythonhosted.org so we check if they have IPv6.

$ host files.pythonhosted.org
files.pythonhosted.org is an alias for dualstack.python.map.fastly.net.
dualstack.python.map.fastly.net has address 151.101.128.223
dualstack.python.map.fastly.net has address 151.101.192.223
dualstack.python.map.fastly.net has address 151.101.64.223
dualstack.python.map.fastly.net has address 151.101.0.223
dualstack.python.map.fastly.net has IPv6 address 2a04:4e42:600::223
dualstack.python.map.fastly.net has IPv6 address 2a04:4e42:200::223
dualstack.python.map.fastly.net has IPv6 address 2a04:4e42::223
dualstack.python.map.fastly.net has IPv6 address 2a04:4e42:400::223

As we see files.pythonhosted.org is an alias for dualstack.python.map.fastly.net. Which has, as the dualstack in the name implies, both IPv4 and IPv6 addresses.

How to we get to the target?#

We’ll now use traceroute to track our way to the target. We are using TCP for probing and run one trace for IPv4 and one for IPv6.

# traceroute -6 -T files.pythonhosted.org
traceroute to files.pythonhosted.org (2a04:4e42:400::223), 30 hops max, 80 byte packets
 1  2001:678:340::1 (2001:678:340::1)  0.367 ms  2284.382 ms *
 2  2a03:3e00:1:1::1 (2a03:3e00:1:1::1)  0.792 ms * *
 3  2a03:3e00:1::1 (2a03:3e00:1::1)  0.874 ms * *
 4  2a03:3e00:0:8000::2 (2a03:3e00:0:8000::2)  2.659 ms * *
 5  * * *
 6  2a02:2d8:1:700c:232a:: (2a02:2d8:1:700c:232a::)  4.002 ms * *
 7  GW-Fastly.retn.net (2a02:2d8:1:7010:232a::1)  1.090 ms * *
 8  2a04:4e42:400::223 (2a04:4e42:400::223)  4.011 ms * *
#
# traceroute -4 -T files.pythonhosted.org
traceroute to files.pythonhosted.org (151.101.64.223), 30 hops max, 60 byte packets
 1  routerhk.quux.de.43.235.109.in-addr.arpa (109.235.43.65)  0.585 ms  670.105 ms *
 2  109.235.46.2 (109.235.46.2)  670.167 ms  879.770 ms  879.655 ms
 3  * * *
 4  * * *
 5  fra1.decixfra.fastly.net (80.81.195.54)  0.982 ms * *
 6  151.101.64.223 (151.101.64.223)  0.907 ms * *

Here the IPv4 path is shorter, but that’s nothing to worry about. They both look okay. I would start to worry if one of the traceroutes would be way longer. Sometimes the host names tell you where you are in the world, e.g. fra1.decixfra.fastly.net is very likely in Frankfurt, Germany.

Understanding traceroutes is not easy. I’m planing a blog post and maybe a whole presentation about the topic, but I highly recommend the following slide deck: https://archive.nanog.org/sites/default/files/10_Roisman_Traceroute.pdf

If one of the traceroutes is way of, e.g. your tracing from Berlin to Munich and see names that point to routers located in London, Barcelona or New York, you should talk to your provider.

If the traceroutes look okay it’s now time to download something from there. You want something not to big, not to small. I chose some random Django tar.gz here.

$ wget -4 https://files.pythonhosted.org/packages/f6/22/ae506393d2fb47d8ea8256cb600072b02d971134e26bd1d4b8fbfb8fca9e/Django-5.2b1.tar.gz
[...]
2025-03-08 18:05:38 (30.3 MB/s) - ‘Django-5.2b1.tar.gz’ saved [10816962/10816962]
$
$ wget -6 https://files.pythonhosted.org/packages/f6/22/ae506393d2fb47d8ea8256cb600072b02d971134e26bd1d4b8fbfb8fca9e/Django-5.2b1.tar.gz
2025-03-08 18:05:46 (37.8 MB/s) - ‘Django-5.2b1.tar.gz.1’ saved [10816962/10816962]

And again IPv6 is faster than IPv4 so no problem.

If one of these Downloads is way slower it’s time to get out the big guns and use tcpdump and / or Wireshark, capture the traffic and see if we find anything in there. But that is a topic for a whole series of blog posts.

What else can we use for troubleshooting?#

Using curl we can also check the connection time to a server for both IPv4 and IPv6.

$ curl -4 -s -o /dev/null -w '%{time_total}\n' https://files.pythonhosted.org/
0.081652
$ curl -6 -s -o /dev/null -w '%{time_total}\n' https://files.pythonhosted.org/
0.094733

Curl has more values than just time_total you can play around with. Check the man page.