Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Linux: SO_BINDTODEVICE not used for http tracker announce ("leaks" real IP) #6772

Closed
ckcr4lyf opened this issue Mar 9, 2022 · 26 comments
Closed
Milestone

Comments

@ckcr4lyf
Copy link

ckcr4lyf commented Mar 9, 2022

libtorrent version (or branch): 2.0.5-1

platform/architecture: Arch Linux Linux arch 5.16.10-arch1-1 , x64 Intel

compiler and compiler version: Sorry dunno, installed qbit from pacman today (2022-03-09)

TL;DR SO_BINDTODEVICE is only used on the peer BT connection, so it is possible for the TCP packets for HTTP announce to be sent from wrong interface (and result in tracker registering the wrong external IP)

My qBittorrent settings:
image

However the announce request is being sent from the wlp0s20f3 interface (though source IP is indeed 10.16.0.5). More details below...

I have two gateways on my machine, with my secondary gateway having a higher metric. (Note: Technically the 2nd gateway is an OpenVPN connection - but I ave configured routes manually and I am not trying a killswitch or anything like that, so I will try and refer to it as gateway #2)

My main gateway has Public IP 183.x.x.x , local is 192.x.x.131 on wlp0s20f3
My secondary gateway has Public IP 152.x.x.x, local is 10.x.x.5 on tun0.

Routing table:

$ route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.128.1   0.0.0.0         UG    600    0        0 wlp0s20f3 <- gateway 1 , my "normal" internet connection
0.0.0.0         10.16.0.1       0.0.0.0         UG    650    0        0 tun0 <- gateway 2, metric higher than gateway 1
10.16.0.0       0.0.0.0         255.255.255.0   U     0      0        0 tun0
152.x.x.x       192.168.128.1   255.255.255.255 UGH   0      0        0 wlp0s20f3 <- This is for my OpenVPN connection to go through
192.168.128.0   0.0.0.0         255.255.255.0   U     600    0        0 wlp0s20f3

I verify that the routing to internet happens correctly (default via interface) by:

raghu@arch:~$ curl ifconfig.co
183.x.x.x (Gateway 1 - i.e. my real public IP)
raghu@arch:~$ curl ifconfig.co --interface tun0
152.x.x.x (Gateway 2 - i.e. Public IP of server OpenVPN running on)

When I add an http tracker in qbittorrent, the packets are sent out on the wlp0s20f3 interface instead of tun0. I verified this by running the following tcpdumps at the same time:

interface of gateway 1

$ sudo tcpdump -nnSX port 6969 -i wlp0s20f3
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on wlp0s20f3, link-type EN10MB (Ethernet), snapshot length 262144 bytes
19:42:54.854394 IP 10.16.0.5.49891 > 95.217.167.10.6969: Flags [S], seq 1323415725, win 64240, options [mss 1460,sackOK,TS val 2183656394 ecr 0,nop,wscale 7], length 0
	0x0000:  4500 003c f33e 4000 4006 3685 0a10 0005  E..<.>@[email protected].....
	0x0010:  5fd9 a70a c2e3 1b39 4ee1 b8ad 0000 0000  _......9N.......
	0x0020:  a002 faf0 1127 0000 0204 05b4 0402 080a  .....'..........
	0x0030:  8227 f3ca 0000 0000 0103 0307            .'..........
<some TCP retransmissions>

interface of gateway 2

$ sudo tcpdump -nnSX port 6969 -i tun0
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
<nothing turned up here>

The packet you see is the SYN to establish TCP connection. Weirdly the source IP is 10.16.0.5 which belongs to tun0 interface. Since I explicitly set that in qbittorrent, I would assume it would go through there! It isn't routed however, and just gets

However, when I launch qbittorrent, I see a ton of UDP traffic on the tun0 interface - the DHT stuffs. Clearly this means qbittorrent can access it, but for the announce it isnt!

Additionally, the tracker does receive the SYN, and even responses with a SYN/ACK, though this is lost - my guess is Gateway 1 (my ISP router) , tries to route the SYN/ACK back to 10.16.0.5, realizes it has no idea who that IP belongs to. I confirm this because partly, since I run that tracker:

Here is TCP dump from server if it is of interest
$ sudo tcpdump -nnSX "(src 183.x.x.x and dst port 6969) or (dst 183.x.x.x and src port 6969)"
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
13:20:39.574907 IP 183.x.x.x.49007 > 95.217.167.10.6969: Flags [S], seq 3526928834, win 64240, options [mss 1460,sackOK,TS val 2185921013 ecr 0,nop,wscale 7], length 0
	0x0000:  4500 003c 61ed 4000 3506 52fc b7b2 d23c  E..<[email protected]....<
	0x0010:  5fd9 a70a bf6f 1b39 d238 a9c2 0000 0000  _....o.9.8......
	0x0020:  a002 faf0 6158 0000 0204 05b4 0402 080a  ....aX..........
	0x0030:  824a 81f5 0000 0000 0103 0307            .J..........
13:20:39.574934 IP 95.217.167.10.6969 > 183.x.x.x.49007: Flags [S.], seq 1979577045, ack 3526928835, win 65160, options [mss 1460,sackOK,TS val 1634788240 ecr 2185921013,nop,wscale 7], length 0
	0x0000:  4500 003c 0000 4000 4006 a9e9 5fd9 a70a  E..<..@.@..._...
	0x0010:  b7b2 d23c 1b39 bf6f 75fd f2d5 d238 a9c3  ...<.9.ou....8..
	0x0020:  a012 fe88 9101 0000 0204 05b4 0402 080a  ................
	0x0030:  6170 e390 824a 81f5 0103 0307            ap...J......

This means the tracker will see my real IP, though since I don't complete the TCP handshake (because of broken routing), I dont actually make the HTTP announce. To confirm - I have configured the interface in qBit, and that interface passes traffic over gateway 2 (verified via the previous cURL)

(If I do a manual curl to the tracker, and source IP is 192.x.x.x, then I can complete the handshake + HTTP req).

I tried to poke around libtorrent source, but C/C++ programming is not my strong suit. From a networking / TCP/IP point of view, I do believe this is a bug however.

Lastly, I was actually trying to replicate a bug I faced in windows qbit (different version), where similar behavior would occur, and the handshake would complete, resulting in the tracker registering my real IP!!!. Will make a separate issue for it.

@AllSeeingEyeTolledEweSew
Copy link
Contributor

This is, unfortunately, the way the Linux networking stack works.

In the normal case, Linux routes packets based only on destination IP. You have multiple default routes, and your wifi interface route has a lower metric, so that will be used. Your source ip / bind address is disregarded.

Routing based on information other than the destination IP is called "policy routing". This is managed with ip rule, and usually needs coordination with iptables and NetworkManager-or-similar to do anything useful.

I'm actually surprised curl works the way you describe. Looking at its source, it looks like curl --interface uses setsockopt(SOL_BINDTODEVICE), which I was unaware of. AFAIK this is the only way to get policy routing behavior, without policy routing. However this call is Linux-specific, and I don't think many other programs have ever used it.

Personally, I do bittorrent-over-VPN on Linux by using docker. In one container I run my vpn client, configured to route all traffic. It doesn't route all traffic on the host, just the traffic in its container. I then run my torrent client using --network container:my_vpn_container. It's a simple solution that works for any VPN client and any torrent client.

All that said ...

@arvidn It might be nice to have libtorrent do setsockopt(SOL_BINDTODEVICE) on Linux. This use case is probably common, and this call is by far the simplest way to achieve it.

@ckcr4lyf
Copy link
Author

ckcr4lyf commented Mar 11, 2022

@AllSeeingEyeTolledEweSew thanks for the response. The docker idea is a good one, can force traffic while still having my "normal" internet as well, will play around with it + port forwarding!

Regarding the local interface, it seems that Linux indeed treats this differently - I was able to make it work on Windows (kinda). Very interestingly, ndoe.js also does not set that option it seems - when I was debugging this issue on windows, I wrote a node script to make a TCP connection from a local address:

import net from 'net';

const s = net.createConnection({
    localAddress: '10.16.0.5',
    host: 'a_server_i_own',
    port: 58384
});
On linux, when my route via 10.6.0.1 has a higher metric, it tries to be sent out from ethernet
raghu@arch:~$ sudo tcpdump -nnSX -i enp0s31f6 port 58384
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on enp0s31f6, link-type EN10MB (Ethernet), snapshot length 262144 bytes
14:25:58.662663 IP 10.16.0.5.39723 > REDACTED.58384: Flags [S], seq 955769022, win 64240, options [mss 1460,sackOK,TS val 1034859309 ecr 0,nop,wscale 7], length 0
	0x0000:  4500 003c ce3c 4000 4006 6178 0a10 0005  E..<.<@[email protected]....
	0x0010:  2ee8 d20a 9b2b e410 38f7 e0be 0000 0000  .....+..8.......
	0x0020:  a002 faf0 0b36 0000 0204 05b4 0402 080a  .....6..........
	0x0030:  3dae b32d 0000 0000 0103 0307            =..-........
_this also happens when there is NO route via the tun0 interface (manually delete it)_

On linux, when my route via 10.6.0.1 has the lowest metric (first choice), it goes via tun0 correctly
raghu@arch:~$ sudo tcpdump -nnSX -i tun0 port 58384
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
14:28:34.199905 IP 10.16.0.5.46527 > REDACTED.58384: Flags [S], seq 733732749, win 64240, options [mss 1460,sackOK,TS val 1035014847 ecr 0,nop,wscale 7], length 0
	0x0000:  4500 003c 0edd 4000 4006 20d8 0a10 0005  E..<..@.@.......
	0x0010:  2ee8 d20a b5bf e410 2bbb df8d 0000 0000  ........+.......
	0x0020:  a002 faf0 4c7e 0000 0204 05b4 0402 080a  ....L~..........
	0x0030:  3db1 12bf 0000 0000 0103 0307            =...........

This behavior aligns with what you mentioned, as when I do a quick search of node.js source for SOL_BINDTODEVICE there are no results.

Interesting: On **WINDOWS** when there is no route, the node script gives an error that there is no route
> node .\index.mjs
node:events:498
      throw er; // Unhandled 'error' event
      ^

Error: connect ENETUNREACH REDACTED:58384
    at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1157:16)
