La solución que encontré para trabajar es la siguiente. En primer lugar, tenemos que cambiar la configuración de ARP y RP. A /etc/sysctl.conf, agregue lo siguiente y reinicie (también hay un comando para configurarlo dinámicamente):
net.ipv4.conf.default.arp_filter = 1
net.ipv4.conf.default.rp_filter = 2
net.ipv4.conf.all.arp_filter = 1
net.ipv4.conf.all.rp_filter = 2
El filtro arp era necesario para permitir que las respuestas de eth0 se enrutaran a través de una WAN. La opción de filtro rp era necesaria para asociar estrictamente los paquetes entrantes con la NIC en la que entraron (a diferencia del modelo débil que los asocia con cualquier NIC que coincida con la subred). Un comentario de EJP me llevó a este paso crítico.
Después de eso, SO_BINDTODEVICE comenzó a funcionar. Cada uno de los dos sockets estaba vinculado a su propia NIC y, por lo tanto, podía saber de qué NIC procedía un mensaje según el socket de donde procedía.
s=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
rc=setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, nic, IF_NAMESIZE);
memset((char *) &si_me, 0, sizeof(si_me));
si_me.sin_family = AF_INET;
si_me.sin_port = htons(LISTEN_PORT);
si_me.sin_addr.s_addr = htonl(INADDR_ANY);
rc=bind(s, (struct sockaddr *)&si_me, sizeof(si_me))
A continuación, quería responder a los datagramas entrantes con datagramas cuya dirección de origen es la de la NIC de la que provino la solicitud original. La respuesta allí es simplemente buscar la dirección de esa NIC y vincular el socket de salida a esa dirección (usando bind
).
s=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
get_nic_addr(nics, (struct sockaddr *)&sa)
sa.sin_port = 0;
rc = bind(s, (struct sockaddr *)&sa, sizeof(struct sockaddr));
sendto(s, ...);
int get_nic_addr(const char *nic, struct sockaddr *sa)
{
struct ifreq ifr;
int fd, r;
fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) return -1;
ifr.ifr_addr.sa_family = AF_INET;
strncpy(ifr.ifr_name, nic, IFNAMSIZ);
r = ioctl(fd, SIOCGIFADDR, &ifr);
if (r < 0) { ... }
close(fd);
*sa = *(struct sockaddr *)&ifr.ifr_addr;
return 0;
}
(Tal vez buscar la dirección de la NIC cada vez parece un desperdicio, pero es mucho más código para estar informado cuando cambia una dirección, y estas transacciones ocurren solo una vez cada pocos segundos en un sistema que no funciona con batería).
Puede obtener la dirección de destino utilizada por el remitente a través del IP_RECVDSTADDR
opción si su plataforma lo admite, usando recvmsg()
. Es bastante complicado, descrito en Programación de red Unix, volumen I, 3ra edición, #22.2, y en el hombre página.
Con respecto a su edición, se enfrenta a lo que se conoce como el "modelo de sistema de extremo débil" de TCP/IP. Básicamente, una vez que llega un paquete, el sistema puede optar por entregarlo a través de cualquier interfaz adecuada que escuche el puerto correcto. Se trata en alguna parte de los RFC de TCP/IP.
Estás pasando un valor ilegal a setsockopt
.
rc=setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, nic, strlen(nic));
La página de manual dice de SO_BIND_TO_DEVICE
:
La opción aprobada es una terminación nula de longitud variable cadena de nombre de interfaz con el tamaño máximo de IFNAMSIZ
strlen
no incluye el nulo de terminación. Puedes probar:
rc=setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, nic, 1 + strlen(nic));
dnsmasq
tiene esto funcionando correctamente, y usa
setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, intname, IF_NAMESIZE)