Skip to content

network: Conns with the same local/remote address are local#7840

Merged
mattklein123 merged 5 commits intoenvoyproxy:masterfrom
andrewjjenkins:fixislocal
Aug 16, 2019
Merged

network: Conns with the same local/remote address are local#7840
mattklein123 merged 5 commits intoenvoyproxy:masterfrom
andrewjjenkins:fixislocal

Conversation

@andrewjjenkins
Copy link
Copy Markdown
Contributor

@andrewjjenkins andrewjjenkins commented Aug 6, 2019

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

  • The affected function Utility::isLocalConnection() is only called from the filter chain matcher.
  • The behavior before seems to me to be incorrect, so I do not believe anyone would be unpleasantly surprised by this change.
  • Covered by existing unit tests (with fixes)

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:

  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).
  4. The pointer to the local address was the same as the pointer to a
    wrapper around an address we got out of getifaddrs() (probably not
    intentional)

(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):

  1. 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. 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:

  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).

So instead convert to use remote_address->ip()->addressAsString() to
get 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

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));
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@andrewjjenkins andrewjjenkins changed the title Conns with the same local/remote address are local (fix #7839) network: Conns with the same local/remote address are local Aug 6, 2019
@mattklein123
Copy link
Copy Markdown
Member

@PiotrSikora @silentdai PTAL

@stale
Copy link
Copy Markdown

stale bot commented Aug 14, 2019

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!

@stale stale bot added the stale stalebot believes this issue/PR has not been touched recently label Aug 14, 2019
@andrewjjenkins
Copy link
Copy Markdown
Contributor Author

ping @PiotrSikora @silentdai please let me know if there's any more info that would help

@stale stale bot removed the stale stalebot believes this issue/PR has not been touched recently label Aug 14, 2019
@lambdai
Copy link
Copy Markdown
Contributor

lambdai commented Aug 14, 2019

Sorry I missed the previous notification!
isLocalConnection is invoked after the local address is restored. It works perfectly. Thanks!

/lgtm
/approve

@lambdai
Copy link
Copy Markdown
Contributor

lambdai commented Aug 14, 2019

btw if you have 2 ip A and B on one machine, A can talk to B "locally".
There might be alternative solution other than this envoy LOCAL matcher.

@andrewjjenkins
Copy link
Copy Markdown
Contributor Author

Thanks much @silentdai!
Understood about the multihomed case. That's not currently the case I'm working on, I'm working on IP A calling IP A, so I'm happy with this improvement.

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 mattklein123 self-assigned this Aug 14, 2019
Copy link
Copy Markdown
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

virtual ~Instance() = default;

virtual bool operator==(const Instance& rhs) const PURE;
virtual bool equalsExceptPort(const Instance& rhs) const PURE;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO this is an abstraction leak to expose port up to this level. I would remove this interface change.

Andrew Jenkins added 2 commits August 15, 2019 00:15
… IPs

Signed-off-by: Andrew Jenkins <andrew@volunteers.acasi.org>
Signed-off-by: Andrew Jenkins <andrew@volunteers.acasi.org>
// 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:
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

*addr, (af_look_up == AF_INET) ? sizeof(sockaddr_in) : sizeof(sockaddr_in6));

if (remote_address == local_address) {
if (*remote_address == *local_address) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this and fixing this up. A few small comments. Nice work!

/wait

*addr, (af_look_up == AF_INET) ? sizeof(sockaddr_in) : sizeof(sockaddr_in6));

if (remote_address == local_address) {
if (*remote_address == *local_address) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Make the equality comparison not named operator==(), make it some other method (addressEquals() on the Ip class is a better name than equalsExceptPort() on Address).
  2. Remove port from 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Ipv4Instance::Ipv4Instance(const sockaddr_in* address) : InstanceBase(Type::Ip) {
ip_.ipv4_.address_ = *address;
ip_.friendly_address_ = sockaddrToString(*address);
// Based on benchmark testing, this reserve+append implementation runs faster than absl::StrCat.
fmt::format_int port(ntohs(address->sin_port));
friendly_name_.reserve(ip_.friendly_address_.size() + 1 + port.size());
friendly_name_.append(ip_.friendly_address_);
friendly_name_.push_back(':');
friendly_name_.append(port.data(), port.size());
validateIpv4Supported(friendly_name_);
}

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>
Copy link
Copy Markdown
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this. Lets delete the broken code per discussion and then we are good to go! Thank you!

/wait

*addr, (af_look_up == AF_INET) ? sizeof(sockaddr_in) : sizeof(sockaddr_in6));