Emitted 'error' event on Socket instance at:
    at emitErrorNT (node:internal/streams/destroy:157:8)
    at emitErrorCloseNT (node:internal/streams/destroy:122:3)
    at processTicksAndRejections (node:internal/process/task_queues:83:21) {
  errno: -4062,
  code: 'ENETUNREACH',
  syscall: 'connect',
  address: 'REDACTED',
  port: 58384
}

I think, if it would not be to hard to add the option so that it does not affect windows users, it would be nice!

@AllSeeingEyeTolledEweSew
Copy link
Contributor

Look like libtorrent actually does do SO_BINDTODEVICE. https://github.com/arvidn/libtorrent/blob/RC_2_0/include/libtorrent/enum_net.hpp#L188

I must have mistyped a grep to look for it before.

So IIUC, you should get desired behavior if you change the libtorrent setting outgoing_interfaces to tun0. I'm not sure what setting is being controlled by the qbittorrent gui.

@ckcr4lyf
Copy link
Author

From the qbittorrent source itself, it seems it does create a setting for outgoing interfaces - https://github.com/qbittorrent/qBittorrent/blob/master/src/base/bittorrent/session.cpp#L1501

Though as you say, I do not know if it is being controlled by the GUI (nor is my C++ good enough to find out).

Can you confirm if libtorrent should always use SO_BINDTODEVICE ? I am not able to follow the #if macros etc. well, but if so, then maybe I should open this issue in https://github.com/qbittorrent/qBittorrent/ instead...

