如何指定主机名查询所用网络接口?Linux嵌入式场景技术求助
我正在开发一款运行于Linux+BusyBox环境的嵌入式设备应用,设备配备Wi-Fi和3G两个通信接口。应用要求优先通过Wi-Fi尝试连接,失败则切换至3G网络。目前我通过绑定方式强制连接使用指定接口,代码如下:
#include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <errno.h> #include <net/if.h> static void resolveDns(const char *hostname, struct addrinfo **destInfo) { int err; struct addrinfo hints; memset(&hints, 0, sizeof(hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; if ((err = getaddrinfo(hostname, "80", &hints, destInfo)) != 0) { fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(err)); exit(EXIT_FAILURE); } struct sockaddr_in *addr = (struct sockaddr_in *)((*destInfo)->ai_addr); printf("Destination IP: %s\n", inet_ntoa(addr->sin_addr)); } static void getComInterface(const char *iface, struct ifreq *ifr) { ifr->ifr_addr.sa_family = AF_INET; strcpy(ifr->ifr_name, iface); int sock = socket(AF_INET, SOCK_DGRAM, 0); int err = ioctl(sock, SIOCGIFADDR, ifr); close(sock); if (err) { fprintf(stderr, "ioctl error: %d\n", err); exit(EXIT_FAILURE); } printf("Interface IP: %s\n", inet_ntoa(((struct sockaddr_in *) &ifr->ifr_addr)->sin_addr)); } int main() { int err; struct ifreq ifr; getComInterface("wlan0", &ifr); struct addrinfo *destInfo; resolveDns("www.google.com", &destInfo); int s = socket(AF_INET, SOCK_STREAM, 0); err = bind(s, &ifr.ifr_addr, sizeof(ifr.ifr_addr)); if (err) { fprintf(stderr, "bind error = %d, %d\n", err, errno); exit(EXIT_FAILURE); } err = connect(s, destInfo->ai_addr, destInfo->ai_addrlen); if (err) { fprintf(stderr, "connect error = %d, %d \n", err, errno); exit(EXIT_FAILURE); } printf("Ok!\n"); freeaddrinfo(destInfo); close(s); return EXIT_SUCCESS; }
但该方案无法解决DNS查询阶段的接口指定问题。请问是否可以强制getaddrinfo使用选定的接口?或者有无无需断开其他接口即可强制所有连接使用指定接口的方法?另外,若您知晓Ubuntu等复杂系统下的实现方案,也请分享。
一、强制getaddrinfo使用指定接口
getaddrinfo本身没有直接指定接口的参数,但咱们可以绕开系统默认DNS路由,通过绑定socket到指定接口后手动发送DNS请求的方式解决。具体有两种思路:
思路1:手动实现DNS查询
自己构造DNS报文、发送请求并解析响应,完全掌控查询走的接口:
- 创建UDP socket,绑定到指定接口的IP地址(和你现有代码里的逻辑一致);
- 构造标准DNS查询报文,发送到指定的DNS服务器(比如公共DNS
8.8.8.8); - 解析DNS响应报文,提取目标域名对应的IP地址。
思路2:用resolv库指定查询接口
借助resolv.h里的res_ninit和res_nsend函数,绑定查询用的socket到指定接口,同时指定DNS服务器:
#include <resolv.h> // ... // 创建socket并绑定到wlan0 int sock = socket(AF_INET, SOCK_DGRAM, 0); struct ifreq ifr; getComInterface("wlan0", &ifr); bind(sock, &ifr.ifr_addr, sizeof(ifr.ifr_addr)); // 初始化resolv状态 res_state res = malloc(sizeof(struct __res_state)); res_ninit(res); // 指定DNS服务器(以Google DNS为例) struct sockaddr_in dns_addr; memset(&dns_addr, 0, sizeof(dns_addr)); dns_addr.sin_family = AF_INET; dns_addr.sin_port = htons(53); inet_pton(AF_INET, "8.8.8.8", &dns_addr.sin_addr); res->nsaddr_list[0] = *(struct sockaddr *)&dns_addr; res->nscount = 1; // 发送DNS查询 unsigned char buf[512]; int len = res_nsend(res, "www.google.com", strlen("www.google.com"), C_IN, T_A, buf, sizeof(buf)); // 后续解析buf中的响应提取IP...
二、嵌入式Linux+BusyBox下强制所有连接用指定接口(无需断开其他接口)
在BusyBox环境里,有两种高效的方案:
方法1:调整路由表优先级
通过修改路由的metric值(数值越小优先级越高),让系统优先选择Wi-Fi接口:
# 查看当前默认路由 ip route show default # 设置Wi-Fi默认路由的metric为10(比3G的metric小,比如3G设为20) ip route add default via <wlan0网关IP> dev wlan0 metric 10
当Wi-Fi不可用时,系统会自动切换到metric更高的3G路由。如果只针对特定目标地址,可以添加专用路由:
ip route add <目标IP>/32 dev wlan0
方法2:使用SO_BINDTODEVICE套接字选项
相比绑定IP,这个选项可以直接把socket绑定到接口名称(比如wlan0),不受接口IP变化影响,需要root权限:
int s = socket(AF_INET, SOCK_STREAM, 0); char iface[] = "wlan0"; if (setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, iface, strlen(iface)) < 0) { perror("setsockopt SO_BINDTODEVICE"); exit(EXIT_FAILURE); } // 后续执行connect或send操作即可
三、Ubuntu等复杂系统下的实现方案
Ubuntu这类系统有更灵活的网络管理工具,推荐以下几种方式:
1. 通过NetworkManager设置接口优先级
直接调整Wi-Fi和3G连接的路由metric,让系统自动切换:
# 查看所有连接的UUID nmcli connection show # 设置Wi-Fi连接的metric为10(优先级更高) nmcli connection modify <Wi-Fi连接UUID> ipv4.route-metric 10 # 设置3G连接的metric为20 nmcli connection modify <3G连接UUID> ipv4.route-metric 20
2. 基于进程UID的专属路由
如果只想让你的应用走指定接口,可通过ip rule实现隔离:
# 创建新的路由表 echo "100 wlanroute" >> /etc/iproute2/rt_tables # 为该表添加默认路由,走wlan0 ip route add default via <wlan0网关> dev wlan0 table wlanroute # 添加规则:你的应用UID使用这个路由表 ip rule add uidrange <你的应用UID>-<你的应用UID> table wlanroute
3. 绑定DNS查询接口
借助systemd-resolved可以指定DNS查询的接口,或者用socket绑定的方式(和嵌入式场景思路一致),确保DNS请求走选定接口。
内容的提问来源于stack exchange,提问作者Matheus Rossi Saciotto

