-
Notifications
You must be signed in to change notification settings - Fork 0
Home
The written code implements UDP support in ProxyChains-NG. It allows redirecting UDP packets from a client program to SOCKS5 proxies (SOCKS4a and HTTP proxies are not supported). It implements support for the SOCKS5 UDP_ASSOCIATE
command (see RFC1928) that allows opening UDP relays on some SOCKS5 compatible servers (the ones that also implements UDP_ASSOCIATE
, for instance https://gost.run/en/)
As for TCP, it works by hooking network-related libc functions.
In order to transmit UDP packet through a compatible SOCKS5 proxy server, a client program must follow this workflow:
- Request the opening of a UDP relay port on the proxy server.
- Send its UDP packets encapsulated in a specific SOCKS5 header to the opened relay port.
The UDP relay opening query is done on a TCP connection to the SOCKS port of the server, using the UDP_ASSOCIATE
command (https://datatracker.ietf.org/doc/html/rfc1928#section-4).
The server replies with the address and port of the opened relay, where the client must send the adequately encapsulated UDP packets.
The UDP relay remains opened and keeps relaying packets until this TCP connection terminates.
To send a UDP packet, the client program must encapsulate it in a SOCKS5 header (defined in https://datatracker.ietf.org/doc/html/rfc1928#section-7) containing the final destination address and port. It must then send this encapsulated packet to the relay address and port previously obtained on the TCP connection.
ProxyChains make non SOCKS5aware clients compatible with UDP_ASSOCIATE
by hooking some network-related library functions and modifying the data transmitted on network connections.
In the following:
- ProxyChains refers to the library code containing the functions hooks
- client program or client refers to the program being preloaded with ProxyChains dynamic library
- proxy server refers to a SOCKS5 server that implements
UDP_ASSOCIATE
For each UDP socket opened by the client program, a new chain of UDP relays is opened and maintained by ProxyChains on the different proxy servers declared in the ProxyChains configuration file. UDP packets sent by the client program are encapsulated adequately to be sent through the chain of relays and towards their final destination. When the UDP socket is closed by the client program, ProxyChains closes the chain of relay by terminating the TCP connection associated with each relay.
All the data associated with a UDP relay (TCP socket used to send the UDP_ASSOCIATE
command, associated SOCKS5 proxy data, address and port of the opened relay, etc.) is stored in a udp_relay_node
structure. These structures can be linked together as a chained list, representing a chain of relays opened for a client UDP socket.
All the data associated with a UDP relay chain (the client UDP socket for which the relays chain was opened, the head of the chained list representing the chain of relays, the default final destination if the client UDP socket is "connected") is stored in a udp_relay_chain
structure.
Finally, the list of all currently opened relay chains (one for each client program's UDP socket) is maintained in a chained list of udp_relay_chain
structures whose head and tail are referenced in a udp_relay_chain_list
structure (in the relay_chains
global variable).
The relay_chains_mutex
mutex is associated to the relay_chains
global variable and allows preventing race condition issues when the client program uses multiple threads.
The support of UDP relaying is only available with STRICT_TYPE
chains of SOCKS5_TYPE
proxies. DYNAMIC_TYPE
, ROUND_ROBIN_TYPE
and RANDOM_TYPE
chains are not supported. Thus, it is assumed that the first proxy of the chain is reachable from the host where the client program runs, that the second proxy is reachable by the first one, the third one by the second one, etc.
When opening UDP relay chains, a TCP connection is opened with each proxy server of the chain to send the UDP_ASSOCIATE
command and manage the relay. The TCP connection to proxy server N is proxified through the chain of proxy servers 1 to N-1 (using the SOCKS5 CONNECT
command).
Once all the relays of a chain are opened, client program UDP packets can be sent through. Each packet is encapsulated in successive SOCKS5 headers: the outermost one will be read and decapsulated by the first proxy of the chain, and contains the address and port of the second proxy UDP relay. The innermost one will be read and decapsulated by the last proxy server of the chain and contains the address and port of the final destination.
The ProxyChains workflow with TCP connections is relatively easy as TCP is a connected and stateful protocol. For TCP sockets, network-related functions are most often called in the same order by the client program:
-
socket()
to obtain a socket file descriptor -
connect()
to connect the socket to its final destination -
send()
andrecv()
to send and receive data on the socket -
close()
to terminate the connection and close the file descriptor
To send or receive data on a TCP socket, the latter must be connected to a remote peer with a call to connect()
containing the address and port of the peer. Once connected, send()
and recv()
calls send and receive data to and from the connected peer only. Thus, ProxyChains just needs to hook the connect()
call. Instead of connecting to the final destination directly, it connects the socket to the first proxy of the chain, uses the SOCKS CONNECT
command to tell the proxy server to connect to the second proxy server, and keeps using CONNECT
on the successive proxies until it reaches the final destination, through the whole chain of proxies.
Once this process is done, ProxyChains does not need to hook send()
and recv()
functions: when the client program call send()
, data will be sent on the TCP socket to the first proxy server of the chain, that will send it to the second one, which will transfer to the third one et cetera until the data reaches the final destination. When the remote peer sends data, all the proxy servers transfer it back through the chain, and a call to recv()
in the client program allows reading the data.
The UDP workflow is a lot more complicated, as UDP sockets are not connected. A UDP socket can be used to send data without going through a prior connection process to a remote peer. Calling sendto()
, sendmsg()
or sendmmsg()
with the data and destination address as parameters is enough. A single UDP socket can thus be used to send UDP packets to various peers with successive calls to the aforementioned send functions.
connect()
can also be used with UDP sockets, but it does not generate a network handshake as it does with TCP sockets. When connect()
is used on UDP sockets, it associates a peer address with the UDP sockets so that further call to send()
on this socket will be equivalent to calling sendto()
with this peer's address as parameter. Moreover, calls to recv()
, recvfrom()
, poll()
, epoll()
, etc. on a "connected" UDP socket only return if data coming from the associated peer is available (the kernel filters data coming from other peers).
When connect()
or sendto()/sendmsg()/sendmmsg()
are called, ProxyChains checks in the relay_chains
global variable if a relay chain is already opened for the socket file descriptor passed in parameters. If one exists, it is used for the remaining operations. Otherwise, ProxyChains goes through the process of opening a relay chain, and stores all associated data in the structures mentioned in #General.
connect()
calls made by the client program need to be hooked and modified by ProxyChains. Indeed, if these calls are kept unmodified, and the UDP socket is "connected" to a final destination, later calls to recv()
, poll()
, etc. won't return if the data is not coming from the associated final peer. Yet, data is expected to be coming back through the relay chain, and will appear to be coming from the UDP relay opened on the first proxy server of the chain.
Thus, two things must be done when connect()
is called:
- Store the final destination provided in parameters by the client program in order to be able to retrieve it in subsequent calls to
send()
,sendmsg()
, orsendmmsg()
on the same socket file descriptor. Indeed, these calls might not contain any destination address as it assumed that the default one (set with the previousconnect()
call) will be used. In ProxyChains, this destination address is stored in theconnected_peer_addr
member of theudp_relay_chain
structure associated with the relay chain opened for the UDP socket. - Connect the UDP socket to the first relay of the associated chain, so that later calls to
recv()
orpoll()
return when data is received from this relay.
In order for this process to be transparent for the client program, getpeername()
calls must also be hooked to return the address stored in connected_peer_addr
instead of the address of the first relay of the chain. See #getpeername().
All these data sending functions must be hooked by ProxyChains.
write()
calls are hooked and if the provided file descriptor is a socket, ProxyChains calls send()
with 0 flags instead.
The final destination address is either passed in parameters by the client program (for sendto()
, sendmsg()
and sendmmsg()
), or retrieved from the connected_peer_addr
member of the associated udp_relay_chain
structure by calling getpeername()
(for send()
, sendmsg()
and sendmmsg()
).
As explained before, if no relay chain exists for the UDP socket, one is opened.
Once the chain to use is known and opened, the SOCKS UDP headers are generated, and the data provided in parameters by the client program is encapsulated.
The encapsulated data is then sent to the first relay of the chain, and the hook ends.
All these data receiving functions must be hooked by ProxyChains.
read()
calls are hooked and if the provided file descriptor is a socket, ProxyChains calls recv()
with 0 flags instead.(WARNING: As stated in https://man7.org/linux/man-pages/man2/recv.2.html in NOTES, "If a zero-length datagram is pending, read(2) and recv() with a flags argument of zero provide different behavior. In this circumstance, read(2) has no effect (the datagram remains pending), while recv() consumes the pending datagram.")
The relay chain associated with the UDP socket passed in the calls is retrieved by going through the chained list relay_chains
. The data is received by calling the original receiving functions (true_recvfrom
, true_recvmsg
and true_recvmmsg
). This data comes from the first relay of the associated chain and is encapsulated with multiple SOCKS5 UDP headers. These headers are decapsulated one by one and ProxyChains that the data has indeed been sent back through the right chain.
The actual data is copied in the buffer provided by the client program, so is the address of the final destination the data is coming back from.
Socket closing functions are hooked. If a relay chain is associated with the closed socket, it is also closed (control TCP sockets are closed, which closes the opened relays) and associated structures are freed.
Some client programs appear not to use libc close()
function, so uv_close()
is also hooked to ensure wider compatibility.
getpeername()
calls need to be hooked by ProxyChains so that they return the final destination address the UDP socket was associated to in a previous call to connect()
(and not the address of the first relay of the associated chain that the socket is effectively connected to, because of the changes performed in connect()
hooks). See #connect().
Specific flags can be passed in receive and send functions. Hooks forward these flags to the original true_
functions as-is, except for:
-
Send flags:
- MSG_DONTROUTE: this flag is droped
- MSG_MORE: EOPNOTSUPP error is returned
-
Receive flags:
- MSG_TRUNC: this flag is forwarded but if it is set, we return the real length of the packet/datagram even when it was longer than the passed buffer (buf)