@ckcr4lyf
Copy link
Author

ckcr4lyf commented Mar 11, 2022

FYI: Here is an strace the moment I add the tracker to the torrent.

[pid 235222] socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 32
[pid 235222] bind(32, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("10.16.0.5")}, 16) = 0
[pid 235222] connect(32, {sa_family=AF_INET, sin_port=htons(7777), sin_addr=inet_addr("23.23.23.23")}, 16) = -1 EINPROGRESS (Operation now in progress)

I chose a random IP / PORT combo (23.23.23.23:7777) just to track the TCP handshake (well the SYN).

tcp dump again shows that it is being sent out via wrong interface (ethernet)

strace of all socket calls when I launch qbit ``` $ strace -f -e trace=network qbittorrent 2>&1 | grep "socket(" socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0) = 3 [pid 241503] socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0) = 7 [pid 241503] socket(AF_NETLINK, SOCK_DGRAM|SOCK_CLOEXEC, NETLINK_ROUTE) = 9 [pid 241505] socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE) = 12 [pid 241505] socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE) = 13 [pid 241505] socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE) = 13 [pid 241505] socket(AF_INET, SOCK_DGRAM, IPPROTO_IP) = 14 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC, 0) = 18 [pid 241505] socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 13 [pid 241505] socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) = 14 [pid 241510] socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0) = 25 [pid 241503] socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 31 [pid 241503] socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0) = 31 [pid 241505] socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 32 ```

