Как я могу получить имя/индекс интерфейса, связанный с сокетом TCP?

Я пишу TCP-сервер, которому нужно знать, с какого интерфейса приходит каждое соединение. Я не могу использовать адрес/подсеть, чтобы определить, какой интерфейс использовался, поскольку могут существовать интерфейсы с одинаковыми значениями адреса/подсети. Он основан на Linux, и нет необходимости в переносимости кода.

Все, что я смог найти, это функции для получения всех интерфейсов или одного интерфейса по индексу. Я не смог найти способ получить интерфейс, связанный с принятым сокетом TCP.

Любые идеи? Что-то я пропустил?

РЕДАКТИРОВАТЬ: Повторюсь, IP-адреса в моем случае не уникальны. Ни адреса назначения (сам сервер), ни адреса источника (клиенты). Да, это очень экстремальная схема IP.


person Leeor    schedule 11.05.2009    source источник
comment
Единственный правильный ответ здесь, учитывая, что вы ищете код для добавления на свой сервер, — это ответ от User1. Я проверил это, и это работает — вы должны принять это. См. также stackoverflow.com/questions/43659634/   -  person EML    schedule 28.04.2017


Ответы (9)


Используйте getsockname(), чтобы получить IP локального конца соединения TCP. Затем используйте getifaddrs(), чтобы найти соответствующий интерфейс:

struct sockaddr_in addr;
struct ifaddrs* ifaddr;
struct ifaddrs* ifa;
socklen_t addr_len;

addr_len = sizeof (addr);
getsockname(sock_fd, (struct sockaddr*)&addr, &addr_len);
getifaddrs(&ifaddr);

// look which interface contains the wanted IP.
// When found, ifa->ifa_name contains the name of the interface (eth0, eth1, ppp0...)
for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next)
{
    if (ifa->ifa_addr)
    {
        if (AF_INET == ifa->ifa_addr->sa_family)
        {
            struct sockaddr_in* inaddr = (struct sockaddr_in*)ifa->ifa_addr;

            if (inaddr->sin_addr.s_addr == addr.sin_addr.s_addr)
            {
                if (ifa->ifa_name)
                {
                    // Found it
                }
            }
        }
    }
}
freeifaddrs(ifaddr);

Выше приведен просто грязный пример, необходимы некоторые модификации:

  1. Добавить отсутствующие проверки ошибок
  2. поддержка IPv6
person SKi    schedule 23.06.2016
comment
Обратите внимание, что это if (inaddr->sin_addr.s_addr == addr.sin_addr.s_addr), вероятно, никогда не совпадает. Я бы посоветовал вам применить сетевую маску, чтобы немного улучшить шансы: if (inaddr->sin_addr.s_addr == (addr.sin_addr.s_addr & ((struct sockaddr_in*)ifa->ifa_netmask)->sin_addr.s_addr)) - person Alexis Wilke; 30.05.2018
comment
@AlexisWilke: мы без проблем использовали этот тип оператора if в производственном программном обеспечении в течение многих лет. Я не понимаю, почему он не соответствует ни одному элементу в списке, потому что inaddr содержит локальный (не одноранговый или подсетевой) IP-адрес. - person SKi; 18.06.2018
comment
Правильно, в моем случае я хочу найти интерфейс, соответствующий любому адресу, поэтому у меня может быть 192.168.3.111, который не будет соответствовать 192.168.3.0, если я сначала не применю маску интерфейса. - person Alexis Wilke; 19.06.2018
comment
Несколько интерфейсов могут принадлежать одной и той же IP-сети, несколько интерфейсов могут даже иметь один и тот же IP-адрес. Какой интерфейс используется для отправки трафика, можно определить только просмотрев таблицу маршрутизации. - person Mecki; 26.06.2020

В общем, вам не нужно знать, через какой интерфейс будут отправляться/получаться пакеты; это работа таблицы маршрутизации ядра. Трудно найти интерфейс для сокета, потому что на самом деле нет прямой связи. Маршрутизация пакетов может меняться в течение времени существования сокета в зависимости от информации о маршрутизации.

Для сокетов дейтаграмм (UDP) вы можете использовать getsockopt(s, IPPROTO_IP, IP_PKTINFO, ...); см. getsockopt(2) и ip(7).

