使用本地IPv6套接字发送UDP到本地IPv4地址

3

我想知道在Ubuntu 12.04上是否可以使用本地链路IPv6地址从套接字发送UDP数据包到同一无线网络上的设备,同时使用其IPv4地址。我已经成功地使用其IPv6地址向目标接口发送了UDP数据包。

我有一个带有IPv6地址的套接字,IPv6ONLY未设置:

int fd = socket(AF_INET6, SOCK_DGRAM, 0);
int no = 0;
setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&no, sizeof(no));
bind(fd, (sockaddr*)&sa, sizeof(sa));

我刚刚使用sendto函数向一个对等方发送数据:
struct sockaddr_in6 peer = create_ipv6_sockaddr(55555, "193.156.108.67", 0, 0, true);
sendto(sock,content,strlen(content),0,(struct sockaddr*)&peer,sizeof(peer));

此函数创建sockaddr_in6:

sockaddr_in6 create_ipv6_sockaddr(int port, string addr, uint32_t flowinfo, uint32_t scope_id, bool ipv4=false) {
    struct sockaddr_in6 si;
    si.sin6_family = AF_INET6;
    if (ipv4) {
        si.sin6_family = AF_INET;
    }
    si.sin6_port = htons(port);
    si.sin6_flowinfo = flowinfo; // Should be 0 or 'random' number to distinguish this flow
    if (ipv4) {
        addr = "::ffff:" + addr;
    }
    inet_pton(AF_INET6, addr.c_str(), &si.sin6_addr);
    if (!si.sin6_addr.s6_addr) {
        perror("Address is wrong..");
    }
    si.sin6_scope_id = scope_id;
    return si;
}

如果地址是IPv4地址,我会在其前面添加::ffff:,并将地址族设置为AF_INET。

这是否可行?如果可以,我做错了什么?

编辑:

总之,如果IPv6套接字绑定到特定的IP(或接口,不确定哪个),则无法发送IPv4消息。因此,IPv6套接字应使用通配符::0。sockaddr_in6结构的地址族仍应为AF_INET6,并带有前置的::ffff:。如果使用AF_INET4,则该代码不会返回错误,但根据我的经验,实际上不会发送任何消息。

实际上,如果您不是自己创建sockaddr结构,而是从getaddrinfo获取它,则可以直接将其传递给IPv6通配符套接字以发送。

多年后的编辑:

应@Erfan的要求,我找出了代码。恐怕我现在不再完全理解它,也不能告诉您它是否实际有效。

sockaddr_in6 create_ipv6_sockaddr(int port, string addr, uint32_t flowinfo, uint32_t scope_id, bool ipv4=false) {
    struct sockaddr_in6 si;
    si.sin6_family = AF_INET6;
    //  if (ipv4) {
    //      si.sin6_family = AF_INET;
    //  }
    si.sin6_port = htons(port);
    si.sin6_flowinfo = flowinfo; // Should be 0 or 'random' number to distinguish this flow
    if (ipv4) {
        addr = "::ffff:" + addr;
    }
    inet_pton(AF_INET6, addr.c_str(), &si.sin6_addr);
    if (!si.sin6_addr.s6_addr) {
        perror("Address is wrong..");
    }
    //  char s[40];
    //  inet_ntop(AF_INET6, &(si.sin6_addr), s, sizeof(s));
    //  fprintf(stderr, "Sockaddr %d %s\n", si.sin6_family, s);
    si.sin6_scope_id = scope_id;
    if (scope_id == 0 && !ipv4) {
        si.sin6_scope_id = ipv6_to_scope_id(&si); // Interface number
    }
    return si;
}

int ipv6_to_scope_id(sockaddr_in6 *find) {
    struct ifaddrs *addrs, *iap;
    struct sockaddr_in6 *sa;
    char host[NI_MAXHOST];

    getifaddrs(&addrs);
    for (iap = addrs; iap != NULL; iap = iap->ifa_next) {
        if (iap->ifa_addr && (iap->ifa_flags & IFF_UP) && iap->ifa_addr->sa_family == AF_INET6) {
            sa = (struct sockaddr_in6 *)(iap->ifa_addr);
            if (memcmp(&find->sin6_addr.s6_addr, &sa->sin6_addr.s6_addr, sizeof(sa->sin6_addr.s6_addr)) == 0) {
                getnameinfo(iap->ifa_addr, sizeof(struct sockaddr_in6), host, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);
                fprintf(stderr, "Found interface %s with scope %d\n", host, sa->sin6_scope_id);
                return sa->sin6_scope_id;
            }
        }
    }
    freeifaddrs(addrs);
    return 0;
}

并且进行绑定:

