2つのネットワークインターフェイスと2つの異なるインターネット接続を持つコンピュータがあります。複数のルーティングテーブルがあることを知っています。しかし、非常に単純なシナリオがあります。出てくるSSHアプリケーションは常にwlan0を通過する必要があります。それでは、なぜそんなに複雑なことをするのでしょうか。
カールで初めてテストしてみると完璧に動作します。
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の変更にのみ焦点を当てています。
答え1
(Linuxのみ)を探しているようですSO_BINDTODEVICE
。からman 7 socket
:
SO_BINDTODEVICE
Bind this socket to a particular device like “eth0”, as specified in the passed
interface name. If the name is an empty string or the option length is zero,
the socket device binding is removed. The passed option is a variable-length
null-terminated interface name string with the maximum size of IFNAMSIZ.
If a socket is bound to an interface, only packets received from that particular
interface are processed by the socket. Note that this works only for some socket
types, particularly AF_INET sockets. It is not supported for packet sockets (use
normal bind(2) there).
以下は、これを使用するサンプルプログラムです。
#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, "Connection to the server failed...\n");
return EXIT_FAILURE;
}
// Make an HTTP request to 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("Response: '%s'\n", buffer);
close(sockfd);
return EXIT_SUCCESS;
}
SO_BINDTODEVICE
このプログラムは、私のネットワークインターフェース()の1つをバインドするために使用されますenp0s3
。その後、Google サーバーのいずれかに接続して簡単な HTTP リクエストを実行し、応答の最初の数バイトを印刷します。
実行例は次のとおりです。
$ ./a.out
Response: 'HTTP/1.1 200 OK'
答え2
有用な情報を提供してくれた@Andy Daltonに感謝します。これに基づいて、すべてのプログラムにSO_BINDTODEVICEを実装するためにLD_PRELOADの小さなコードを書いています。
#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>
//Credits go to https://catonmat.net/simple-ld-preload-tutorial and https://catonmat.net/simple-ld-preload-tutorial-part-two
//And of course to https://unix.stackexchange.com/a/648721/334883
//compile with gcc -nostartfiles -fpic -shared bindInterface.c -o bindInterface.so -ldl -D_GNU_SOURCE
//Use with BIND_INTERFACE=<network interface> LD_PRELOAD=./bindInterface.so <your program> like 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" prints two times "MySocket". First is for DNS-Lookup.
//If your first nameserver is not reachable via bound interface,
//then it will try the next nameserver until it succeeds or stops with name resolution error.
//This is why it could take significantly longer than 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("Warning: Programm with LD_PRELOAD startet, but BIND_INTERFACE environment variable not set\n");
fprintf(stderr,"Warning: Programm with LD_PRELOAD startet, but BIND_INTERFACE environment variable not set\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
注:これを行うと、バインドされたインターフェイスが最初のネームサーバーに到達しようとするため、Thisを使用するよりも時間がかかることがあります。/etc/resolv.conf
そのネームサーバーに接続できない場合は、2番目のネームサーバーが使用されます。たとえば、/etc/resolv.conf
GoogleのパブリックDNSサーバー8.8.8.8を最初に編集して入れると、カールバージョンと同じくらい高速です。このオプションを使用すると、--interface
カールはIPアドレスを確認するのではなく、実際の接続を確立したときにのみこのインターフェイスにバインドされます。したがって、ボンディングされたインターフェイスとプライバシー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); when #include <arpa/inet.h> is not included
char *dest = inet_ntoa(socketAddress->sin_addr); //with #include <arpa/inet.h>
printf("connecting to: %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("Bound Interface: %s\n",boundInterface.ifr_name);
return (int)original_connect(sockfd, addr, addrlen);
}
上記と同じオプションを使用してコンパイルします。そして使用
LD_PRELOAD=./bindInterface.so curl --interface wlan0 ifconfig.me
connecting to: 192.168.178.1 / Bound Interface: none
connecting to: 34.117.59.81 / Bound Interface: wlan0
185.107.XX.XX
注2:変更は/etc/resolv.conf
永久的ではありません。これは別のトピックです。これは時間がかかる理由を示すために行われます。