Для потоковых (TCP) сокетов одним из вариантов может быть открытие нескольких прослушивающих сокетов, по одному для каждого интерфейса в системе, и использование setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, ...) для привязки каждого к одному интерфейсу; см. setsockopt(2) и socket(7).

person Kieron    schedule 11.05.2009
comment
Да, установка отдельного сокета для каждого интерфейса сработает, но, учитывая, что существуют десятки интерфейсов, и они динамически активируются и отключаются, этого я хочу избежать. - person Leeor; 12.05.2009

Вот код C++11 для поиска имени интерфейса сокета:

std::string to_string(sockaddr_in const& addr)
{
    char buf[INET_ADDRSTRLEN];
    if (inet_ntop(AF_INET, &addr.sin_addr, buf, sizeof(buf)) == nullptr)
    {
        std::clog << "inet_ntop: " << strerror(errno) << '\n';
        return {};
    }
    return buf;
}

std::string to_string(sockaddr_in6 const& addr)
{
    char buf[INET6_ADDRSTRLEN];
    if (inet_ntop(AF_INET6, &addr.sin6_addr, buf, sizeof(buf)) == nullptr)
    {
        std::clog << "inet_ntop: " << strerror(errno) << '\n';
        return {};
    }
    return buf;
}

std::string to_string(sockaddr_storage const& addr, socklen_t len)
{
    switch (addr.ss_family)
    {
    case AF_INET:
    {
        auto& a = reinterpret_cast<sockaddr_in const&>(addr);
        if (len < sizeof(a))
        {
            std::clog << "Invalid sockaddr length: " << len << '\n';
            return {};
        }
        return to_string(a);
    }
    case AF_INET6:
    {
        auto& a = reinterpret_cast<sockaddr_in6 const&>(addr);
        if (len < sizeof(a))
        {
            std::clog << "Invalid sockaddr length: " << len << '\n';
            return {};
        }
        return to_string(a);
    }
    default:
    {
        std::clog << "Invalid sockaddr family: " << addr.ss_family << '\n';
        return {};
    }
    }
}

std::string get_iface_name(sockaddr_in const& addr)
{
    ifaddrs *ifa = nullptr;
    if (getifaddrs(&ifa) == -1)
    {
        std::clog << "getifaddrs: " << strerror(errno) << '\n';
        return {};
    }
    std::unique_ptr<ifaddrs, void(*)(ifaddrs*)>
        finally{ifa, freeifaddrs};

    for (; ifa; ifa = ifa->ifa_next)
    {
        if (!ifa->ifa_addr)
            continue;
        if (!ifa->ifa_name)
            continue;
        if (ifa->ifa_addr->sa_family != AF_INET)
            continue;
        auto& a = reinterpret_cast<sockaddr_in&>(*ifa->ifa_addr);
        if (a.sin_addr.s_addr == addr.sin_addr.s_addr)
            return ifa->ifa_name;
    }

    std::clog << "No interface found for IPv4 address " << to_string(addr) << '\n';
    return {};
}

std::string get_iface_name(sockaddr_in6 const& addr)
{
    ifaddrs *ifa = nullptr;
    if (getifaddrs(&ifa) == -1)
    {
        std::clog << "getifaddrs: " << strerror(errno) << '\n';
        return {};
    }
    std::unique_ptr<ifaddrs, void(*)(ifaddrs*)>
        finally{ifa, freeifaddrs};

    for (; ifa; ifa = ifa->ifa_next)
    {
        if (!ifa->ifa_addr)
            continue;
        if (!ifa->ifa_name)
            continue;
        if (ifa->ifa_addr->sa_family != AF_INET6)
            continue;
        auto& a = reinterpret_cast<sockaddr_in6&>(*ifa->ifa_addr);
        if (memcmp(a.sin6_addr.s6_addr,
                   addr.sin6_addr.s6_addr,
                   sizeof(a.sin6_addr.s6_addr)) == 0)
            return ifa->ifa_name;
    }

    std::clog << "No interface found for IPv6 address " << to_string(addr) << '\n';
    return {};
}