if (remote_address == local_address) {
if (*remote_address == *local_address) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@andrewjjenkins
Copy link
Copy Markdown
Contributor Author

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!

Copy link
Copy Markdown
Member

@mattklein123 mattklein123 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome, thank you!

@mattklein123 mattklein123 merged commit 1c27f54 into envoyproxy:master Aug 16, 2019
@andrewjjenkins andrewjjenkins deleted the fixislocal branch August 16, 2019 22:26
@htuch
Copy link
Copy Markdown
Member

htuch commented Aug 21, 2019

@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.

@andrewjjenkins
Copy link
Copy Markdown
Contributor Author

Before this commit, the address comparisons were accidentally pointer comparisons. So the only things considered local were:

  1. Pipes
  2. Sockets to a loopback address (127.0.0.1 or ::1)

I believe it was intended to also include sockets where the remote IP was the same as the local IP (e.g. 1.2.3.4:23001 -> 1.2.3.4:80). But this isn't how the implementation was actually working - it was comparing pointers to addresses, not addresses (and even if it wasn't, the operator that would have been invoked would have said that 1.2.3.4:23001 != 1.2.3.4:80).

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 1.2.3.4:23001 -> 1.2.3.4:80 to be local, even if 1.2.3.4 isn't an address on a local interface. I think that's consistent with what was originally intended.

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:

  1. You have two local interfaces, eth0=1.2.3.4 and eth1=4.3.2.1.
  2. We are asked "Is a connection with source addr 1.2.3.4 and dest addr 4.3.2.1 local?"

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 connect()ing and is therefore following the routing rules with source addr 1.2.3.4, right? It's possible (even likely?) that packets destined for 4.3.2.1 will go to itself. But I'm not sure it's guaranteed. Especially on Strong End System Model configurations. The routes via eth0 may include a different 4.3.2.1.

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 getifaddr stuff though I'd recommend not calling getifaddr on each call. Also I can't think of a case where getifaddr doesn't interact badly with IP_FREEBIND.

@htuch
Copy link
Copy Markdown
Member

htuch commented Aug 22, 2019

@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:

  • Sometimes local connections won't be LOCAL, see your example of the two local interfaces.
  • Sometimes remote connection might be LOCAL. I don't have strong evidence for this, but I suspect clever enough configuration of things like NAT, IP_FREEBIND or network namespaces could do something like this.

I think we could resolve all of ^^ by renaming this to SAME_IP in the v3 xDS APIs, to avoid user confusion. The operator needs to decide the implications of SAME_IP, not Envoy.

We should also rename Utility::isLocalConnection, this is a mine waiting to be encountered if we start depending on this elsewhere in the code.

@andrewjjenkins @nickolaev @georgi-d @PiotrSikora @mattklein123 WDYT?

@mattklein123
Copy link
Copy Markdown
Member

WDYT?

These suggestions make sense to me.

@nickolaev
Copy link
Copy Markdown

Makes sense to me too.

@andrewjjenkins
Copy link
Copy Markdown
Contributor Author

Clarifying is a great idea.

Currently and before I showed up, the Local ConnectionType FilterChainMatch and the Utility::isLocalConnection() function say that these connections are local:

  1. All Pipes
  2. Connections to a loopback addr 127.0.0.1 or ::1.

I'm not sure those fit under the definition of SAME_IP but I'm guessing we don't want to change the behavior (esp. for loopback). We can call it SAME_IP and add clarifying language to the doc string that it's really Same IP or Pipe or connections to loopback?

htuch added a commit to htuch/envoy that referenced this pull request Dec 29, 2019
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>
htuch added a commit that referenced this pull request Dec 30, 2019
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>
mattklein123 pushed a commit to envoyproxy/data-plane-api that referenced this pull request Dec 30, 2019
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
prakhag1 pushed a commit to prakhag1/envoy that referenced this pull request Jan 3, 2020
…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>
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

Successfully merging this pull request may close these issues.

FilterChainMatch source_type LOCAL doesn't match local connections

6 participants