libtorrent icon indicating copy to clipboard operation
libtorrent copied to clipboard

public ip address is not detected from loopback interface

Open milahu opened this issue 8 months ago • 4 comments

on some machines, the public ip address is assigned to the loopback interface lo for "fully redundant networking" (high availability network setup) for example at feralhosting.com

but then, qbittorrent fails to connect to trackers and DHT peers, and all torrents are "stalled" aka "no connections"

the qbittorrent status bar says

DHT: 0 nodes, green globe, green gauge, down 0 B/s (0 B), up 0 B/s (140 KiB)

other libs

this just works with other bittorrent libraries

  • rtorrent (todo verify)
  • rqbit - written in rust, based on the tokio async runtime
  • anacrolix/torrent - written in go

example

my lo device looks like this

$ ip a s lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo                       valid_lft forever preferred_lft forever
    inet 10.0.0.1/32 scope global lo                     valid_lft forever preferred_lft forever
    inet 185.21.216.155/32 scope global lo

my public ip address is

$ curl -s http://httpbin.org/ip
{
  "origin": "185.21.216.155"
}

qbittorrent advanced settings

  • Network interface: lo
  • Optional IP address to bind to: 185.21.216.155

... but libtorrent still ignores the lo interface

workaround

create a network namespace ("container") with an internal non-loopback network interface linked to the external loopback network interface

code

code search: loopback

https://github.com/arvidn/libtorrent/blob/21cbbf74ee4cdf41c5877fddb84bde128d217914/docs/manual.rst?plain=1#L871-L883

code search: if_flags::loopback

https://github.com/arvidn/libtorrent/blob/21cbbf74ee4cdf41c5877fddb84bde128d217914/src/session_impl.cpp#L334

expected

libtorrent should

  • use global addresses on local interfaces
  • ignore local addresses like 127.0.0.1 or 10.0.0.1

milahu avatar Apr 11 '25 09:04 milahu

so, is the issue that libtorrent incorrectly assumes that the LOOPBACK flag on the interface means it can only reach the local machine? Does it work if you remove the || (ipface.flags & if_flags::loopback) check?

arvidn avatar Apr 27 '25 12:04 arvidn

Does it work if you remove the || (ipface.flags & if_flags::loopback) check?

no, qbittorrent still says DHT: 0 nodes

https://github.com/arvidn/libtorrent/blob/21cbbf74ee4cdf41c5877fddb84bde128d217914/src/session_impl.cpp#L331-L338

with debug prints in session_impl.cpp i get

session_impl.cpp: ipface.name = lo
session_impl.cpp: ipface.interface_address = 127.0.0.1
session_impl.cpp: ipface.interface_address.is_loopback() = 1
session_impl.cpp: is_link_local(ipface.interface_address) = 0
session_impl.cpp: (ipface.flags & if_flags::loopback) = 4
session_impl.cpp: !is_global(ipface.interface_address) = 1
session_impl.cpp: !(ipface.flags & if_flags::pointopoint) = 1
session_impl.cpp: has_any_internet_route(routes) = 1
session_impl.cpp: !has_internet_route(ipface.name, family(ipface.interface_address), routes) = 0
session_impl.cpp: local = 1

session_impl.cpp: ipface.name = lo
session_impl.cpp: ipface.interface_address = 10.0.0.1
session_impl.cpp: ipface.interface_address.is_loopback() = 0
session_impl.cpp: is_link_local(ipface.interface_address) = 0
session_impl.cpp: (ipface.flags & if_flags::loopback) = 4
session_impl.cpp: !is_global(ipface.interface_address) = 1
session_impl.cpp: !(ipface.flags & if_flags::pointopoint) = 1
session_impl.cpp: has_any_internet_route(routes) = 1
session_impl.cpp: !has_internet_route(ipface.name, family(ipface.interface_address), routes) = 0
session_impl.cpp: local = 0

session_impl.cpp: ipface.name = lo
session_impl.cpp: ipface.interface_address = 185.21.216.155
session_impl.cpp: ipface.interface_address.is_loopback() = 0
session_impl.cpp: is_link_local(ipface.interface_address) = 0
session_impl.cpp: (ipface.flags & if_flags::loopback) = 4
session_impl.cpp: !is_global(ipface.interface_address) = 0
session_impl.cpp: !(ipface.flags & if_flags::pointopoint) = 1
session_impl.cpp: has_any_internet_route(routes) = 1
session_impl.cpp: !has_internet_route(ipface.name, family(ipface.interface_address), routes) = 0
session_impl.cpp: local = 0