I do not see the option for SO_BINDTODEVICE , anywhere in the call to socket() so yeah, I guess either qbit isnt passing it properly, or libtorrent isn't applying it.

However, the source IP is set in the bind call following socket creation (though I understand, in linux this does not control routing without SO_BINDTODEVICE)

EDIT: Just to reiterate - I am talking about HTTP Tracker Announces here, in case it is just handled differently in libtorrent (ignoring network interface options or something)

@ckcr4lyf
Copy link
Author

ckcr4lyf commented Mar 11, 2022

On latest Deluge

libtorrent-rasterbar 1:2.0.5-1
deluge 1:2.0.5-1

with interface, IP set as:

image

I manual add a fake tracker http://44.44.44.44:7777/xd

strace:

[pid 264287] socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 21
[pid 264287] bind(21, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("10.16.0.5")}, 16) = 0
[pid 264287] connect(21, {sa_family=AF_INET, sin_port=htons(7777), sin_addr=inet_addr("44.44.44.44")}, 16 <unfinished ...>

(note - no SO_BINDTODEVICE)

tcpdump shows packet sent out from ethernet (not tun0) , source IP is "correct" as 10.16.0.5.

$ sudo tcpdump -nnSX -i enp0s31f6 port 7777
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on enp0s31f6, link-type EN10MB (Ethernet), snapshot length 262144 bytes
16:48:52.743064 IP 10.16.0.5.60127 > 44.44.44.44.7777: Flags [S], seq 3570900446, win 64240, options [mss 1460,sackOK,TS val 3279814717 ecr 0,nop,wscale 7], length 0
	0x0000:  4500 003c d0a0 4000 4006 07af 0a10 0005  E..<..@.@.......
	0x0010:  2c2c 2c2c eadf 1e61 d4d7 9dde 0000 0000  ,,,,...a........
	0x0020:  a002 faf0 629b 0000 0204 05b4 0402 080a  ....b...........
	0x0030:  c37e 003d 0000 0000 0103 0307            .~.=........

@ghost
Copy link

ghost commented Mar 14, 2022

What happens if you select only the interface in qbit and not optional ip.

@ckcr4lyf
Copy link
Author

ckcr4lyf commented Mar 15, 2022

What happens if you select only the interface in qbit and not optional ip.

Just tried, this is what I get:

[pid 2306566] socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 34
[pid 2306566] bind(34, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("10.16.0.5")}, 16) = 0
[pid 2306566] socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP) = 36
[pid 2306566] bind(36, {sa_family=AF_INET6, sin6_port=htons(0), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "fe80::150d:aa65:ebb6:4fe6", &sin6_addr), sin6_scope_id=if_nametoindex("tun0")}, 28) = 0
[pid 2306566] connect(34, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("12.23.34.45")}, 16) = -1 EINPROGRESS (Operation now in progress)

On adding a fake tracker http://12.23.34.45:5555/lll . There is an IPv6 socket as well, but i dont have ipv6 on any interface, just loopback i guess.

SYN gets sent out via ethernet again:

$ sudo tcpdump -nnSX port 5555 -i enp0s31f6
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on enp0s31f6, link-type EN10MB (Ethernet), snapshot length 262144 bytes
14:53:50.610856 IP 10.16.0.5.43705 > 12.23.34.45.5555: Flags [S], seq 3614554469, win 64240, options [mss 1460,sackOK,TS val 3107862242 ecr 0,nop,wscale 7], length 0
	0x0000:  4500 003c d4d9 4000 4006 2d8a 0a10 0005  E..<..@[email protected].....
	0x0010:  0c17 222d aab9 15b3 d771 b965 0000 0000  .."-.....q.e....
	0x0020:  a002 faf0 3887 0000 0204 05b4 0402 080a  ....8...........
	0x0030:  b93e 36e2 0000 0000 0103 0307            .>6.........