int bind_ipv6 (sockaddr_in6 sa) {
    int fd = socket(AF_INET6, SOCK_DGRAM, 0);
    if (fd < 0) {
        perror("Creating socket failed");
    }
    char str[40];
    inet_ntop(AF_INET6, &(sa.sin6_addr), str, sizeof(str));
    //  fprintf(stderr, "Bind to %s:%d\n", str, ntohs(sa.sin6_port));
    int no = 0;
    if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&no, sizeof(no)) < 0 ) { // Only works with wildcard
        perror("V6ONLY failed");
    }
    if (bind(fd, (sockaddr*)&sa, sizeof(sa)) < 0) {
        perror("Binding failed");
    }

    return fd;
}

这里有原始套接字的示例:http://www.pdbuchan.com/rawsock/rawsock.html。在udp6_to4.c示例中,如果是IPV4,则使用AF_INET调用inet_pton。 - lucasg
2个回答

4
当您使用::ffff:映射地址时,实际上是在使用IPv6软件套接字发送IPv4数据包。这使得支持两种协议更容易,但不允许混合使用不同的地址族。
网络上的数据包可以是IPv4数据包(具有IPv4源和目标地址)或IPv6数据包(具有IPv6源和目标地址)。::ffff:地址将永远不会在实际数据包中使用。它只存在于软件表示中。
如果您使用IPv6套接字与::ffff:地址通信,则在传输过程中它们将变成普通的IPv4数据包,并且您本地的IPv4地址将用于连接的一侧。

所以我现在仍然需要创建一个sockaddr_in6结构体。然后IPv6套接字将把它转换为具有本地IPv4地址作为源地址和IPv4目标地址的IPv4数据包。那么为什么我仍然会收到“网络不可达”的错误? - Vincent Ketelaars
尝试不使用 si.sin6_family = AF_INET;,并将其视为来自软件的IPv6。 - Sander Steffann
很抱歉,这不会改变任何事情。 - Vincent Ketelaars
1
哦,你在 sa 中放了什么?如果绑定到 IPv6 地址,则无法再从 IPv4 源地址发送数据包。只有当本地套接字绑定到通配符地址时,IPV6_V6ONLY=0 才有效。 - Sander Steffann
嗨@Oxidator,你能否在修改后发布可工作的代码?我在我的应用程序中遇到了同样的问题,必须说我在这些网络方面非常菜。你的帮助将使我的生活更轻松。提前感谢... :) - Erfan
显示剩余7条评论

0

::ffff:地址只用于IPV6_V6ONLY=0服务器套接字。

如果您想连接到IPv4地址,请创建一个匹配的套接字。

如果您只使用getaddrinfo(),可以简化您的生活:基本上,您将主机/端口组合放入其中,它将为您提供一个可连接的“地址条目”列表。这些条目包含创建和连接套接字所需的一切。

以下是一个简短的代码片段示例:

struct addrinfo hints = { .ai_socktype=SOCK_STREAM, .ai_flags = AI_CANONNAME };
struct addrinfo * ai;
char name[100];
int sockfd = -1;
int st = getaddrinfo(host, sport, &hints, &ai);
if (st < 0) {
    // complain and exit
}
// Now we have the wanted infos in ai.
struct addrinfo * aii;
for (aii=ai; aii; aii=aii->ai_next) {
    /* Create a socket */
    if((sockfd = socket(aii->ai_family, aii->ai_socktype, aii->ai_protocol)) == -1) {
        continue;
    }
    else {
        /* Establish a connection */
        if(connect(sockfd, aii->ai_addr, aii->ai_addrlen ) == -1) {
            close(sockfd);
            sockfd = -1;
            continue;
        }
        else {
            // Success. Get IP address connected to in a readable form.
            int st = getnameinfo(aii->ai_addr, aii->ai_addrlen, name, sizeof name,
                NULL, 0, NI_NUMERICHOST);
            if (st != 0) name[0] = '\0';
            break;
        }
    }
}
freeaddrinfo(ai);

我很好奇,代码的第一行应该如何运作? - Vincent Ketelaars
@Oxidator 第一行是给定“结构体”的初始化器(请参见这里这里)。 - glglgl
我非常喜欢这个语法,但是我的编译器由于某些原因不喜欢它。 - Vincent Ketelaars
@Oxidator 那你应该做出改变:查阅一下在你的系统(或者通用)中 struct addrinfo是如何定义的并且适当地设置值。或者使用struct addrinfo hints = {}; hints.ai_socktype=SOCK_STREAM; hints.ai_flags = AI_CANONNAME;。或者不要使用过时的编译器。;-) - glglgl
结构体addrinfo hints = { AI_CANONNAME, AF_UNSPEC, SOCK_STREAM };,请参见此处 - glglgl
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接