network: Conns with the same local/remote address are local#7840
network: Conns with the same local/remote address are local#7840mattklein123 merged 5 commits intoenvoyproxy:masterfrom
Conversation
Before this commit, a connection was considered local if any true:
1. It was Envoy::Network::Address::Type::Pipe
2. Remote address is loopback
3. The pointer to the local address was the same as the pointer to the
remote address (probably not intentional).
Condition 3 was probably intended to call
Envoy::Network::Address::operator==() but because the "==" sign is
between two SharedPtrs, it is actually checking that the pointers are
equal (both are pointing at the same Envoy::Network::Address). There's
an unfortunate aligned glitch in the unit test that obfuscates this.
As implemented, this causes the
listener.FilterChainMatch.ConnectionSourceType LOCAL to not match
connections that I consider local (like connections from "4.4.4.4:1234"
to "4.4.4.4:4321").
I think a better condition 3 would be (and probably close to what was
intended):
3. The local IP address is the same as the remote IP address, the
ports do not have to match.
This commit adjusts Utility::isLocalConnection() and helpers to achieve
that.
First, fix the bug in the unit test where socket.localAddress() on the
mock returned remote_addr instead of local_addr. We didn't have any
test cases that EXPECT_TRUE() for the addresses matching (they all rely
on Pipe or isLoopbackAddress()), so we didn't catch this issue before.
Next would be to change this pointer comparison:
remote_address == socket.localAddress()
to actually invoke Address::operator==():
*remote_address == *socket.localAddress()
I made this change locally and confirmed that we now invoke operator==()
but this still won't handle the case that the IP address matches but the
ports are different ("4.4.4.4:1234" to "4.4.4.4:4321" is local).
Next, add equalsExceptPort() to the Address interface. This will check
that the socket address matches but ignore the port. Implement this for
all the Addresses (IPv4, IPv6, and pipe). Pipes don't have ports, so
equalsExceptPort() is the same as operator==() for pipes;
isLocalConnection() already considered all pipes to be local regardless
of their address.
Now, use equalsExceptPort() for deciding if a socket is locally
connected. This fixes all existing unit tests.
Finally, extend the unit test to also check that we are ignoring the
port (e.g. a socket with local address "4.4.4.4:1234" and remote
"4.4.4.4:4321" is locally connected). Also add one for IPv6 that
doesn't rely on one of the addresses being "::1" to hit the
isLoopbackAddress() check for success.
This fixes envoyproxy#7839
Signed-off-by: Andrew Jenkins <andrew@volunteers.acasi.org>
| testing::NiceMock<Network::MockConnectionSocket> socket; | ||
|
|
||
| EXPECT_CALL(socket, remoteAddress()).WillRepeatedly(testing::ReturnRef(local_addr)); | ||
| EXPECT_CALL(socket, localAddress()).WillRepeatedly(testing::ReturnRef(local_addr)); |
There was a problem hiding this comment.
This is a glitch in the unit tests (returning local_addr for calls to remoteAddress()) that obfuscated the fact that isLocalConnection() was accidentally doing pointer comparisons.
|
@PiotrSikora @silentdai PTAL |
|
This pull request has been automatically marked as stale because it has not had activity in the last 7 days. It will be closed in 7 days if no further activity occurs. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions! |
|
ping @PiotrSikora @silentdai please let me know if there's any more info that would help |
|
Sorry I missed the previous notification! /lgtm |
|
btw if you have 2 ip A and B on one machine, A can talk to B "locally". |
|
Thanks much @silentdai! Leaving whether we consider IP A connecting to IP B as "local" as discussion for another day, I know people who see that different ways. @PiotrSikora - thoughts? |
mattklein123
left a comment
There was a problem hiding this comment.
At a high level this makes sense to me, but I have some code comments. Thank you! (@PiotrSikora I would still like to hear if you have any objections to the meat of this change.)
/wait
include/envoy/network/address.h
Outdated
| virtual ~Instance() = default; | ||
|
|
||
| virtual bool operator==(const Instance& rhs) const PURE; | ||
| virtual bool equalsExceptPort(const Instance& rhs) const PURE; |
There was a problem hiding this comment.
IMO this is an abstraction leak to expose port up to this level. I would remove this interface change.
… IPs Signed-off-by: Andrew Jenkins <andrew@volunteers.acasi.org>
Signed-off-by: Andrew Jenkins <andrew@volunteers.acasi.org>
source/common/network/utility.cc
Outdated
| // while the client is not actually local. Example could be an iptables intercepted | ||
| // connection. However, this is a rare exception and such assumption results in big | ||
| // performance optimization. | ||
| // Before calling getifaddrs, verify the obvious checks. These are local: |
There was a problem hiding this comment.
Updated comment per @mattklein123 's request.
I removed note about corner case as I feel that if something like iptables has made Envoy see the same remote and local IP addresses, then we are expected to treat it as local; it's not a corner case. I'm also fine with putting it back.
source/common/network/utility.cc
Outdated
| *addr, (af_look_up == AF_INET) ? sizeof(sockaddr_in) : sizeof(sockaddr_in6)); | ||
|
|
||
| if (remote_address == local_address) { | ||
| if (*remote_address == *local_address) { |
There was a problem hiding this comment.
This comparison from before is also wrong I believe, as Address::addressFromSockAddr() returns a SharedPtr, so this is a pointer comparison that is never going to be true.
I couldn't get the isLocalAddress tests to fail with or without this change, I am not sure this code is well-exercised (and that's probably hard without a mock for getifaddrs).
Which raises a question for me: should we remove the getifaddrs() code entirely? Unless I'm confused (totally possible :-) ) I don't think anyone ever got a return of true out of this block before. Seems like calling getifaddrs for each isLocalAddress() call could be expensive. And checking if the local and remote IP addresses match on a connected socket you created meets my definition of "local".
There was a problem hiding this comment.
Yeah from looking at the coverage report, this code has no coverage and it is clearly broken as you point out (nice find): https://storage.googleapis.com/envoy-coverage/report-master/coverage/proc/self/cwd/source/common/network/utility.cc.html
Given that it never worked and we have no coverage, I would be inclined to delete all of it until someone wants to bring it back. @envoyproxy/maintainers any thoughts here?
There was a problem hiding this comment.
OK since no one else responded I will make the call to kill this broken code which no one must be using. Let's delete it! Thank you.
mattklein123
left a comment
There was a problem hiding this comment.
Thanks for working on this and fixing this up. A few small comments. Nice work!
/wait
source/common/network/utility.cc
Outdated
| *addr, (af_look_up == AF_INET) ? sizeof(sockaddr_in) : sizeof(sockaddr_in6)); | ||
|
|
||
| if (remote_address == local_address) { | ||
| if (*remote_address == *local_address) { |
There was a problem hiding this comment.
Yeah from looking at the coverage report, this code has no coverage and it is clearly broken as you point out (nice find): https://storage.googleapis.com/envoy-coverage/report-master/coverage/proc/self/cwd/source/common/network/utility.cc.html
Given that it never worked and we have no coverage, I would be inclined to delete all of it until someone wants to bring it back. @envoyproxy/maintainers any thoughts here?
| isLoopbackAddress(*remote_address)) { | ||
| return true; | ||
| } | ||
| const auto local_ip = socket.localAddress()->ip(); |
There was a problem hiding this comment.
I think you can simplify this code by just comparing remote_address->ip()->addressAsString() == local_address->ip()->addressAsString(). If we deeply care about speed, I might go ahead and add an equality comparison on the Ip interface, and then deal with it inside there so we don't duplicate this code elsewhere?
There was a problem hiding this comment.
addressAsString() is fine with me.
Equality comparison on Ip can be done, but I have some hesitation. I think that means adding operator==() PURE to Envoy::Network::Address::Ip, and for my purposes, making it compare IPs but not ports, even though port is a member of the Ipv4/Ipv6 implementation. But, Envoy::Network::Address::Instance would have operator==() PURE that for IPs, compares it but with ports.
If I was a programmer that just showed up, I would be surprised if *address1 == *address2 did compare ports, but *address1->ip() == *address2->ip() did not compare ports, especially since the port is a property of *address1->ip() (so *address1->ip()->port() != *address2->ip()->port()). Basically I think since port is a property of the IP address, it's gotta be reflected in all operator==() up and down the class hierarchy the same.
My complaint though is a style consideration and I'm new here so I'm happy to do it if you're OK with it.
Other options:
- Make the equality comparison not named
operator==(), make it some other method (addressEquals()on the Ip class is a better name thanequalsExceptPort()on Address). - Remove
portfrom the Address. Maybe ports are on some new thing, maybe Sockaddrs, but not Ip. Feels like this is a pretty big change.
I'll update to use addressAsString() so we can take a look. I'm OK with any of these choices though.
There was a problem hiding this comment.
Hmm, I just looked and addressAsString() includes port, so I don't think that is going to work either?
Maybe just factor that code out into a utility function that we can test separately? You could make a function that takes 2 Address::Ip and returns whether they are equal without ports? WDYT?
There was a problem hiding this comment.
Turns out that addressAsString() does not include port, it's only the IP address. address->asString() does include the IP and port. Pushed code that works using addressAsString() (and double-checked locally that if I mangle an address but not a port, the string comparison fails as expected).
Gory details: addressAsString() returns the private member friendly_address_ which is initialized with only the IPv4/IPv6 as a string (excluding port). friendly_name_ includes the IPv4/IPv6 and the port.
envoy/source/common/network/address_impl.cc
Lines 178 to 189 in cb15cc3
So, the code that I just pushed uses addressAsString() and works correctly. I am also not terrified of the performance cost of string comparison especially since we are already always constructing the strings at Ipv4/Ipv6 construction time.
…stion Signed-off-by: Andrew Jenkins <andrew@volunteers.acasi.org>
mattklein123
left a comment
There was a problem hiding this comment.
Thanks for working on this. Lets delete the broken code per discussion and then we are good to go! Thank you!
/wait
source/common/network/utility.cc
Outdated
| *addr, (af_look_up == AF_INET) ? sizeof(sockaddr_in) : sizeof(sockaddr_in6)); | ||
|
|
||
| if (remote_address == local_address) { | ||
| if (*remote_address == *local_address) { |
There was a problem hiding this comment.
OK since no one else responded I will make the call to kill this broken code which no one must be using. Let's delete it! Thank you.
Signed-off-by: Andrew Jenkins <andrew@volunteers.acasi.org>
|
Updated the PR description to describe where we landed. OK, @mattklein123 I believe I addressed all your comments (incl. removing the broken code). @silentdai There are no behavior changes since you reviewed (since it turns out the code at the end of the function was broken). All good to merge once CI passes I think. Thanks for working through this! |
|
@andrewjjenkins @mattklein123 @PiotrSikora how does this work with IP_FREEBIND? Also, what about NAT? I'm wondering if there are any corner conditions that we've not considered with the new assumption. |
|
Before this commit, the address comparisons were accidentally pointer comparisons. So the only things considered local were:
I believe it was intended to also include sockets where the remote IP was the same as the local IP (e.g. After the change, we will now consider local any connection where the source IP and the destination IP match. So in the case of IP_FREEBIND, we will now consider For NAT, it looks like the before implementation wanted to consider if the remote address matched any of the addresses on any of the local interfaces. Again, the implementation accidentally wasn't doing this in reality because of pointer comparisons on top of invoking a comparison operator that includes ports. So pedantically speaking, I feel this isn't a regression in observed behavior: I argue no one ever got that comparison to return true because of the pointer comparison problem. Not pedantically speaking, fair to ask what we should do. Relevant case is I think:
I argue that it is not necessarily local. If the source addr is 1.2.3.4, that means Envoy bound to 1.2.3.4 before Since it's not guaranteed, I'm OK with erring on the side of caution and not declaring these local. Especially since no one was ever getting them marked local before due to bugs. Is that the case you're thinking of @htuch or something else? I'm happy to try to re-enable the |
|
@andrewjjenkins I'm just doing some security due diligence work, hence the followup. I agree that your PR is improving the existing situation. Looking at the problem more broadly, I think we just have a "truth in advertising" issue. This can lead to operator confusion and potential security issues due to a mismatch of intent and operation. We claim that we are providing a local connection detection, but it's not always true:
I think we could resolve all of ^^ by renaming this to We should also rename @andrewjjenkins @nickolaev @georgi-d @PiotrSikora @mattklein123 WDYT? |
These suggestions make sense to me. |
|
Makes sense to me too. |
|
Clarifying is a great idea. Currently and before I showed up, the Local ConnectionType FilterChainMatch and the
I'm not sure those fit under the definition of |
Also Utility::isLocalConnection() to Utility::isSameIpOrLoopback(). The idea is to improve descriptive accuracy, see envoyproxy#7840 (comment). Risk level: Low Testing: additional protoxform golden test added for enum value renames. Fixes envoyproxy#8081 Signed-off-by: Harvey Tuch <htuch@google.com>
Also Utility::isLocalConnection() to Utility::isSameIpOrLoopback(). The idea is to improve descriptive accuracy, see #7840 (comment). Risk level: Low Testing: additional protoxform golden test added for enum value renames. Fixes #8081 Signed-off-by: Harvey Tuch <htuch@google.com>
Also Utility::isLocalConnection() to Utility::isSameIpOrLoopback(). The idea is to improve descriptive accuracy, see envoyproxy/envoy#7840 (comment). Risk level: Low Testing: additional protoxform golden test added for enum value renames. Fixes #8081 Signed-off-by: Harvey Tuch <htuch@google.com> Mirrored from https://github.com/envoyproxy/envoy @ 2a9175a41ec8075af7f8429555825c8bc2dcdce2
…yproxy#9518) Also Utility::isLocalConnection() to Utility::isSameIpOrLoopback(). The idea is to improve descriptive accuracy, see envoyproxy#7840 (comment). Risk level: Low Testing: additional protoxform golden test added for enum value renames. Fixes envoyproxy#8081 Signed-off-by: Harvey Tuch <htuch@google.com> Signed-off-by: Prakhar <prakhar_au@yahoo.com>
Description: Connections with the same local/remote address should be considered local for the purposes of filter chain matching, but are currently considered remote (unless other criteria, like loopback addresses, make them local).
Risk Level: Low
Utility::isLocalConnection()is only called from the filter chain matcher.Testing: Fixed unit tests, added new checks.
Docs Changes: None, this makes us correctly honor our docs.
Release Notes: None proposed, I'm OK with a note that this bug was fixed if that's desireable.
Fixes #7839
Before this commit, a connection was considered local if any true:
remote address (probably not intentional).
wrapper around an address we got out of
getifaddrs()(probably notintentional)
(from https://github.com/envoyproxy/envoy/blob/master/source/common/network/utility.cc#L241-L251 )
Condition 3 and 4 were probably intended to call
Envoy::Network::Address::operator==() but because the "==" sign is
between two SharedPtrs, it is actually checking that the pointers are
equal (both are pointing at the same Envoy::Network::Address). There's
an unfortunate aligned glitch in the unit test that obfuscates this for
condition 3, and condition 4 was not covered by any tests.
As implemented, this causes the
listener.FilterChainMatch.ConnectionSourceType LOCAL to not match
connections that I consider local (like connections from "4.4.4.4:1234"
to "4.4.4.4:4321").
I think a better condition 3 would be (and probably close to what was
intended):
ports do not have to match.
This commit adjusts Utility::isLocalConnection() and helpers to achieve
that. Then removes the broken condition 4 entirely.
First, fix the bug in the unit test where socket.localAddress() on the
mock returned remote_addr instead of local_addr. We didn't have any
test cases that EXPECT_TRUE() for the addresses matching (they all rely
on Pipe or isLoopbackAddress()), so we didn't catch this issue before.
Next would be to change this pointer comparison:
to actually invoke Address::operator==():
I made this change locally and confirmed that we now invoke operator==()
but this still won't handle the case that the IP address matches but the
ports are different ("4.4.4.4:1234" to "4.4.4.4:4321" is local).
So instead convert to use
remote_address->ip()->addressAsString()toget the IP address as a string and compare. We evaluated other options like
directly comparing the bits of the IPv4/v6 but decided against it at this point
since we're already constructing the addressAsString() output anyway. If it
proves to be a performance problem, a common function to compare the bits
instead of the string could be dropped in with no behavior change.
Signed-off-by: Andrew Jenkins andrew@volunteers.acasi.org