Again, with "correct" source IP, wrong interface.

The setting:
image

@ckcr4lyf

This comment was marked as off-topic.

@ckcr4lyf
Copy link
Author

Look like libtorrent actually does do SO_BINDTODEVICE. https://github.com/arvidn/libtorrent/blob/RC_2_0/include/libtorrent/enum_net.hpp#L188

I must have mistyped a grep to look for it before.

So IIUC, you should get desired behavior if you change the libtorrent setting outgoing_interfaces to tun0. I'm not sure what setting is being controlled by the qbittorrent gui.

I should have done this before, but if I add a peer manually , then it does do so:

[pid 2312139] socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 34
[pid 2312139] setsockopt(34, SOL_SOCKET, SO_BINDTODEVICE, "tun0\0", 5) = 0
[pid 2312139] bind(34, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
[pid 2312139] setsockopt(34, SOL_TCP, TCP_NOTSENT_LOWAT, [16384], 4) = 0
[pid 2312139] connect(34, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("77.66.55.44")}, 16) = -1 EINPROGRESS (Operation now in progress)

Note the [pid 2312139] setsockopt(34, SOL_SOCKET, SO_BINDTODEVICE, "tun0\0", 5) = 0

tcpdump shows SYN from correct interface. So it seems the issue is that HTTP announce does not respect the interface setting. @arvidn I wonder if this is a conscious decision? I remember reading an issue where you'd mentioned that you didn't see a problem with the announce going from an IP which was connectable (#4803 (comment))

But in my case, I would not be able to accept incoming connections via ethernet, since qbit is bound (listening) on tun0. (So when the tracker gives my gateway IP of ethernet to other peers they wont be able to connect)

@ckcr4lyf ckcr4lyf changed the title Linux: Outgoing connection made on wrong interface (and "leaks" real IP) Linux: SO_BINDTODEVICE not used for http tracker announce ("leaks" real IP) Mar 15, 2022
@arvidn
Copy link
Owner

arvidn commented Mar 15, 2022

the intention is that the tracker announce is made from all the interfaces listed in listen_interfaces, which also have listen sockets associated with them.

the outgoing_interfaces is a bit of a fringe setting. Its intention is to load balance outgoing peer connections across multiple interfaces. It won't affect tracker announces.

@ckcr4lyf
Copy link
Author

the intention is that the tracker announce is made from all the interfaces listed in listen_interfaces, which also have listen sockets associated with them.

In my (admittedly unique) case, I would argue it is made from the IP , since for it to be made from the interface it would explicitly provide the SO_BINDTODEVICE option. qbit is only listening on 10.16.0.5 (which is tun0), and while the bind() syscall does indicate that the address is being selected correctly, the lack of SO_BINDTODEVICE results in the incorrect interface (rather Linux's default priority based on purely destination).

Would you consider adding that option (SO_BINDTODEVICE) for the HTTP announce? Since otherwise in this case of dual gateway , it is not possible to correctly announce with the lower priority (in routing table) interface. That is to say the tracker will always get the SYN from the "wrong" gateway, and as a result, never complete the TCP handshake.

@arvidn
Copy link
Owner

arvidn commented Mar 15, 2022

I think what's going on is that libtorrent (may) only use SO_BINDTODEVICE when listen_interfaces is configured to a specific interface, rather than an IP.

Could you try to specify tun0 as listen_interfaces rather than the IP?

@ckcr4lyf
Copy link
Author

Althought I am not using libtorrent directly, I do think the following settings in the torrent clients should be equivalent.

qBittorrent

image

strace for manual add HTTP tracker

[pid 2415723] socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 34
[pid 2415723] bind(34, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("10.16.0.5")}, 16) = 0
[pid 2415723] socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP) = 36
[pid 2415723] bind(36, {sa_family=AF_INET6, sin6_port=htons(0), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "fe80::4bf1:c0fb:88f2:9dea", &sin6_addr), sin6_scope_id=if_nametoindex("tun0")}, 28) = 0
[pid 2415723] connect(34, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("23.34.32.34")}, 16) = -1 EINPROGRESS (Operation now in progress)

