Fixed clientside_mark and client port logging in TPROXY mode#150
Fixed clientside_mark and client port logging in TPROXY mode#150yadij merged 2 commits intosquid-cache:masterfrom gozzy:clientside_mark_tproxy
Conversation
|
Can one of the admins verify this patch? |
|
Actually, the initial behavior looks strange: why change client's port? If we do really transparent proxying, we should bind client's "address:port" pair to a socket. Instead the port was changed for some reasons and squid->server connection used a random port. I have not seen any recommendations to choose arbitrary port in TPROXY mode and the change made in this commit does not seem to break anything, at least on our setup. |
|
The "reasons" for setting port to 0 on TPROXY traffic is that TCP/IP considers each unique set of src-IP:port, dst-IP:port to be a connection and that is enforced by modern kernels and/or NIC device drivers - attempts to reuse the same IP:port for anything result in "address already in use" errors from the networking layers. Since dst-IP:port is identical for both the client->Squid and Squid->server connections, and the TPROXY is all about spoofing the src-IP to also be identical on both. That leaves the src-port as the only possible way for TCP/IP to avoid dropping one of the connections as invalid or routing packets on it to the wrong recipent. Setting port to 0 in the records of the client connection is not right, but at the time TPROXY was implemented there was no better place. Things have changed since then in how Squid connection state is managed. It may be time to move the 0 assignment to a later timing, but I have not checked where that may be. |
|
Maybe it could be moved to Comm::TcpAcceptor::acceptOne? Just right after getting nfmark. |
|
IMHO, assuming Squid cares about the actual client port number (e.g., for logging it and sending it to external ACLs), the only logical place to zero that port is when passing a copy of the actual client address to the bind(2) call wrapper (or equivalent). That approach preserves the true client port number for all high-level Squid purposes while making low-level OS IP stack happy. |
|
I am still not sure if zeroizing in Ip::Intercept::TproxyTransparent() is necessary. At the end of FwdState::connectStart() I see this:
Also, in squid's log there is something like this:
Local address is logged without a port. If I understand correctly, the address is written without port if the port is random. Currently it's hard for me to trace everything that happens but there definetely should be a reason why everything works without 'newConn->remote.port(0)' :) Could it be that zeroizing is also done in connectStart? |
Good point, although I do not see a similar reset in tunnel.cc (which duplicates a lot of FwdState code/purpose) so be careful about relying on the above code alone. Again, none of those existing random places feel logical for a local port reset if a zero local port is always needed for opening a TCP connection from Squid. |
|
I think the right place in current code is getOutgoingAddress() in FwdState.cc (and its equivalent in tunnel.cc). There is already a section for tproxy there which should set conn->local.port(0) just before setting the flags. This will need testing of TPROXY intercepted port 80, 443, 25 (FTP) traffic of course, and also an intercepted CONNECT message intended for another proxy (for the tunnel.cc code), and also ICAP traffic. If any of those results in that "Address already in use" or similar errors the source of its outbound IP:port will need to be tracked down and fixed - if possible by making it use the getOutgoingAddress() function. |
|
tunnel.cc seems to use PeerSelector, which in it's turn uses getOutgoingAddress(), if I'm not mistaken. There's a following chain: At the moment I think that adding |
|
Updated the PR: zeroizing moved to FwdState. Things seem to work so far... We've checked the following cases (HTTP, HTTPS and FTP):
I don't completely understand what should be checked about ICAP: tunneling or regular squid+ICAP? |
|
As far as I know; testing that ICAP connections use the right (client) IP when the HTTP is received over a TPROXY port. The c-icap test echo service should be okay for the test. |
|
You mean we should enable 'adaptation_send_client_ip' and check c-icap's logs? |
|
c-icap gets correct client's address in X-Client-IP header. |
Why not all the way down to comm_openex()? In other words, do we have any use cases where a COMM_BIND TCP address in comm_openex() should not use a zero source port? |
|
@rousskov do you mean something like this: ? If yes, does this need to be remarked somehow? |
|
We need to come to an agreement :) |
Not exactly because
Yes, and the key question to answer here, AFAICT, is whether there are use cases where a COMM_BIND TCP address in comm_openex() should not use a zero source port. If there are no such use cases, then we should zero the port at the lowest level possible, to avoid screwing up logging/ACLs/marking (the bug this PR is fixing) and to avoid mistakes where the port is not zero by accident (past bugs). |
|
So we have two options: zeroize port either in getOutgoingAddress or in comm_openex. The first option is proven to work and resets port only when it's really needed. The second option is probably better because no connection is created without comm_openex(), but there're some caveats. @yadij any thoughts? |
because all the debugging, ACLs etc applied between getOutgoingAddr() and comm_openex() will be saying that the port about to be opened will be the one the client used, when it will actually be OS assigned. |
The above problems do not imply we should not zero the port in comm_openex() AFAICT. You mentioned two very different problems:
|
Those two options are not mutually exclusive:
|
|
Maybe you are right and comm_openex should take care of a source port too... However, does it really need to be included in this patch? It looks like a separate problem to me. Also, I don't know if none of TCP connections made by squid requires source port. |
No, I do not think it has to be included. We can fix getOutgoingAddress() alone because its breakage (i.e., returning garbage ports) is independent from comm_openex() API/robustness issues. |
rousskov
left a comment
There was a problem hiding this comment.
I just have a small (but important) polishing request for this fix.
src/FwdState.cc
Outdated
| // some flags need setting on the socket to use this address | ||
| conn->flags |= COMM_DOBIND; | ||
| conn->flags |= COMM_TRANSPARENT; | ||
| conn->local.port(0); // allow random outgoing port to prevent address clashes |
There was a problem hiding this comment.
Please move this line up, right after the local assignment, without an empty line in-between: We are fixing that assignment, and the local value remains wrong until we reset the port. We should keep this fix as close to the assignment it is fixing as possible...
This assignment-fixing logic is repeated in many places, including ClientRequestContext::clientAccessCheck(), ftpOpenListenSocket(), Eui::Eui48::lookup(), IcmpSquid::Recv(), Ftp::Server::listenForDataConnection(), and Ftp::Server::createDataConnection(). We could add and use a dedicated Ip::Address method:
/// makes us the same as addr except for a possibly different port
void reset(const Address &addr, const int port);
but that method does not help avoid similar bugs because buggy code can still mindlessly copy the whole Ip::Address using other public methods and operators. Thus, I doubt we should add this or a similar method. On the other hand, using reset() would make the code a little cleaner when there are two if/else address assignments (as there are in our case). Your call.
src/FwdState.cc
Outdated
| // some flags need setting on the socket to use this address | ||
| conn->flags |= COMM_DOBIND; | ||
| conn->flags |= COMM_TRANSPARENT; | ||
| conn->local.port(0); // allow random outgoing port to prevent address clashes |
There was a problem hiding this comment.
Please s/allow random outgoing/let OS pick the source/ in the comment.
|
I adjusted the future commit message (i.e., PR title/description), twice, to better reflect the scope of the problem (and formatted them to satisfy commit message character limits that will be eventually enforced by the auto-merging bot). Please adjust further if needed. |
The clientside_mark ACL was not working with TPROXY because a conntrack query could not find connmark without a true client port. Ip::Intercept::Lookup() must return true client address, but its TproxyTransparent() component was reseting the client port. We should use zero port when we compute the source address for the Squid-to-peer connection instead.
|
Done. |
|
OK to test |
|
Looks good to me. |
…he#150) The clientside_mark ACL was not working with TPROXY because a conntrack query could not find connmark without a true client port. Ip::Intercept::Lookup() must return true client address, but its TproxyTransparent() component was reseting the client port. We should use zero port when we compute the source address for the Squid-to-peer connection instead.
The clientside_mark ACL was not working with TPROXY because a conntrack query could not find connmark without a true client port. Ip::Intercept::Lookup() must return true client address, but its TproxyTransparent() component was reseting the client port. We should use zero port when we compute the source address for the Squid-to-peer connection instead.
…he#150) The clientside_mark ACL was not working with TPROXY because a conntrack query could not find connmark without a true client port. Ip::Intercept::Lookup() must return true client address, but its TproxyTransparent() component was reseting the client port. We should use zero port when we compute the source address for the Squid-to-peer connection instead.
The clientside_mark ACL was not working with TPROXY because a conntrack query could not find connmark without a true client port. Ip::Intercept::Lookup() must return true client address, but its TproxyTransparent() component was reseting the client port. We should use zero port when we compute the source address for the Squid-to-peer connection instead.
The clientside_mark ACL was not working with TPROXY because a
conntrack query could not find connmark without a true client port.
Ip::Intercept::Lookup() must return true client address, but its
TproxyTransparent() component was reseting the client port. We should
use zero port when we compute the source address for the Squid-to-peer
connection instead.