La soluzione che ho trovato funzionante è la seguente. Prima di tutto, dobbiamo modificare le impostazioni ARP e RP. A /etc/sysctl.conf, aggiungi quanto segue e riavvia (c'è anche un comando per impostarlo dinamicamente):
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
Il filtro arp era necessario per consentire l'instradamento delle risposte da eth0 su una WAN. L'opzione del filtro rp era necessaria per associare rigorosamente i pacchetti in arrivo alla NIC da cui sono entrati (al contrario del modello debole che li associa a qualsiasi NIC che corrisponda alla sottorete). Un commento di EJP mi ha portato a questo passaggio critico.
Successivamente, SO_BINDTODEVICE ha iniziato a funzionare. Ciascuno dei due socket era associato alla propria NIC e quindi potevo stabilire da quale NIC proveniva un messaggio in base al socket da cui proveniva.
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))
Successivamente, volevo rispondere ai datagrammi in arrivo con datagrammi il cui indirizzo di origine è quello del NIC da cui proveniva la richiesta originale. La risposta è semplicemente cercare l'indirizzo di quella NIC e associare il socket in uscita a quell'indirizzo (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;
}
(Forse cercare l'indirizzo del NIC ogni volta sembra uno spreco, ma è molto più codice per essere informati quando un indirizzo cambia, e queste transazioni si verificano solo una volta ogni pochi secondi su un sistema che non funziona a batteria.)
Puoi ottenere l'indirizzo di destinazione utilizzato dal mittente tramite il IP_RECVDSTADDR
opzione se la tua piattaforma lo supporta, utilizzando recvmsg()
. È piuttosto complicato, descritto in Unix Network Programming volume I, 3a edizione, #22.2, e nell'uomo pagina.
Per quanto riguarda la tua modifica, ti trovi di fronte a quello che è noto come il "modello di sistema dell'estremità debole" di TCP/IP. Fondamentalmente una volta che un pacchetto arriva, il sistema può scegliere di consegnarlo tramite qualsiasi interfaccia appropriata in ascolto sulla porta corretta. È discusso da qualche parte nelle RFC TCP/IP.
Stai passando un valore non valido a setsockopt
.
rc=setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, nic, strlen(nic));
La pagina man dice di SO_BIND_TO_DEVICE
:
L'opzione passata è con terminazione nulla di lunghezza variabile stringa del nome dell'interfaccia con la dimensione massima di IFNAMSIZ
strlen
non include il null di terminazione. Puoi provare:
rc=setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, nic, 1 + strlen(nic));
dnsmasq
funziona correttamente e utilizza
setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, intname, IF_NAMESIZE)