프로그램이 IP 주소가 아닌 인터페이스에 바인딩되도록 강제

프로그램이 IP 주소가 아닌 인터페이스에 바인딩되도록 강제

두 개의 네트워크 인터페이스와 두 개의 서로 다른 인터넷 연결이 있는 컴퓨터가 있습니다. 여러 개의 라우팅 테이블이 있다는 것을 알고 있습니다. 하지만 아주 간단한 시나리오가 있습니다. 나가는 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-addressssh와 동일한 옵션을 갖고 있기 때문에 디버깅에 적합합니다 -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 프로그램을 특정 네트워크 인터페이스에 바인딩철사. 하지만 제목이 "유닉스 프로그램을 특정 네트워크 인터페이스에 바인딩"임에도 불구하고 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이 프로그램은 내 네트워크 인터페이스( ) 중 하나를 바인딩 하는 데 사용됩니다 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해당 이름 서버에 연결할 수 없는 경우 두 번째 이름 서버가 사용됩니다. 예를 들어 /etc/resolv.confGoogle의 공개 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은 영구적이지 않습니다. 이것은 또 다른 주제입니다. 이는 시간이 더 오래 걸리는 이유를 보여주기 위해 수행됩니다.

관련 정보