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

listener: fixed a bug where the addresses cannot be updated partially #38601

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions api/envoy/config/listener/v3/listener.proto
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,10 @@ message Listener {
google.protobuf.BoolValue freebind = 11;

// Additional socket options that may not be present in Envoy source code or
// precompiled binaries. The socket options can be updated for a listener when
// precompiled binaries.
// It is not allowed to update the socket options for any existing address if
// :ref:`enable_reuse_port <envoy_v3_api_field_config.listener.v3.Listener.enable_reuse_port>`
// is ``true``. Otherwise, if socket options change during a listener update the update will be rejected
// to make it clear that the options were not updated.
// is ``false`` to avoid the conflict when creating new sockets for the listener.
repeated core.v3.SocketOption socket_options = 13;

// Whether the listener should accept TCP Fast Open (TFO) connections.
Expand Down
3 changes: 3 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ bug_fixes:
Before the :scheme header was always 'http'.
This behavioral change can be temporarily reverted by setting runtime guard
``envoy.reloadable_features.jwt_fetcher_use_scheme_from_uri`` to false.
- area: listener
change: |
Fixed a bug where the addresses cannot be updated partially even the reuse port is enabled.

removed_config_or_runtime:
# *Normally occurs at the end of the* :ref:`deprecation period <deprecated>`
Expand Down
19 changes: 6 additions & 13 deletions source/common/listener_manager/listener_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1128,15 +1128,13 @@ bool ListenerImpl::getReusePortOrDefault(Server::Instance& server,
return initial_reuse_port_value;
}

bool ListenerImpl::socketOptionsEqual(const ListenerImpl& other) const {
return ListenerMessageUtil::socketOptionsEqual(config(), other.config());
}

bool ListenerImpl::hasCompatibleAddress(const ListenerImpl& other) const {
if ((socket_type_ != other.socket_type_) || (addresses_.size() != other.addresses().size())) {
// First, check if the listener has the same socket type and the same number of addresses.
if (socket_type_ != other.socket_type_ || addresses_.size() != other.addresses().size()) {
return false;
}

// Second, check if the listener has the same addresses.
// The listener support listening on the zero port address for test. Multiple zero
// port addresses are also supported. For comparing two listeners with multiple
// zero port addresses, only need to ensure there are the same number of zero
Expand All @@ -1153,17 +1151,12 @@ bool ListenerImpl::hasCompatibleAddress(const ListenerImpl& other) const {
}
other_addresses.erase(iter);
}
return true;

// Third, check if the listener has the same socket options.
return ListenerMessageUtil::socketOptionsEqual(config(), other.config());
}

bool ListenerImpl::hasDuplicatedAddress(const ListenerImpl& other) const {
// Skip the duplicate address check if this is the case of a listener update with new socket
// options.
if ((name_ == other.name_) &&
!ListenerMessageUtil::socketOptionsEqual(config(), other.config())) {
return false;
}

if (socket_type_ != other.socket_type_) {
return false;
}
Expand Down
2 changes: 0 additions & 2 deletions source/common/listener_manager/listener_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,6 @@ class ListenerImpl final : public Network::ListenerConfig,
const envoy::config::listener::v3::Listener& config,
Network::Socket::Type socket_type);

// Compare whether two listeners have different socket options.
bool socketOptionsEqual(const ListenerImpl& other) const;
// Check whether a new listener can share sockets with this listener.
bool hasCompatibleAddress(const ListenerImpl& other) const;
// Check whether a new listener has duplicated listening address this listener.
Expand Down
30 changes: 18 additions & 12 deletions source/common/listener_manager/listener_manager_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -512,21 +512,12 @@ ListenerManagerImpl::addOrUpdateListener(const envoy::config::listener::v3::List
absl::Status
ListenerManagerImpl::setupSocketFactoryForListener(ListenerImpl& new_listener,
const ListenerImpl& existing_listener) {
bool same_socket_options = true;
if (new_listener.reusePort() != existing_listener.reusePort()) {
return absl::InvalidArgumentError(fmt::format(
"Listener {}: reuse port cannot be changed during an update", new_listener.name()));
}

same_socket_options = existing_listener.socketOptionsEqual(new_listener);
if (!same_socket_options && new_listener.reusePort() == false) {
return absl::InvalidArgumentError(
fmt::format("Listener {}: doesn't support update any socket options "
"when the reuse port isn't enabled",
new_listener.name()));
}

if (!(existing_listener.hasCompatibleAddress(new_listener) && same_socket_options)) {
if (!existing_listener.hasCompatibleAddress(new_listener)) {
RETURN_IF_NOT_OK(setNewOrDrainingSocketFactory(new_listener.name(), new_listener));
} else {
RETURN_IF_NOT_OK(new_listener.cloneSocketFactoryFrom(existing_listener));
Expand Down Expand Up @@ -639,9 +630,21 @@ absl::StatusOr<bool> ListenerManagerImpl::addOrUpdateListenerInternal(
return true;
}

bool ListenerManagerImpl::hasListenerWithDuplicatedAddress(const ListenerList& list,
bool ListenerManagerImpl::hasListenerWithDuplicatedAddress(const ListenerList& listener_list,
const ListenerImpl& listener) {
for (const auto& existing_listener : list) {
// This is new listener or new version of existing listener but with different addresses
// or different socket options.
// Check if the listener has duplicated address with existing listeners.

for (const auto& existing_listener : listener_list) {
if (listener.reusePort() && existing_listener->name() == listener.name()) {
Copy link
Member

@Shikugawa Shikugawa Mar 2, 2025

Choose a reason for hiding this comment

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

I prefer to reject reuse_port == false and the listeners with the same name listen with the duplicated address early as my original PR.

if (existing_listener.hasDuplicatedAddress(new_listener) && new_listener.reusePort() == false) {
return absl::InvalidArgumentError(fmt::format("Listener {}: doesn't support update addresses "
"when the reuse port isn't enabled",
new_listener.name()));
}

In this implementation, the user can receive only messages that identify "listening address is duplicated". I guess the users may confused about the message, and can't know why this configuration is rejected.

Copy link
Member

@Shikugawa Shikugawa Mar 2, 2025

Choose a reason for hiding this comment

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

Oh, But I think this message helps users to understand the root cause. So personally current implementation is LGTM.

ENVOY_LOG(warn, "To check if the listener has duplicated addresses with other listeners or "
"'enable_reuse_port' is set to 'false' for the listener");

// If reuse port is enabled, we can skip the check between different versions of the
// same listener as they can create their own sockets anyway.
// If reuse port is disabled, the duplicated addresses check is necessary to ensure
// there is no address conflict between this two versions.
continue;
}

if (existing_listener->hasDuplicatedAddress(listener)) {
return true;
}
Expand Down Expand Up @@ -1180,6 +1183,9 @@ absl::Status ListenerManagerImpl::setNewOrDrainingSocketFactory(const std::strin
}
}

// TODO(wbpcode): if we cannot clone the socket factory from the draining listener, we should
// check the duplicated addresses again the draining listeners to avoid the creation failure
// of the sockets.
if (draining_listener_ptr != nullptr) {
RETURN_IF_NOT_OK(listener.cloneSocketFactoryFrom(*draining_listener_ptr));
} else {
Expand Down
89 changes: 89 additions & 0 deletions test/common/listener_manager/listener_manager_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,95 @@ name: bar
addOrUpdateListener(parseListenerFromV3Yaml(yaml2));
}

TEST_P(ListenerManagerImplWithRealFiltersTest, AllowAddressesUpdateParitially) {
const std::string yaml1 = R"EOF(
name: foo
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a comment saying what part of the listener config is changing so the reader doesn't have to manually diff the two configs in their head.

address:
socket_address:
address: 127.0.0.1
port_value: 1000
additional_addresses:
- address:
socket_address:
address: 127.0.0.2
port_value: 1000
filter_chains:
- filters: []
name: foo
)EOF";

const std::string yaml2 = R"EOF(
name: foo
address:
socket_address:
address: 127.0.0.1
port_value: 1000
additional_addresses:
- address:
socket_address:
address: 127.0.0.3
port_value: 2000
filter_chains:
- filters: []
name: foo
)EOF";

EXPECT_CALL(listener_factory_, createListenSocket(_, _, _, default_bind_type, _, 0)).Times(2);
addOrUpdateListener(parseListenerFromV3Yaml(yaml1));
EXPECT_CALL(listener_factory_, createListenSocket(_, _, _, default_bind_type, _, 0)).Times(2);
addOrUpdateListener(parseListenerFromV3Yaml(yaml2));
}

TEST_P(ListenerManagerImplWithRealFiltersTest,
AllowUpdateSocketOptionsIfNotDuplicatedEvenReusePortIsDisabled) {
const std::string yaml1 = R"EOF(
name: foo
address:
socket_address:
address: 127.0.0.1
port_value: 1000
additional_addresses:
- address:
socket_address:
address: 127.0.0.2
port_value: 1000
enable_reuse_port: false
filter_chains:
- filters: []
name: foo
)EOF";

const std::string yaml2 = R"EOF(
name: foo
address:
socket_address:
address: 127.0.0.4
port_value: 1000
additional_addresses:
- address:
socket_address:
address: 127.0.0.3
port_value: 2000
enable_reuse_port: false
socket_options:
- level: 1
name: 9
int_value: 1
filter_chains:
- filters: []
name: foo
)EOF";

EXPECT_CALL(listener_factory_,
createListenSocket(_, _, _, ListenerComponentFactory::BindType::NoReusePort, _, 0))
.Times(2);
addOrUpdateListener(parseListenerFromV3Yaml(yaml1));
EXPECT_CALL(listener_factory_,
createListenSocket(_, _, _, ListenerComponentFactory::BindType::NoReusePort, _, 0))
.Times(2);
addOrUpdateListener(parseListenerFromV3Yaml(yaml2));
}

TEST_P(ListenerManagerImplWithRealFiltersTest, SetListenerPerConnectionBufferLimit) {
const std::string yaml = R"EOF(
address:
Expand Down
Loading