std::string get_iface_name(sockaddr_storage const& addr, socklen_t len)
{
    switch (addr.ss_family)
    {
    case AF_INET:
    {
        auto& a = reinterpret_cast<sockaddr_in const&>(addr);
        if (len < sizeof(a))
        {
            std::clog << "Invalid sockaddr length: " << len << '\n';
            return {};
        }
        return get_iface_name(a);
    }
    case AF_INET6:
    {
        auto& a = reinterpret_cast<sockaddr_in6 const&>(addr);
        if (len < sizeof(a))
        {
            std::clog << "Invalid sockaddr length: " << len << '\n';
            return {};
        }
        return get_iface_name(a);
    }
    default:
    {
        std::clog << "Invalid sockaddr family: " << addr.ss_family << '\n';
        return {};
    }
    }
}

std::string get_iface_name(int sockfd)
{
    sockaddr_storage addr;
    socklen_t len = sizeof(addr);
    if (getsockname(sockfd, (sockaddr*)&addr, &len) == -1)
    {
        std::clog << "getsockname: " << strerror(errno) << '\n';
        return {};
    }
    std::clog << "getsockname '" << to_string(addr, len) << '\'' << '\n';
    return get_iface_name(addr, len);
}
person Waxrat    schedule 18.04.2018
comment
Сбой, если я устанавливаю маршрут для адреса детонации, чтобы он указывал на другой интерфейс. Процесс принятия решения о том, какой интерфейсный трафик будет приниматься, называется маршрутизацией, а таблица маршрутизации является соответствующим источником данных. - person Mecki; 26.06.2020

Таблица маршрутизации ядра решает, на какой интерфейс отправить пакет, отсюда и возможность связывания устройств. Беглый просмотр "Linux Socket Programming, Warren W. Gay" говорит о том, что указание интерфейса - это плохо, и что из-за динамики ядра (брандмауэр, переадресация) он более сложен.

Я бы предложил изменить вашу IP-схему таким образом, чтобы IP-информация говорила вам о вашем интерфейсе (ах), просматривая его так же, как это делает ifconfig, в противном случае вы стреляете себе в ногу с точки зрения дизайна.

1) Получите информацию об IP-адресе из сеанса TCP. 2) Найдите, для какого интерфейса (ов) это может быть допустимо.

Однако я буду продолжать искать API ядра. Вам не нужно знать это, абстракция существует по множеству веских причин.

Дополнительная мысль Если подумать над этим, кажется, что если оба интерфейса используют один и тот же IP-адрес, то должна быть разница в маршрутизации диапазонов клиентских адресов (в противном случае использовались бы оба интерфейса). Ваш сервер может проверить таблицу маршрутизации на основе IP-адреса клиента.

person Aiden Bell    schedule 11.05.2009
comment
IP-адреса клиентов также могут перекрываться. Разница, которая позволяет этим IP-адресам перекрываться, заключается в идентификаторе VLAN. Насколько я знаю, это тоже недоступно. - person Leeor; 11.05.2009
comment
вы, возможно, столкнулись с ситуацией, когда вы не можете использовать косвенные доказательства для определения интерфейса. Я не думаю, что будет способ, так как интерфейс является какой-то абстракцией от приложений выше ядра. Возможно, вы могли бы изменить свое ядро, чтобы предоставить некоторые данные через /proc/tcp_sessions_to_devs или что-то еще, но это ОЧЕНЬ - эээ - отчаянно? - person Aiden Bell; 12.05.2009
comment
Еще одна мысль. Попытка изучить библиотеки анализа интерфейса, которые использует Wireshark (libpcap), и посмотреть, как источник iftop перечисляет сеансы на устройстве. Может быть интересно. - person Aiden Bell; 14.05.2009
comment
Вы можете определить интерфейс, только взглянув на таблицу маршрутизации, а не на конфигурацию интерфейса. Несмотря на то, что к одной и той же IP-сети может быть подключено несколько интерфейсов, несколько интерфейсов могут иметь даже один и тот же IP-адрес. И независимо от того, через какой интерфейс обычно проходит трафик, я всегда могу добавить маршрут и заставить его проходить через другой интерфейс. Именно таблица маршрутизации решает, через какой интерфейс пакет покинет систему, и никто другой. Настройки интерфейса просто создают записи в таблице маршрутизации, но только таблица знает правду. - person Mecki; 26.06.2020

Я думаю, что использование getsockname() после accept() входящего соединения может быть тем, что вам нужно. Две функции getsockname() и getpeername() получают соответственно локальный и удаленный адреса, к которым привязан сокет. Оба должны быть действительны для полностью подключенного сокета TCP.

