Skip to content

Commit

Permalink
Socket option for binding to a network interface by name (#647)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Graeb <[email protected]>
  • Loading branch information
waahm7 and graebm authored Jul 11, 2024
1 parent d1e97c1 commit d04508d
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 3 deletions.
12 changes: 12 additions & 0 deletions include/aws/io/socket.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ enum aws_socket_type {
AWS_SOCKET_DGRAM,
};

#define AWS_NETWORK_INTERFACE_NAME_MAX 16

struct aws_socket_options {
enum aws_socket_type type;
enum aws_socket_domain domain;
Expand All @@ -43,6 +45,16 @@ struct aws_socket_options {
* lost. If zero OS defaults are used. On Windows, this option is meaningless until Windows 10 1703.*/
uint16_t keep_alive_max_failed_probes;
bool keepalive;

/**
* (Optional)
* This property is used to bind the socket to a particular network interface by name, such as eth0 and ens32.
* If this is empty, the socket will not be bound to any interface and will use OS defaults. If the provided name
* is invalid, `aws_socket_init()` will error out with AWS_IO_SOCKET_INVALID_OPTIONS. This option is only
* supported on Linux, macOS, and platforms that have either SO_BINDTODEVICE or IP_BOUND_IF. It is not supported on
* Windows. `AWS_ERROR_PLATFORM_NOT_SUPPORTED` will be raised on unsupported platforms.
*/
char network_interface_name[AWS_NETWORK_INTERFACE_NAME_MAX];
};

struct aws_socket;
Expand Down
92 changes: 90 additions & 2 deletions source/posix/socket.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
#include <aws/io/io.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <net/if.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <sys/types.h>
Expand Down Expand Up @@ -1254,7 +1254,95 @@ int aws_socket_set_options(struct aws_socket *socket, const struct aws_socket_op
socket->io_handle.data.fd,
errno_value);
}

size_t network_interface_length = 0;
if (aws_secure_strlen(options->network_interface_name, AWS_NETWORK_INTERFACE_NAME_MAX, &network_interface_length)) {
AWS_LOGF_ERROR(
AWS_LS_IO_SOCKET,
"id=%p fd=%d: network_interface_name max length must be %d length and NULL terminated",
(void *)socket,
socket->io_handle.data.fd,
AWS_NETWORK_INTERFACE_NAME_MAX);
return aws_raise_error(AWS_IO_SOCKET_INVALID_OPTIONS);
}
if (network_interface_length != 0) {
#if defined(SO_BINDTODEVICE)
if (setsockopt(
socket->io_handle.data.fd,
SOL_SOCKET,
SO_BINDTODEVICE,
options->network_interface_name,
network_interface_length)) {
int errno_value = errno; /* Always cache errno before potential side-effect */
AWS_LOGF_ERROR(
AWS_LS_IO_SOCKET,
"id=%p fd=%d: setsockopt() with SO_BINDTODEVICE for \"%s\" failed with errno %d.",
(void *)socket,
socket->io_handle.data.fd,
options->network_interface_name,
errno_value);
return aws_raise_error(AWS_IO_SOCKET_INVALID_OPTIONS);
}
#elif defined(IP_BOUND_IF)
/*
* If SO_BINDTODEVICE is not supported, the alternative is IP_BOUND_IF which requires an index instead
* of a name. We are not using this everywhere because this requires 2 system calls instead of 1, and is
* dependent upon the type of sockets, which doesn't support AWS_SOCKET_LOCAL. As a future optimization, we can
* look into caching the result of if_nametoindex.
*/
uint network_interface_index = if_nametoindex(options->network_interface_name);
if (network_interface_index == 0) {
int errno_value = errno; /* Always cache errno before potential side-effect */
AWS_LOGF_ERROR(
AWS_LS_IO_SOCKET,
"id=%p fd=%d: network_interface_name \"%s\" not found. if_nametoindex() failed with errno %d.",
(void *)socket,
socket->io_handle.data.fd,
options->network_interface_name,
errno_value);
return aws_raise_error(AWS_IO_SOCKET_INVALID_OPTIONS);
}
if (options->domain == AWS_SOCKET_IPV6) {
if (setsockopt(
socket->io_handle.data.fd,
IPPROTO_IPV6,
IPV6_BOUND_IF,
&network_interface_index,
sizeof(network_interface_index))) {
int errno_value = errno; /* Always cache errno before potential side-effect */
AWS_LOGF_ERROR(
AWS_LS_IO_SOCKET,
"id=%p fd=%d: setsockopt() with IPV6_BOUND_IF for \"%s\" failed with errno %d.",
(void *)socket,
socket->io_handle.data.fd,
options->network_interface_name,
errno_value);
return aws_raise_error(AWS_IO_SOCKET_INVALID_OPTIONS);
}
} else if (setsockopt(
socket->io_handle.data.fd,
IPPROTO_IP,
IP_BOUND_IF,
&network_interface_index,
sizeof(network_interface_index))) {
int errno_value = errno; /* Always cache errno before potential side-effect */
AWS_LOGF_ERROR(
AWS_LS_IO_SOCKET,
"id=%p fd=%d: setsockopt() with IP_BOUND_IF for \"%s\" failed with errno %d.",
(void *)socket,
socket->io_handle.data.fd,
options->network_interface_name,
errno_value);
return aws_raise_error(AWS_IO_SOCKET_INVALID_OPTIONS);
}
#else
AWS_LOGF_ERROR(
AWS_LS_IO_SOCKET,
"id=%p fd=%d: network_interface_name is not supported on this platform.",
(void *)socket,
socket->io_handle.data.fd);
return aws_raise_error(AWS_ERROR_PLATFORM_NOT_SUPPORTED);
#endif
}
if (options->type == AWS_SOCKET_STREAM && options->domain != AWS_SOCKET_LOCAL) {
if (socket->options.keepalive) {
int keep_alive = 1;
Expand Down
19 changes: 19 additions & 0 deletions source/windows/iocp/socket.c
Original file line number Diff line number Diff line change
Expand Up @@ -2333,6 +2333,25 @@ int aws_socket_set_options(struct aws_socket *socket, const struct aws_socket_op
#endif
}

size_t network_interface_length = 0;
if (aws_secure_strlen(options->network_interface_name, AWS_NETWORK_INTERFACE_NAME_MAX, &network_interface_length)) {
AWS_LOGF_ERROR(
AWS_LS_IO_SOCKET,
"id=%p fd=%d: network_interface_name max length must be %d length and NULL terminated",
(void *)socket,
socket->io_handle.data.fd,
AWS_NETWORK_INTERFACE_NAME_MAX);
return aws_raise_error(AWS_IO_SOCKET_INVALID_OPTIONS);
}
if (network_interface_length != 0) {
AWS_LOGF_ERROR(
AWS_LS_IO_SOCKET,
"id=%p fd=%d: network_interface_name is not supported on this platform.",
(void *)socket,
socket->io_handle.data.fd);
return aws_raise_error(AWS_ERROR_PLATFORM_NOT_SUPPORTED);
}

return AWS_OP_SUCCESS;
}

Expand Down
2 changes: 2 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ add_test_case(io_testing_channel)
add_test_case(local_socket_communication)
add_net_test_case(tcp_socket_communication)
add_net_test_case(udp_socket_communication)
add_net_test_case(test_socket_with_bind_to_interface)
add_net_test_case(test_socket_with_bind_to_invalid_interface)
add_test_case(udp_bind_connect_communication)
add_net_test_case(connect_timeout)
add_net_test_case(connect_timeout_cancelation)
Expand Down
2 changes: 1 addition & 1 deletion tests/socket_handler_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,7 @@ static int s_socket_read_to_eof_after_peer_hangup_test_common(

struct local_server_tester local_server_tester;
if (s_local_server_tester_init(allocator, &local_server_tester, &server_args, &c_tester, socket_domain, false)) {
/* Skip test if server can't bind to address (e.g. Gith9ub's ubuntu runners don't allow IPv6) */
/* Skip test if server can't bind to address (e.g. Codebuild's ubuntu runners don't allow IPv6) */
if (aws_last_error() == AWS_IO_SOCKET_INVALID_ADDRESS) {
return AWS_OP_SKIP;
} else {
Expand Down
64 changes: 64 additions & 0 deletions tests/socket_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,70 @@ static int s_test_tcp_socket_communication(struct aws_allocator *allocator, void

AWS_TEST_CASE(tcp_socket_communication, s_test_tcp_socket_communication)

static int s_test_socket_with_bind_to_interface(struct aws_allocator *allocator, void *ctx) {
(void)ctx;
struct aws_socket_options options;
AWS_ZERO_STRUCT(options);
options.connect_timeout_ms = 3000;
options.keepalive = true;
options.keep_alive_interval_sec = 1000;
options.keep_alive_timeout_sec = 60000;
options.type = AWS_SOCKET_STREAM;
options.domain = AWS_SOCKET_IPV4;
#if defined(AWS_OS_APPLE)
strncpy(options.network_interface_name, "lo0", AWS_NETWORK_INTERFACE_NAME_MAX);
#else
strncpy(options.network_interface_name, "lo", AWS_NETWORK_INTERFACE_NAME_MAX);
#endif
struct aws_socket_endpoint endpoint = {.address = "127.0.0.1", .port = 8127};
if (s_test_socket(allocator, &options, &endpoint)) {
#if !defined(AWS_OS_APPLE) && !defined(AWS_OS_LINUX)
if (aws_last_error() == AWS_ERROR_PLATFORM_NOT_SUPPORTED) {
return AWS_OP_SKIP;
}
#endif
ASSERT_TRUE(false, "s_test_socket() failed");
}
options.type = AWS_SOCKET_DGRAM;
options.domain = AWS_SOCKET_IPV4;
ASSERT_SUCCESS(s_test_socket(allocator, &options, &endpoint));

struct aws_socket_endpoint endpoint_ipv6 = {.address = "::1", .port = 1024};
options.type = AWS_SOCKET_STREAM;
options.domain = AWS_SOCKET_IPV6;
if (s_test_socket(allocator, &options, &endpoint_ipv6)) {
/* Skip test if server can't bind to address (e.g. Codebuild's ubuntu runners don't allow IPv6) */
if (aws_last_error() == AWS_IO_SOCKET_INVALID_ADDRESS) {
return AWS_OP_SKIP;
}
ASSERT_TRUE(false, "s_test_socket() failed");
}

return AWS_OP_SUCCESS;
}
AWS_TEST_CASE(test_socket_with_bind_to_interface, s_test_socket_with_bind_to_interface)

static int s_test_socket_with_bind_to_invalid_interface(struct aws_allocator *allocator, void *ctx) {
(void)ctx;
struct aws_socket_options options;
AWS_ZERO_STRUCT(options);
options.connect_timeout_ms = 3000;
options.keepalive = true;
options.keep_alive_interval_sec = 1000;
options.keep_alive_timeout_sec = 60000;
options.type = AWS_SOCKET_STREAM;
options.domain = AWS_SOCKET_IPV4;
strncpy(options.network_interface_name, "invalid", AWS_NETWORK_INTERFACE_NAME_MAX);
struct aws_socket outgoing;
#if defined(AWS_OS_APPLE) || defined(AWS_OS_LINUX)
ASSERT_ERROR(AWS_IO_SOCKET_INVALID_OPTIONS, aws_socket_init(&outgoing, allocator, &options));
#else
ASSERT_ERROR(AWS_ERROR_PLATFORM_NOT_SUPPORTED, aws_socket_init(&outgoing, allocator, &options));
#endif
return AWS_OP_SUCCESS;
}
AWS_TEST_CASE(test_socket_with_bind_to_invalid_interface, s_test_socket_with_bind_to_invalid_interface)

#if defined(USE_VSOCK)
static int s_test_vsock_loopback_socket_communication(struct aws_allocator *allocator, void *ctx) {
/* Without vsock loopback it's difficult to test vsock functionality.
Expand Down

0 comments on commit d04508d

Please sign in to comment.