(tcp dump shows syn sent out via incorrect wifi interface)

strace for manual add UDP tracker

[pid 2415723] sendto(14, "\0\0\4\27'\20\31\200\0\0\0\0|W\20\36", 16, MSG_NOSIGNAL, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("99.99.99.99")}, 16) = 16

tcpdump shows packet sent from "CORRECT" tun0 interface

strace for adding manual peer:

[pid 2415723] sendto(14, "A\0\371\r\305k7\322\0\0\0\0\0\0\0\0B\204\0\0", 20, MSG_NOSIGNAL, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("34.34.34.34")}, 16) = 20
[... some useless stuff ...]
[pid 2415723] socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 36
[pid 2415723] setsockopt(36, SOL_SOCKET, SO_BINDTODEVICE, "tun0\0", 5) = 0
[pid 2415723] bind(36, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
[pid 2415723] setsockopt(36, SOL_TCP, TCP_NOTSENT_LOWAT, [16384], 4) = 0
[pid 2415723] connect(36, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("34.34.34.34")}, 16) = -1 EINPROGRESS (Operation now in progress)
[pid 2415723] getsockname(36, {sa_family=AF_INET, sin_port=htons(38985), sin_addr=inet_addr("10.16.0.5")}, [28 => 16]) = 0

The initial UDP packet (I'm guessing for uTP connection) was sent out via tun0, and the subsequent TCP SYN also sent out via tun0.

Deluge

image

strace for adding HTTP tracker;

[pid 2420320] socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, IPPROTO_IP) = 31
[pid 2420320] getsockopt(31, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
[pid 2420320] connect(31, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("33.33.33.33")}, 16) = -1 EINPROGRESS (Operation now in progress)
[pid 2420320] shutdown(31, SHUT_RDWR)   = 0
[pid 2420383] socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 31
[pid 2420383] bind(31, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("192.168.128.131")}, 16) = 0
[pid 2420383] socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 37
[pid 2420383] bind(37, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("10.16.0.5")}, 16) = 0
[pid 2420383] connect(31, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("33.33.33.33")}, 16) = -1 EINPROGRESS (Operation now in progress)
[pid 2420383] connect(37, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("33.33.33.33")}, 16) = -1 EINPROGRESS (Operation now in progress)

tcp dump shows SYN sent via wifi interface (both 192.x source and 10.x source)

strace for adding UDP tracker:

[pid 2420320] socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, IPPROTO_IP) = 31
[pid 2420320] getsockopt(31, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
[pid 2420320] connect(31, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("77.77.77.77")}, 16) = -1 EINPROGRESS (Operation now in progress)
[pid 2420320] shutdown(31, SHUT_RDWR)   = 0
[pid 2420383] sendto(34, "\0\0\4\27'\20\31\200\0\0\0\0\374\336@\254", 16, MSG_NOSIGNAL, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("77.77.77.77")}, 16) = 16
[pid 2420383] sendto(36, "\0\0\4\27'\20\31\200\0\0\0\0.f\224\336", 16, MSG_NOSIGNAL, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("77.77.77.77")}, 16) = 16

tcpdump shows the 192.x source went out via wifi, the 10.x source went out via tun0

strace for adding peer:

[pid 2420383] sendto(34, "A\0(L\370\201BO\0\0\0\0\0\0\0\0x\351\0\0", 20, MSG_NOSIGNAL, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("12.12.12.13")}, 16) = 20
[... some useless stuff ...]
[pid 2420383] getsockname(34, {sa_family=AF_INET, sin_port=htons(54468), sin_addr=inet_addr("192.168.128.131")}, [28 => 16]) = 0
[pid 2420383] socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 31
[pid 2420383] setsockopt(31, SOL_SOCKET, SO_BINDTODEVICE, "tun0\0", 5) = 0
[pid 2420383] bind(31, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
[pid 2420383] setsockopt(31, SOL_TCP, TCP_NOTSENT_LOWAT, [16384], 4) = 0
[pid 2420383] connect(31, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("12.12.12.13")}, 16) = -1 EINPROGRESS (Operation now in progress)

