Вопрос или проблема
У меня есть машина с двумя сетевыми интерфейсами и двумя разными интернет-соединениями. Я знаю, что существуют несколько маршрутизационных таблиц и подобные вещи. Тем не менее, у меня очень простой сценарий. Исходящее ssh-приложение всегда должно работать через wlan0. Так зачем же делать что-то сложное?
Сначала я протестировал с помощью curl, который отлично справляется со своей задачей:
curl --interface wlan0 ifconfig.me
185.107.XX.XX
curl --interface eth0 ifconfig.me
62.226.XX.XX
Таким образом, без настройки каких-либо специальных маршрутизационных правил для двух интерфейсов, все работает именно так, как я хочу. eth0 является маршрутом по умолчанию.
ip route
default via 192.168.178.1 dev eth0 proto dhcp src 192.168.178.21 metric 202
default via 172.16.1.1 dev wlan0 proto dhcp src 172.16.1.88 metric 303
172.16.1.0/24 dev wlan0 proto dhcp scope link src 172.16.1.88 metric 303
192.168.178.0/24 dev eth0 proto dhcp scope link src 192.168.178.21 metric 202
Теперь попробуйте сделать то же самое с wget. Wget идеально подходит для отладки, так как у него есть --bind-address
, такая же опция, как у ssh с -b
.
wget -O- --bind-address=192.168.178.21 ifconfig.me 2> /dev/null
62.226.XX.XX
Вы получите тот же вывод, если опустить --bind-address
wget -O- --bind-address=172.16.1.88 ifconfig.me 2> /dev/null
Эта команда просто зависает примерно на 9 (!) минут, и в конце ничего не выводит, как и будет делать ssh.
Я знаю об этой теме привязки unix-программы к конкретному сетевому интерфейсу. Однако, даже если заголовок “Привязка unix-программы к конкретному сетевому интерфейсу”, все решения, работающие с LD_PRELOAD, привязываются к IP-адресу. Эта функция уже поддерживается ssh, но здесь не помогает. Firejail мог бы решить эту проблему, но, как обсуждалось в другой теме, у него все еще есть ошибка, не работающая таким образом через Wi-Fi.
Так как можно действительно заставить приложение использовать конкретный интерфейс без всей этой сложной маршрутизации, netns или правил iptables? LD_PRELOAD выглядит очень многообещающе, однако пока что этот код сосредоточен только на изменении привязанного IP, а не интерфейса.
Я думаю, вы ищете (только для Linux) SO_BINDTODEVICE
. Из документации man 7 socket
:
SO_BINDTODEVICE
Привязать этот сокет к определенному устройству, например, “eth0”, как указано в переданном
имени интерфейса. Если имя является пустой строкой или длина опции равна нулю,
привязка сокета к устройству удаляется. Переданная опция — это строка имени интерфейса переменной длины, заканчивающаяся нулем, с максимальным размером IFNAMSIZ.
Если сокет привязан к интерфейсу, только пакеты, полученные с этого конкретного
интерфейса, обрабатываются сокетом. Обратите внимание, что это работает только для некоторых типов сокетов, в частности для сокетов AF_INET. Это не поддерживается для пакетных сокетов (используйте обычный bind(2) в этом случае).
Вот пример программы, которая его использует:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <net/if.h>
int main(void)
{
const int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
return EXIT_FAILURE;
}
const struct ifreq ifr = {
.ifr_name = "enp0s3",
};
if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) {
perror("setsockopt");
return EXIT_FAILURE;
}
const struct sockaddr_in servaddr = {
.sin_family = AF_INET,
.sin_addr.s_addr = inet_addr("142.250.73.196"),
.sin_port = htons(80),
};
if (connect(sockfd, (const struct sockaddr*) &servaddr, sizeof(servaddr)) < 0) {
fprintf(stderr, "Не удалось подключиться к серверу...\n");
return EXIT_FAILURE;
}
// Выполнить HTTP-запрос к Google
dprintf(sockfd, "GET / HTTP/1.1\r\n");
dprintf(sockfd, "HOST: www.google.com\r\n");
dprintf(sockfd, "\r\n");
char buffer[16] = {};
read(sockfd, buffer, sizeof(buffer) - 1);
printf("Ответ: '%s'\n", buffer);
close(sockfd);
return EXIT_SUCCESS;
}
Программа использует SO_BINDTODEVICE
, чтобы привязать один из моих сетевых интерфейсов (enp0s3
). Затем она подключается к одному из серверов Google, выполняет простой HTTP-запрос и выводит первые несколько байт ответа.
Вот пример выполнения:
$ ./a.out
Ответ: 'HTTP/1.1 200 OK'
Спасибо @Andy Dalton за предоставление этой полезной информации. На основе этого я сделал небольшой кусок кода для LD_PRELOAD, чтобы реализовать SO_BINDTODEVICE для каждой программы.
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <dlfcn.h>
#include <net/if.h>
#include <string.h>
#include <errno.h>
//Кредиты идут на https://catonmat.net/simple-ld-preload-tutorial и https://catonmat.net/simple-ld-preload-tutorial-part-two
//И, конечно, на https://unix.stackexchange.com/a/648721/334883
//Компилируйте с помощью gcc -nostartfiles -fpic -shared bindInterface.c -o bindInterface.so -ldl -D_GNU_SOURCE
//Используйте с BIND_INTERFACE=<сетевой интерфейс> LD_PRELOAD=./bindInterface.so <ваша программа>, например, curl ifconfig.me
int socket(int family, int type, int protocol)
{
//printf("MySocket\n"); //"LD_PRELOAD=./bind.so wget -O- ifconfig.me 2> /dev/null" печатает дважды "MySocket". Первое это для DNS-запроса.
//Если ваш первый DNS-сервер недоступен через привязанный интерфейс,
//он попробует следующий DNS-сервер, пока не удастся или не возникнет ошибка разрешения имени.
//Именно поэтому это может занять значительно больше времени, чем curl --interface wlan0 ifconfig.me
char *bind_addr_env;
struct ifreq interface;
int *(*original_socket)(int, int, int);
original_socket = dlsym(RTLD_NEXT,"socket");
int fd = (int)(*original_socket)(family,type,protocol);
bind_addr_env = getenv("BIND_INTERFACE");
int errorCode;
if ( bind_addr_env != NULL && strlen(bind_addr_env) > 0)
{
//printf(bind_addr_env);
strcpy(interface.ifr_name,bind_addr_env);
errorCode = setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &interface, sizeof(interface));
if ( errorCode < 0)
{
perror("setsockopt");
errno = EINVAL;
return -1;
};
}
else
{
printf("Предупреждение: Программа с LD_PRELOAD запущена, но переменная окружения BIND_INTERFACE не установлена\n");
fprintf(stderr,"Предупреждение: Программа с LD_PRELOAD запущена, но переменная окружения BIND_INTERFACE не установлена\n");
}
return fd;
}
Скомпилируйте это с помощью
gcc -nostartfiles -fpic -shared bindInterface.c -o bindInterface.so -ldl -D_GNU_SOURCE
Используйте это с
BIND_INTERFACE=wlan0 LD_PRELOAD=./bindInterface.so wget -O- ifconfig.me 2>/dev/null
Примечание: выполнение этого может занять гораздо больше времени, чем с curl --interface wlan0 ifconfig.me
Это потому, что он также пытается достичь вашего первого DNS-сервера из /etc/resolv.conf
с привязанным интерфейсом. Если этот DNS-сервер недоступен, он использует второй и так далее. Если вы отредактируете /etc/resolv.conf
и поставите, например, публичный DNS-сервер Google 8.8.8.8 на первое место, это будет так же быстро, как версия curl. С опцией --interface
curl привязывается только к этому интерфейсу при установлении фактического соединения, а не при разрешении IP-адреса. Таким образом, при использовании curl с привязанным интерфейсом и приватной VPN он будет утекать DNS-запросы через нормальное соединение, если не будет правильно настроен. Используйте этот код для проверки:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <dlfcn.h>
#include <net/if.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
int connect (int sockfd, const struct sockaddr *addr, socklen_t addrlen)
{
int *(*original_connect)(int, const struct sockaddr*, socklen_t);
original_connect = dlsym(RTLD_NEXT,"connect");
static struct sockaddr_in *socketAddress;
socketAddress = (struct sockaddr_in *)addr;
if (socketAddress -> sin_family == AF_INET)
{
// inet_ntoa(socketAddress->sin_addr.s_addr); когда #include <arpa/inet.h> не включен
char *dest = inet_ntoa(socketAddress->sin_addr); //с #include <arpa/inet.h>
printf("подключаемся к: %s / ",dest);
}
struct ifreq boundInterface =
{
.ifr_name = "none",
};
socklen_t optionlen = sizeof(boundInterface);
int errorCode;
errorCode = getsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, &boundInterface, &optionlen);
if ( errorCode < 0)
{
perror("getsockopt");
return -1;
};
printf("Привязанный интерфейс: %s\n",boundInterface.ifr_name);
return (int)original_connect(sockfd, addr, addrlen);
}
Используйте те же параметры, что и выше для компиляции. Используйте с
LD_PRELOAD=./bindInterface.so curl --interface wlan0 ifconfig.me
подключаемся к: 192.168.178.1 / Привязанный интерфейс: none
подключаемся к: 34.117.59.81 / Привязанный интерфейс: wlan0
185.107.XX.XX
Примечание 2: Изменение /etc/resolv.conf
не является постоянным. Это другая тема. Это было сделано только для демонстрации, почему это занимает больше времени.
Я нашел это очень полезным и создал репозиторий и предварительно скомпилированное!
https://github.com/peppergrayxyz/bind2device
Может быть, Google приведет кого-то еще сюда (:
Ответ или решение
Для выполнения заданной задачи привязки программы к определённому сетевому интерфейсу в Linux, не прибегая к сложным маршрутизационным или iptables-правилам, можно использовать опцию SO_BINDTODEVICE
. Эта функция позволяет привязывать сокет к конкретному сетевому интерфейсу, что делает её особенно полезной в вашем случае.
Что такое SO_BINDTODEVICE
SO_BINDTODEVICE
— это опция для сокетов, которая позволяет привязать сокет к определённому интерфейсу, например, wlan0
или eth0
. Это значит, что все пакеты, поступающие и исходящие через этот сокет, будут обрабатываться только через указанный интерфейс. Данная опция особенно актуальна для сокетов типа AF_INET
(IPv4).
Реализация
Вы можете использовать следующий код, чтобы создать программу, которая будет использовать SO_BINDTODEVICE
для привязки сокетов к заданному интерфейсу:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
int main(void)
{
const int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("socket");
return EXIT_FAILURE;
}
const char* interface_name = "wlan0"; // Укажите нужный интерфейс
struct ifreq ifr;
strncpy(ifr.ifr_name, interface_name, IFNAMSIZ - 1);
if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) {
perror("setsockopt");
return EXIT_FAILURE;
}
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("142.250.73.196"); // IP сервера
servaddr.sin_port = htons(80); // Порт
if (connect(sockfd, (const struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
fprintf(stderr, "Не удалось подключиться к серверу...\n");
return EXIT_FAILURE;
}
// Отправка HTTP-запроса
dprintf(sockfd, "GET / HTTP/1.1\r\n");
dprintf(sockfd, "HOST: www.google.com\r\n");
dprintf(sockfd, "\r\n");
char buffer[256] = {};
read(sockfd, buffer, sizeof(buffer) - 1);
printf("Ответ: '%s'\n", buffer);
close(sockfd);
return EXIT_SUCCESS;
}
Использование LD_PRELOAD
Если вам необходимо привязать любую существующую программу к определённому интерфейсу, вы можете использовать механизм LD_PRELOAD
. Это позволит вам подменять функции, такие как socket()
, чтобы они автоматически привязывались к нужному интерфейсу. Вот пример кода, который вы можете использовать:
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <dlfcn.h>
#include <string.h>
#include <errno.h>
#include <net/if.h>
int socket(int domain, int type, int protocol) {
int (*original_socket)(int, int, int);
original_socket = dlsym(RTLD_NEXT, "socket");
int fd = original_socket(domain, type, protocol);
if (fd < 0) {
return fd; // Возвращаем ошибку, если создание сокета не удалось
}
const char* bind_interface = getenv("BIND_INTERFACE");
if (bind_interface) {
struct ifreq ifr;
strncpy(ifr.ifr_name, bind_interface, IFNAMSIZ);
if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) < 0) {
perror("setsockopt");
return -1;
}
}
return fd;
}
Сборка и использование
Скомпилируйте код с помощью следующей команды:
gcc -fPIC -shared -o bindInterface.so bindInterface.c -ldl
Запустите ваше приложение с LD_PRELOAD, указывая интерфейс:
BIND_INTERFACE=wlan0 LD_PRELOAD=./bindInterface.so wget -O- ifconfig.me
Заключение
Используя SO_BINDTODEVICE
и механизм LD_PRELOAD
, вы сможете привязать любую программу к конкретному сетевому интерфейсу без необходимости в сложных маршрутах или дополнительных настройках. Это значительно упрощает задачу настройки сетевых приложений на многопрофильных серверах.