Редактировать: хотя это кажется верным для OpenBSD в соответствии с справочной страницей, справочная страница Linux значительно отличается, и поэтому getockname() после accept() в Linux почти наверняка бесполезен. Учит меня пользоваться памятью вместо того, чтобы все проверять. вздыхает

person Wuggy    schedule 11.05.2009
comment
на самом деле, я пробовал, и это решение работает в Linux и Windows. - person thang; 19.09.2014
comment
Неверное «имя» — возвращается адрес. OP хочет имя интерфейса, т.е. эт0/и т.д. Как говорится в документах glibc, функции и символы для работы с адресами сокетов были названы непоследовательно, иногда с использованием термина «имя», а иногда с использованием термина «адрес». - person EML; 28.04.2017

Посмотрите на адрес назначения.

Каждый интерфейс обычно привязан к уникальному адресу. Если несколько интерфейсов связаны друг с другом, вероятно, не имеет значения, какой из них используется.

Единственным исключением из этого является использование ipv6 anycast, но даже в этом случае у вас обычно не будет нескольких интерфейсов на одном хосте с одним и тем же IP-адресом.

person JimB    schedule 11.05.2009
comment
В этом случае интерфейсы не привязаны к уникальным адресам. Они тоже не привязаны. Они подключены к разным сетям, хотя имеют перекрывающиеся/идентичные IP-адреса. - person Leeor; 11.05.2009
comment
Если у вас есть TCP-соединение, у вас должен быть IP-адрес. Вам не нужно привязывать сокет к определенному адресу, но он нужен вашему интерфейсу. - person JimB; 11.05.2009
comment
Да, соединение имеет IP-адрес, но оно не уникально. Он не уникален для этого интерфейса и не уникален для этой сети. Мне действительно нужен способ получить интерфейс из самого сокета. - person Leeor; 11.05.2009
comment
Кажется, у вас необычный случай. Я оставлю свой ответ как есть, так как это логично для большинства людей, и @Aiden ответил тем, что я собирался добавить. - person JimB; 11.05.2009

Очевидно, это не то, что я очень глубоко изучил, не говоря уже о том, чтобы попробовать, это может быть что-то для корзины «настолько безумно, что это может сработать» ...

Если это действительно когда-либо будет только для Linux, вы можете написать собственный модуль netfilter, который отслеживает входящие подключения. и отмечает, через какой интерфейс они приходят, и записывает эту информацию куда-нибудь для чтения вашим серверным приложением.

person Kieron    schedule 12.05.2009

Предложение Kieron написать модуль netfilter, вероятно, является одним из способов попробовать, но я хотел бы воздержаться от написания моего самого первого модуля ядра для этого решения.

Я придумал еще один вариант использования исходного NAT и перевода исходного порта соединения, чтобы он соответствовал источнику соединения. Я могу назначить диапазоны портов для каждой сети и проверить их на сервере. Единственная проблема заключается в том, что исходный NAT в iptables выполняется в цепочке POSTROUTING, и я не уверен, что он используется для соединений, принимаемых этим хостом, поэтому мне может понадобиться использовать другой сервер.

Здесь нет простых решений, очень жаль, что я не могу получить имя/индекс интерфейса из сокета...

person Leeor    schedule 12.05.2009
comment
Другой вариант сетевого фильтра — перенаправить трафик на дополнительные локальные интерфейсы, настроенные с разными IP-адресами. - person Bell; 12.05.2009

Я добавляю еще один ответ и потенциальное решение после просмотра источника Wireshark и iftop, которые, похоже, имеют косвенно аналогичную функциональность.

Мне кажется, что вы можете использовать libpcap для обнюхивания интерфейсов. Предполагая, что вы можете идентифицировать какую-то уникальную часть сеанса TCP/IP, вы можете довольно просто отследить его до интерфейса, используя фильтры и отслеживание сеанса.

Никаких модулей ядра (и это хорошо работает с потоками)

http://www.ex-parrot.com/pdw/iftop/ Некоторые простой источник, чтобы заглянуть на www.tcpdump.org/ для libpcap

Я думаю, вы также сможете сопоставлять VLAN, используя его.

Также wireshark может быть полезен для отладки. Надеюсь это поможет! С тех пор это было у меня в голове.

person Aiden Bell    schedule 14.05.2009
comment
Да, это звучит как еще один вариант. Спасибо за попытку! - person Leeor; 17.05.2009