// tun_route.c - Cross-platform system routing table management // Linux: netlink sockets, Windows: IP Helper API, BSD: routing sockets #include "tun_route.h" #include "config_parser.h" #include "../lib/debug_config.h" #include #include #include #include #include "../lib/mem.h" // Platform detection #ifdef _WIN32 #include #include #include #include #elif defined(__linux__) #include #include #include #include #include #include #elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__APPLE__) #include #include #include #include #include #include #endif // Convert prefix length to netmask static uint32_t prefix_to_netmask(uint8_t prefix_len) { return (prefix_len == 0) ? 0 : htonl(~((1U << (32 - prefix_len)) - 1)); } #ifdef __linux__ // Linux netlink implementation struct nl_req { struct nlmsghdr nl; struct rtmsg rt; char buf[256]; }; static int netlink_route(int ifindex, uint32_t network, uint8_t prefix_len, int cmd, int flags) { int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (fd < 0) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Failed to create netlink socket: %s", strerror(errno)); return -1; } // Bind to receive responses struct sockaddr_nl addr; memset(&addr, 0, sizeof(addr)); addr.nl_family = AF_NETLINK; addr.nl_pid = getpid(); addr.nl_groups = 0; if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Failed to bind netlink socket: %s", strerror(errno)); close(fd); return -1; } // Prepare destination address (kernel) struct sockaddr_nl dest; memset(&dest, 0, sizeof(dest)); dest.nl_family = AF_NETLINK; dest.nl_pid = 0; // kernel dest.nl_groups = 0; struct nl_req req; memset(&req, 0, sizeof(req)); req.nl.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); req.nl.nlmsg_type = cmd; // RTM_NEWROUTE or RTM_DELROUTE req.nl.nlmsg_flags = flags | NLM_F_REQUEST | NLM_F_ACK; // Request ACK from kernel req.nl.nlmsg_seq = 1; req.nl.nlmsg_pid = getpid(); req.rt.rtm_family = AF_INET; req.rt.rtm_table = RT_TABLE_MAIN; req.rt.rtm_protocol = RTPROT_STATIC; req.rt.rtm_scope = RT_SCOPE_UNIVERSE; req.rt.rtm_type = RTN_UNICAST; req.rt.rtm_dst_len = prefix_len; // Add destination address attribute struct rtattr *rta = (struct rtattr *)(((char *)&req) + NLMSG_ALIGN(req.nl.nlmsg_len)); rta->rta_type = RTA_DST; rta->rta_len = RTA_LENGTH(4); memcpy(RTA_DATA(rta), &network, 4); req.nl.nlmsg_len = NLMSG_ALIGN(req.nl.nlmsg_len) + RTA_ALIGN(rta->rta_len); // Add output interface attribute rta = (struct rtattr *)(((char *)&req) + NLMSG_ALIGN(req.nl.nlmsg_len)); rta->rta_type = RTA_OIF; rta->rta_len = RTA_LENGTH(4); memcpy(RTA_DATA(rta), &ifindex, 4); req.nl.nlmsg_len = NLMSG_ALIGN(req.nl.nlmsg_len) + RTA_ALIGN(rta->rta_len); if (sendto(fd, &req, req.nl.nlmsg_len, 0, (struct sockaddr *)&dest, sizeof(dest)) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Failed to send netlink message: %s", strerror(errno)); close(fd); return -1; } DEBUG_INFO(DEBUG_CATEGORY_TUN, "Netlink message sent: seq=%u pid=%u len=%d", req.nl.nlmsg_seq, req.nl.nlmsg_pid, req.nl.nlmsg_len); // Wait for response with timeout using poll struct pollfd pfd = { .fd = fd, .events = POLLIN }; int poll_ret = poll(&pfd, 1, 1000); // 1 second timeout if (poll_ret < 0) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "poll() failed: %s", strerror(errno)); close(fd); return -1; } else if (poll_ret == 0) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Timeout waiting for netlink response (sent %d bytes)", req.nl.nlmsg_len); close(fd); return -1; } DEBUG_INFO(DEBUG_CATEGORY_TUN, "Poll returned: revents=%d", pfd.revents); // Receive response char reply[4096]; ssize_t len = recv(fd, reply, sizeof(reply), 0); if (len < 0) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Failed to receive netlink response: %s", strerror(errno)); close(fd); return -1; } DEBUG_INFO(DEBUG_CATEGORY_TUN, "Received netlink response: len=%d", (int)len); struct nlmsghdr *nl_hdr = (struct nlmsghdr *)reply; DEBUG_INFO(DEBUG_CATEGORY_TUN, "Netlink msg: type=%d seq=%d pid=%d", nl_hdr->nlmsg_type, nl_hdr->nlmsg_seq, nl_hdr->nlmsg_pid); if (nl_hdr->nlmsg_type == NLMSG_ERROR) { struct nlmsgerr *err = (struct nlmsgerr *)NLMSG_DATA(nl_hdr); if (err->error < 0) { errno = -err->error; DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Netlink error: %s", strerror(errno)); close(fd); return -1; } } close(fd); return 0; } // Fallback: use ip route command static int ip_route_cmd(const char *ifname, uint32_t network, uint8_t prefix_len, const char *cmd) { struct in_addr addr; addr.s_addr = network; char cmdline[256]; snprintf(cmdline, sizeof(cmdline), "ip route %s %s/%d dev %s >/dev/null 2>&1", cmd, ip_to_str(&addr, AF_INET).str, prefix_len, ifname); int ret = system(cmdline); return (ret == 0) ? 0 : -1; } int tun_route_add(uint32_t ifindex, const char *ifname, uint32_t network, uint8_t prefix_len) { if (ifindex == 0 && (!ifname || !ifname[0])) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Interface index and name are both invalid"); return -1; } network = htonl(network); // Try netlink first if (ifindex != 0 && netlink_route(ifindex, network, prefix_len, RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL) == 0) { return 0; } // Fallback to ip route command DEBUG_WARN(DEBUG_CATEGORY_TUN, "Netlink failed, trying ip route command for ifindex %u", ifindex); const char *dev = ifname ? ifname : ""; return ip_route_cmd(dev, ntohl(network), prefix_len, "add"); } int tun_route_del(uint32_t ifindex, const char *ifname, uint32_t network, uint8_t prefix_len) { if (ifindex == 0 && (!ifname || !ifname[0])) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "ifindex and ifname are both invalid"); return -1; } network = htonl(network); // Try netlink first if (ifindex != 0 && netlink_route(ifindex, network, prefix_len, RTM_DELROUTE, 0) == 0) { return 0; } // Fallback to ip route command DEBUG_WARN(DEBUG_CATEGORY_TUN, "Netlink failed, trying ip route command for ifindex %u", ifindex); const char *dev = ifname ? ifname : ""; return ip_route_cmd(dev, ntohl(network), prefix_len, "del"); } int tun_route_flush(const char *ifname) { // Use ip route flush as primary method - works reliably char cmdline[256]; snprintf(cmdline, sizeof(cmdline), "ip route flush dev %s >/dev/null 2>&1", ifname); int ret = system(cmdline); if (ret != 0) { // Fallback: try to list and delete routes one by one DEBUG_WARN(DEBUG_CATEGORY_TUN, "ip route flush failed, trying alternative method"); snprintf(cmdline, sizeof(cmdline), "ip route show dev %s 2>/dev/null | while read r; do ip route del $r dev %s >/dev/null 2>&1; done", ifname, ifname); ret = system(cmdline); } return (ret == 0) ? 0 : -1; } int tun_route_del_all(uint32_t ifindex, const char *ifname, struct CFG_ROUTE_ENTRY *routes) { int count = 0; struct CFG_ROUTE_ENTRY *entry = routes; while (entry) { uint32_t network = ntohl(entry->ip.addr.v4.s_addr); if (entry->ip.family == AF_INET) { if (tun_route_del(ifindex, ifname, network, entry->netmask) == 0) { count++; } } entry = entry->next; } DEBUG_INFO(DEBUG_CATEGORY_TUN, "Deleted %d routes via ifindex=%lu", count, (unsigned long)ifindex); return count; } #elif defined(_WIN32) // Windows IP Helper API implementation int tun_route_add(uint32_t ifindex, const char *ifname, uint32_t network, uint8_t prefix_len) { (void)ifname; // Not used on Windows if (ifindex == 0) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "ifindex is 0"); return -1; } uint32_t dest = htonl(network); uint32_t mask = prefix_to_netmask(prefix_len); char cmd[256]; struct in_addr dest_addr; dest_addr.s_addr = dest; snprintf(cmd, sizeof(cmd), "netsh interface ip add route %s/%d interface=%lu store=active >NUL 2>&1", inet_ntoa(dest_addr), prefix_len, (unsigned long)ifindex); int sys_ret = system(cmd); if (sys_ret == 0) { DEBUG_INFO(DEBUG_CATEGORY_TUN, "Added route %s/%d via ifindex=%lu", inet_ntoa(dest_addr), prefix_len, (unsigned long)ifindex); return 0; } MIB_IPFORWARDROW route; memset(&route, 0, sizeof(route)); route.dwForwardDest = dest; route.dwForwardMask = mask; route.dwForwardPolicy = 0; route.dwForwardNextHop = 0; route.dwForwardIfIndex = ifindex; route.dwForwardType = MIB_IPROUTE_TYPE_INDIRECT; route.dwForwardProto = MIB_IPPROTO_NETMGMT; route.dwForwardAge = 0; route.dwForwardNextHopAS = 0; route.dwForwardMetric1 = 1; route.dwForwardMetric2 = 0; route.dwForwardMetric3 = 0; route.dwForwardMetric4 = 0; route.dwForwardMetric5 = 0; DWORD ret = CreateIpForwardEntry(&route); if (ret != NO_ERROR) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Failed to add route %s/%d: %lu", inet_ntoa(dest_addr), prefix_len, ret); return -1; } return 0; } int tun_route_del(uint32_t ifindex, const char *ifname, uint32_t network, uint8_t prefix_len) { (void)ifname; // Not used on Windows if (ifindex == 0) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "ifindex is 0"); return -1; } struct in_addr dest_addr; dest_addr.s_addr = htonl(network); char cmd[256]; snprintf(cmd, sizeof(cmd), "netsh interface ip delete route %s/%d interface=%lu store=active >NUL 2>&1", inet_ntoa(dest_addr), prefix_len, (unsigned long)ifindex); int sys_ret = system(cmd); if (sys_ret == 0) { DEBUG_INFO(DEBUG_CATEGORY_TUN, "Deleted route %s/%d via ifindex=%lu", inet_ntoa(dest_addr), prefix_len, (unsigned long)ifindex); return 0; } MIB_IPFORWARDROW route; memset(&route, 0, sizeof(route)); route.dwForwardDest = htonl(network); route.dwForwardMask = prefix_to_netmask(prefix_len); route.dwForwardIfIndex = ifindex; DWORD ret = DeleteIpForwardEntry(&route); if (ret != NO_ERROR) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Failed to delete route %s/%d: %lu", inet_ntoa(dest_addr), prefix_len, ret); return -1; } return 0; } int tun_route_flush(const char *ifname) { PMIB_IPFORWARDTABLE table = NULL; DWORD size = 0; DWORD ret; DWORD ifindex = if_nametoindex(ifname); if (ifindex == 0) { return -1; } // Get table size ret = GetIpForwardTable(NULL, &size, 0); if (ret != ERROR_INSUFFICIENT_BUFFER) { return -1; } table = (PMIB_IPFORWARDTABLE)u_malloc(size); if (!table) return -1; ret = GetIpForwardTable(table, &size, 0); if (ret != NO_ERROR) { u_free(table); return -1; } // Delete all routes for this interface for (DWORD i = 0; i < table->dwNumEntries; i++) { if (table->table[i].dwForwardIfIndex == ifindex) { DeleteIpForwardEntry(&table->table[i]); } } u_free(table); return 0; } int tun_route_del_all(uint32_t ifindex, const char *ifname, struct CFG_ROUTE_ENTRY *routes) { int count = 0; struct CFG_ROUTE_ENTRY *entry = routes; while (entry) { uint32_t network = ntohl(entry->ip.addr.v4.s_addr); if (entry->ip.family == AF_INET) { if (tun_route_del(ifindex, ifname, network, entry->netmask) == 0) { count++; } } entry = entry->next; } DEBUG_INFO(DEBUG_CATEGORY_TUN, "Deleted %d routes via ifindex=%lu", count, (unsigned long)ifindex); return count; } #else // BSD / macOS routing socket implementation static int routing_socket_cmd(int ifindex, uint32_t network, uint8_t prefix_len, int cmd) { int fd = socket(PF_ROUTE, SOCK_RAW, AF_INET); if (fd < 0) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Failed to create routing socket: %s", strerror(errno)); return -1; } uint8_t buf[512]; memset(buf, 0, sizeof(buf)); struct rt_msghdr *rtm = (struct rt_msghdr *)buf; rtm->rtm_msglen = sizeof(struct rt_msghdr); rtm->rtm_version = RTM_VERSION; rtm->rtm_type = cmd; // RTM_ADD or RTM_DELETE rtm->rtm_index = ifindex; rtm->rtm_pid = getpid(); rtm->rtm_addrs = RTA_DST | RTA_NETMASK | RTA_IFP; rtm->rtm_flags = RTF_UP | RTF_GATEWAY | RTF_STATIC; // Destination address (network) struct sockaddr_in *sin = (struct sockaddr_in *)(rtm + 1); sin->sin_family = AF_INET; sin->sin_len = sizeof(struct sockaddr_in); sin->sin_addr.s_addr = htonl(network); rtm->rtm_msglen += sizeof(struct sockaddr_in); // Netmask sin++; sin->sin_family = AF_INET; sin->sin_len = sizeof(struct sockaddr_in); sin->sin_addr.s_addr = prefix_to_netmask(prefix_len); rtm->rtm_msglen += sizeof(struct sockaddr_in); // Interface name sin++; struct sockaddr_dl *sdl = (struct sockaddr_dl *)sin; sdl->sdl_family = AF_LINK; sdl->sdl_index = ifindex; sdl->sdl_len = sizeof(struct sockaddr_dl); rtm->rtm_msglen += sizeof(struct sockaddr_dl); if (write(fd, rtm, rtm->rtm_msglen) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Failed to send routing message: %s", strerror(errno)); close(fd); return -1; } // Read response ssize_t n = read(fd, buf, sizeof(buf)); if (n >= sizeof(struct rt_msghdr)) { if (rtm->rtm_errno != 0) { errno = rtm->rtm_errno; DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Routing error: %s", strerror(errno)); close(fd); return -1; } } close(fd); return 0; } // Fallback: use route command static int route_cmd(const char *ifname, uint32_t network, uint8_t prefix_len, const char *cmd) { struct in_addr addr; addr.s_addr = htonl(network); char cmdline[256]; snprintf(cmdline, sizeof(cmdline), "route %s -net %s/%d -interface %s >/dev/null 2>&1", cmd, ip_to_str(&addr, AF_INET).str, prefix_len, ifname); int ret = system(cmdline); return (ret == 0) ? 0 : -1; } int tun_route_add(uint32_t ifindex, const char *ifname, uint32_t network, uint8_t prefix_len) { if (ifindex == 0 && (!ifname || !ifname[0])) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Interface index and name are both invalid"); return -1; } // Try routing socket first if (ifindex != 0 && routing_socket_cmd(ifindex, network, prefix_len, RTM_ADD) == 0) { return 0; } // Fallback to route command DEBUG_WARN(DEBUG_CATEGORY_TUN, "Routing socket failed, trying route command for ifindex %u", ifindex); const char *dev = ifname ? ifname : ""; return route_cmd(dev, network, prefix_len, "add"); } int tun_route_del(uint32_t ifindex, const char *ifname, uint32_t network, uint8_t prefix_len) { if (ifindex == 0 && (!ifname || !ifname[0])) { DEBUG_ERROR(DEBUG_CATEGORY_TUN, "ifindex and ifname are both invalid"); return -1; } // Try routing socket first if (ifindex != 0 && routing_socket_cmd(ifindex, network, prefix_len, RTM_DELETE) == 0) { return 0; } // Fallback to route command DEBUG_WARN(DEBUG_CATEGORY_TUN, "Routing socket failed, trying route command for ifindex %u", ifindex); const char *dev = ifname ? ifname : ""; return route_cmd(dev, network, prefix_len, "delete"); } int tun_route_flush(const char *ifname) { // Use route command to flush routes - more reliable on BSD char cmdline[256]; snprintf(cmdline, sizeof(cmdline), "route -n show -interface %s 2>/dev/null | grep -v '^[[:space:]]*#' | " "awk '/^[0-9]+\\./{print $1}' | while read r; do route delete -net $r >/dev/null 2>&1; done", ifname); int ret = system(cmdline); return (ret == 0) ? 0 : -1; } int tun_route_del_all(uint32_t ifindex, const char *ifname, struct CFG_ROUTE_ENTRY *routes) { int count = 0; struct CFG_ROUTE_ENTRY *entry = routes; while (entry) { uint32_t network = ntohl(entry->ip.addr.v4.s_addr); if (entry->ip.family == AF_INET) { if (tun_route_del(ifindex, ifname, network, entry->netmask) == 0) { count++; } } entry = entry->next; } DEBUG_INFO(DEBUG_CATEGORY_TUN, "Deleted %d routes via ifindex=%lu", count, (unsigned long)ifindex); return count; } #endif // Platform-independent functions int tun_route_add_all(uint32_t ifindex, const char *ifname, struct CFG_ROUTE_ENTRY *routes) { int count = 0; struct CFG_ROUTE_ENTRY *entry = routes; while (entry) { uint32_t network = ntohl(entry->ip.addr.v4.s_addr); if (entry->ip.family == AF_INET) { if (tun_route_add(ifindex, ifname, network, entry->netmask) == 0) { struct in_addr addr; addr.s_addr = entry->ip.addr.v4.s_addr; DEBUG_INFO(DEBUG_CATEGORY_TUN, "Added route %s/%d via %s", ip_to_str(&addr, AF_INET).str, entry->netmask, ifname); count++; } else { struct in_addr addr; addr.s_addr = entry->ip.addr.v4.s_addr; DEBUG_ERROR(DEBUG_CATEGORY_TUN, "Failed to add route %s/%d via %s", ip_to_str(&addr, AF_INET).str, entry->netmask, ifname); } } entry = entry->next; } return count; }