Again, the first seems to be a uTP attempt, tcpdump shows src was 192.x and the UDP packet sent out via wifi. The subsequent TCP SYN was sent correctly vis tun0 , probably due to the SO_BINDTODEVICE I'm guessing.

Again, I do not know exactly the internals of qbit/deluge with libtorrent, but I would think that when I change settings they would update it in libtorrent, an SO_BINDTODEVICE is being used for peers, but for trackers (HTTP) it is not, and the default linux kernel routing without it results in the "wrong" interface. If there are any other tests or such I can try let me know!

@ckcr4lyf
Copy link
Author

If anyone comes across this, I am using the following workaround on linux as a workaround:

ip route add table 47 to 10.16.0.0/24 dev tun1
ip route add table 47 to default via 10.16.0.1 dev tun1
ip rule add from 10.16.0.0/24 table 47 priority 47

i.e. Policy Based routing. Since it just routes all based on source address, it is a bit simpler than application level policy routing (tagging packets and whatnot).

I used https://casualhacking.io/blog/2010/12/22/route-based-on-source-ip-address-linux-bsd.html as a reference

@arvidn
Copy link
Owner

arvidn commented May 9, 2022

Again, I do not know exactly the internals of qbit/deluge with libtorrent, but I would think that when I change settings they would update it in libtorrent, an SO_BINDTODEVICE is being used for peers, but for trackers (HTTP) it is not, and the default linux kernel routing without it results in the "wrong" interface. If there are any other tests or such I can try let me know!

From your screenshot, your "Incoming address" is blank. Assuming this corresponds to the libtorrent listen_interfaces setting, the behavior you see is expected.

The outgoing interface is meant to load balance between multiple interfaces your outgoing peer connections

The purpose of the outgoing tracker connection is to make other peers know how to connect to you. That's why the tracker connections needs to be made from the same interface that your accepting incoming connections on. i.e. it's not taking outgoing_interfaces into account.

Have you tried setting your "Incoming Address" to tun0?

@ckcr4lyf
Copy link
Author

ckcr4lyf commented May 9, 2022

I have tried that as well -> #6772 (comment)

The tcpdump + strace from that attempt shows no SO_BINDTODEVICE , and thus being sent from wrong interface (correct source IP). Correct me if this is not what you meant by the incoming address , it seems I need to specify an IP address, so I cannot put tun0 directly

@arvidn
Copy link
Owner

arvidn commented May 9, 2022

Correct me if this is not what you meant by the incoming address , it seems I need to specify an IP address, so I cannot put tun0 directly

That doesn't sound right. I was assuming "Incoming Address" controlled the listen_interfaces setting in libtorrent.

Digging a little bit in deluge, it looks like they always configure two listen interfaces, always the same device/IP but with different ports. Sometimes even with the same port. The internal setting in deluge is called listen_interface.

this is where it duplicates the listen interfaces. It looks like a bug.

Anyway, regarding you setting an interface name as your listen address, the string appears to be checked to be a valid interface, here by this function.

is it possible your tun0 interface isn't up by the time Deluge performs this check?

this seems to suggest it is the "incoming address" that's controlling listen_interfaces.

@ghost
Copy link

ghost commented May 9, 2022

I think he mentioned of having same issue with qbit as well on the OP.

@ckcr4lyf
Copy link
Author

If I set "Incoming Address" to tun0 string, deluge will clear it. Then if I add an HTTP tracker, all packets, from three source IP sent from lowest metric interface (I have ethernet 192.x.x.x, wifi 172.x.x.x, and VPN 10.x.x.x active):

image

If I set it to 10.16.0.5 , only that source IP is used, but still ethernet interface:

image

But when I add a peer , it is ok (first UDP must be a uTP attempt):

image

this is because for adding a peer, SO_BINDTODEVICE is used:

[pid 2329078] socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 32
[pid 2329078] setsockopt(32, SOL_SOCKET, SO_BINDTODEVICE, "tun0\0", 5) = 0
[pid 2329078] bind(32, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
[pid 2329078] setsockopt(32, SOL_TCP, TCP_NOTSENT_LOWAT, [16384], 4) = 0
[pid 2329078] connect(32, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("66.66.66.66")}, 16) = -1 EINPROGRESS (Operation now in progress)