185.21.216.155 is my public ip address

milahu avatar May 01 '25 10:05 milahu

im trying to fix this, once again...

in my use-local-interfaces-2 branch i have a added some debug prints to src/session_impl.cpp and i have added a DHT test client in tools/dht_client_test.cpp

mkdir build && cd build
cmake -Dbuild_tools=on ..
make
./tools/dht_client_test --bind lo --btih a9ae5333b345d9c66ed09e2f72eef639dec5ad1d

as expected: on my "good" server, the DHT test client connects to many DHT nodes on my "bad" server, the DHT test client always says DHT nodes: 0

a minimal UDP echo test does work on my "bad" server either binding to all IP addresses 0.0.0.0 or binding only to my public IP address $(curl ip.me)

udp-echo-test-boost-sync server --max-time 999 --addr 0.0.0.0
udp-echo-test-boost-sync server --max-time 999 --addr $(curl ip.me)

milahu avatar Oct 01 '25 05:10 milahu

one problem is this line

		if (m.addr.address() != i->second->target_addr()) continue;

in this part of libtorrent/src/kademlia/rpc_manager.cpp

bool rpc_manager::incoming(msg const& m, node_id* id)
{
// ...
	observer_ptr o;
	auto range = m_transactions.equal_range(tid);
	for (auto i = range.first; i != range.second; ++i)
	{
// FIXME
		if (m.addr.address() != i->second->target_addr()) continue;
		o = i->second;
		i = m_transactions.erase(i);
		break;
	}

on my "bad" server all UDP response packets seem to come from my public IP address

so m.addr.address() is my public IP address and i->second->target_addr() is the actual peer IP address

so observer_ptr o remains unset so the incoming DHT message is dropped by

	if (!o)
	{
// ...
		return false;
	}

i have tried to fix this in fd283118c04e7e3cfd11c3c7cb52d5da487b7aab and 1b5f7c73c813b0d2831a6b47c8f8d1fb7b2500ee by removing the line if (m.addr.address() != i->second->target_addr()) continue; and by replacing the wrong m.addr with local_m_addr = i->second->target_ep();

but DHT is still not working on my "bad" server...

maybe i have to actually replace m.addr but that would require passing a mutable (non-constant) reference of m to bool rpc_manager::incoming(msg const& m, node_id* id)

i have added "a million debug prints" to the DHT handling code maybe someone can decode this mess...

dht_client_test.good.out.1760463461.gz dht_client_test.bad.out.1760514284.gz

what i dont understand: my "bad" server receives much less nodes values in traversal_observer::reply

$ cat dht_client_test.good.out.1760463461 | grep '^traversal_observer::reply look_for_nodes' | cut -c65- | tr ' ' $'\n' | sort -u | wc -l
197

$ cat dht_client_test.bad.out.1760514284 | grep '^traversal_observer::reply look_for_nodes' | cut -c65- | tr ' ' $'\n' | sort -u | wc -l
24

and my "bad" server receives absolutely no values values in get_peers_observer::reply

$ cat dht_client_test.good.out.1760463461 | grep -E '\[get_peers_observer::reply\] got [0-9]+ peers in' | cut -d: -f4- | xargs -r echo | tr ' ' $'\n' | sort -u | wc -l
134

$ cat dht_client_test.bad.out.1760514284 | grep -E '\[get_peers_observer::reply\] got [0-9]+ peers in' | cut -d: -f4- | xargs -r echo | tr ' ' $'\n' | sort -u | wc -l
0

i do not have root access to my "bad" server so i cannot use tools like tcpdump but i can add more debug prints

the "public IP address assigned to the lo interface" network setup can be reproduced with linux containers (systemd-nspawn, docker, ...) AI prompt:

show me how to create a systemd-nspawn container on MY_DISTRO linux with my public IP address assigned to the "lo" interface, so that the container's "lo" interface has both my localhost IP address (127.0.0.1) and my public IP address (for example 192.168.178.21) (and maybe also the container's own IP address like 10.0.0.1)

so the output of ip address show lo should look like this:

$ ip address show lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo                       valid_lft forever preferred_lft forever
    inet 10.0.0.1/32 scope global lo                     valid_lft forever preferred_lft forever
    inet 185.21.216.155/32 scope global lo

milahu avatar Oct 15 '25 08:10 milahu