However, since you mention it is a fringe setting, it is quite possible deluge/qbit just dont behave as I expect them to, which is unfortunate as it results in the tracker registering the wrong IP.

Regardless, I would not ask of you to debug clients' implementation of libtorrent, perhaps I should open an issue in there saying they are not calling the underlying function correctly?

@arvidn
Copy link
Owner

arvidn commented May 10, 2022

I believe this is an oversight. UDP tracker announces use a socket that has been SO_BINDTODEVICE correctly, but it doesn't look like HTTP trackers do. I will need to fix this.

@arvidn arvidn added this to the 1.2.17 milestone May 10, 2022
@Motophan
Copy link

Motophan commented May 11, 2022

Just my two cents, but on (qbittorrent 4.4.1) a tracker I happen to announce w/ several layers of internet connectivity layers.
I was inside the peer list with 172.0.0.4 (docker bridge IP), 123.123.123.123 IP (Pubic IP 1), 123.123.123.124 (Public IP 2), fe80::4 (docker ipv6 bridge), and 2001:0db8:85a3:0000:0000:8a2e:0370:7334 (Public IPv6)

If I ran a vpn on the docker bridge it would, I expect, add another level of attenuation.

The tracker in question would accept my announcement, but not pass on the internal IP's to peers but would log them. I am not sure how to IP inspect a announcement, however.

@arvidn
Copy link
Owner

arvidn commented May 11, 2022

I believe this fixes it: #6860

I made a clean-up commit right before that one on RC_1_2 which would be necessary to apply this patch. It would probably be easiest to use the commit instead, if anyone wants to try it out.

@ckcr4lyf
Copy link
Author

Thanks a lot arvidn. I will try compiling it and then compile qbit / deluge against it!

@ckcr4lyf
Copy link
Author

I believe this fixes it: #6860

I made a clean-up commit right before that one on RC_1_2 which would be necessary to apply this patch. It would probably be easiest to use the commit instead, if anyone wants to try it out.

Awesome, I can confirm it works! Adding an http tracker now. strace of socket call:

[pid 3424090] socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 34
[pid 3424090] setsockopt(34, SOL_SOCKET, SO_BINDTODEVICE, "tun0\0", 5) = 0
[pid 3424090] bind(34, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("10.16.0.5")}, 16) = 0
[pid 3424090] connect(34, {sa_family=AF_INET, sin_port=htons(5555), sin_addr=inet_addr("3.3.3.3")}, 16) = -1 EINPROGRESS (Operation now in progress)

Correct SO_BINDTODEVICE , and tcpdump shows correct interface:

$ sudo tcpdump -nnSX port 5555 -i tun0
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
12:35:41.041935 IP 10.16.0.5.50221 > 3.3.3.3.5555: Flags [S], seq 1992976676, win 64240, options [mss 1460,sackOK,TS val 215313631 ecr 0,nop,wscale 7], length 0
	0x0000:  4500 003c ea56 4000 4006 404b 0a10 0005  E..<.V@.@.@K....
	0x0010:  0303 0303 c42d 15b3 76ca 6924 0000 0000  .....-..v.i$....
	0x0020:  a002 faf0 0971 0000 0204 05b4 0402 080a  .....q..........
	0x0030:  0cd5 6cdf 0000 0000 0103 0307
For those curious, this is how I managed to compile it (was a pain tbh...):

Clone libtorrent, checkout RC_1_2 and merge in the commit:

git clone https://github.com/arvidn/libtorrent.git
cd libtorrent
git checkout RC_1_2
git merge tracker-bind-to-device

Build libtorrent with python (had to install cmake, ninja)

mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=14 -DPython3_EXECUTABLE=/home/raghu/.pyenv/shims/python  -D python-bindings=ON  -G Ninja ..

(it could not find my python3 otherwise)

Then, clone and install deluge

git clone https://github.com/deluge-torrent/deluge.git
cd deluge
python setup.py build
sudo python setup.py install

The latest deluge with LT 1.2 has some other errors in logs which I ignored for this test

@arvidn
Copy link
Owner

arvidn commented May 12, 2022

thanks for testing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants