diff --git a/dnstc.c b/dnstc.c index 43881d88..5f9feddf 100644 --- a/dnstc.c +++ b/dnstc.c @@ -68,7 +68,7 @@ static void dnstc_pkt_from_client(int fd, short what, void *_arg) ssize_t pktlen, outgoing; assert(fd == EVENT_FD(&self->listener)); - pktlen = red_recv_udp_pkt(fd, buf.raw, sizeof(buf), &clientaddr); + pktlen = red_recv_udp_pkt(fd, buf.raw, sizeof(buf), &clientaddr, NULL); if (pktlen == -1) return; diff --git a/redudp.c b/redudp.c index 9516a500..262af3ee 100644 --- a/redudp.c +++ b/redudp.c @@ -15,6 +15,7 @@ */ #include +#include #include #include #include @@ -33,30 +34,157 @@ #include "redudp.h" #define redudp_log_error(client, prio, msg...) \ - redsocks_log_write_plain(__FILE__, __LINE__, __func__, 0, &(client)->clientaddr, &(client)->instance->config.destaddr, prio, ## msg) + redsocks_log_write_plain(__FILE__, __LINE__, __func__, 0, &(client)->clientaddr, get_destaddr(client), prio, ## msg) #define redudp_log_errno(client, prio, msg...) \ - redsocks_log_write_plain(__FILE__, __LINE__, __func__, 1, &(client)->clientaddr, &(client)->instance->config.destaddr, prio, ## msg) + redsocks_log_write_plain(__FILE__, __LINE__, __func__, 1, &(client)->clientaddr, get_destaddr(client), prio, ## msg) static void redudp_pkt_from_socks(int fd, short what, void *_arg); static void redudp_drop_client(redudp_client *client); static void redudp_fini_instance(redudp_instance *instance); static int redudp_fini(); +static int redudp_transparent(int fd); typedef struct redudp_expected_assoc_reply_t { socks5_reply h; socks5_addr_ipv4 ip; } PACKED redudp_expected_assoc_reply; +struct bound_udp4_key { + struct in_addr sin_addr; + uint16_t sin_port; +}; + +struct bound_udp4 { + struct bound_udp4_key key; + int ref; + int fd; +}; + /*********************************************************************** * Helpers */ +// TODO: separate binding to privileged process (this operation requires uid-0) +static void* root_bound_udp4 = NULL; // to avoid two binds to same IP:port + +static int bound_udp4_cmp(const void *a, const void *b) +{ + return memcmp(a, b, sizeof(struct bound_udp4_key)); +} + +static void bound_udp4_mkkey(struct bound_udp4_key *key, const struct sockaddr_in *addr) +{ + memset(key, 0, sizeof(*key)); + key->sin_addr = addr->sin_addr; + key->sin_port = addr->sin_port; +} + +static int bound_udp4_get(const struct sockaddr_in *addr) +{ + struct bound_udp4_key key; + struct bound_udp4 *node, **pnode; + + bound_udp4_mkkey(&key, addr); + // I assume, that memory allocation for lookup is awful, so I use + // tfind/tsearch pair instead of tsearch/check-result. + pnode = tfind(&key, &root_bound_udp4, bound_udp4_cmp); + if (pnode) { + assert((*pnode)->ref > 0); + (*pnode)->ref++; + return (*pnode)->fd; + } + + node = calloc(1, sizeof(*node)); + if (!node) { + log_errno(LOG_ERR, "calloc"); + goto fail; + } + + node->key = key; + node->ref = 1; + node->fd = socket(AF_INET, SOCK_DGRAM, 0); + if (node->fd == -1) { + log_errno(LOG_ERR, "socket"); + goto fail; + } + + if (0 != redudp_transparent(node->fd)) + goto fail; + + if (0 != bind(node->fd, (struct sockaddr*)addr, sizeof(*addr))) { + log_errno(LOG_ERR, "bind"); + goto fail; + } + + pnode = tsearch(node, &root_bound_udp4, bound_udp4_cmp); + if (!pnode) { + log_errno(LOG_ERR, "tsearch(%p) == %p", node, pnode); + goto fail; + } + assert(node == *pnode); + + return node->fd; + +fail: + if (node) { + if (node->fd != -1) + redsocks_close(node->fd); + free(node); + } + return -1; +} + +static void bound_udp4_put(const struct sockaddr_in *addr) +{ + struct bound_udp4_key key; + struct bound_udp4 **pnode, *node; + void *parent; + + bound_udp4_mkkey(&key, addr); + pnode = tfind(&key, &root_bound_udp4, bound_udp4_cmp); + assert(pnode && (*pnode)->ref > 0); + + node = *pnode; + + node->ref--; + if (node->ref) + return; + + parent = tdelete(node, &root_bound_udp4, bound_udp4_cmp); + assert(parent); + + redsocks_close(node->fd); // expanding `pnode` to avoid use after free + free(node); +} + +static int redudp_transparent(int fd) +{ + int on = 1; + int error = setsockopt(fd, SOL_IP, IP_TRANSPARENT, &on, sizeof(on)); + if (error) + log_errno(LOG_ERR, "setsockopt(..., SOL_IP, IP_TRANSPARENT)"); + return error; +} + +static int do_tproxy(redudp_instance* instance) +{ + return instance->config.destaddr.sin_addr.s_addr == 0; +} + +static struct sockaddr_in* get_destaddr(redudp_client *client) +{ + if (do_tproxy(client->instance)) + return &client->destaddr; + else + return &client->instance->config.destaddr; +} + static void redudp_fill_preamble(socks5_udp_preabmle *preamble, redudp_client *client) { preamble->reserved = 0; preamble->frag_no = 0; /* fragmentation is not supported */ preamble->addrtype = socks5_addrtype_ipv4; - preamble->ip.addr = client->instance->config.destaddr.sin_addr.s_addr; - preamble->ip.port = client->instance->config.destaddr.sin_port; + preamble->ip.addr = get_destaddr(client)->sin_addr.s_addr; + preamble->ip.port = get_destaddr(client)->sin_port; } static struct evbuffer* socks5_mkmethods_plain_wrapper(void *p) @@ -104,6 +232,8 @@ static void redudp_drop_client(redudp_client *client) redudp_log_errno(client, LOG_ERR, "event_del"); redsocks_close(fd); } + if (client->sender_fd != -1) + bound_udp4_put(&client->destaddr); list_for_each_entry_safe(q, tmp, &client->queue, list) { list_del(&q->list); free(q); @@ -344,7 +474,8 @@ static void redudp_relay_connected(struct bufferevent *buffev, void *_arg) redudp_client *client = _arg; int do_password = socks5_is_valid_cred(client->instance->config.login, client->instance->config.password); int error; - redudp_log_error(client, LOG_DEBUG, ""); + char relayaddr_str[RED_INET_ADDRSTRLEN]; + redudp_log_error(client, LOG_DEBUG, "via %s", red_inet_ntop(&client->instance->config.relayaddr, relayaddr_str, sizeof(relayaddr_str))); if (!red_is_socket_connected_ok(buffev)) { redudp_log_errno(client, LOG_NOTICE, "red_is_socket_connected_ok"); @@ -382,7 +513,7 @@ static void redudp_timeout(int fd, short what, void *_arg) redudp_drop_client(client); } -static void redudp_first_pkt_from_client(redudp_instance *self, struct sockaddr_in *clientaddr, char *buf, size_t pktlen) +static void redudp_first_pkt_from_client(redudp_instance *self, struct sockaddr_in *clientaddr, struct sockaddr_in *destaddr, char *buf, size_t pktlen) { redudp_client *client = calloc(1, sizeof(*client)); @@ -395,9 +526,13 @@ static void redudp_first_pkt_from_client(redudp_instance *self, struct sockaddr_ INIT_LIST_HEAD(&client->queue); client->instance = self; memcpy(&client->clientaddr, clientaddr, sizeof(*clientaddr)); + if (destaddr) + memcpy(&client->destaddr, destaddr, sizeof(client->destaddr)); evtimer_set(&client->timeout, redudp_timeout, client); // XXX: self->relay_ss->init(client); + client->sender_fd = -1; // it's postponed until socks-server replies to avoid trivial DoS + client->relay = red_connect_relay(&client->instance->config.relayaddr, redudp_relay_connected, redudp_relay_error, client); if (!client->relay) @@ -431,7 +566,7 @@ static void redudp_pkt_from_socks(int fd, short what, void *_arg) assert(fd == EVENT_FD(&client->udprelay)); - pktlen = red_recv_udp_pkt(fd, pkt.buf, sizeof(pkt.buf), &udprelayaddr); + pktlen = red_recv_udp_pkt(fd, pkt.buf, sizeof(pkt.buf), &udprelayaddr, NULL); if (pktlen == -1) return; @@ -455,8 +590,8 @@ static void redudp_pkt_from_socks(int fd, short what, void *_arg) return; } - if (pkt.header.ip.port != client->instance->config.destaddr.sin_port || - pkt.header.ip.addr != client->instance->config.destaddr.sin_addr.s_addr) + if (pkt.header.ip.port != get_destaddr(client)->sin_port || + pkt.header.ip.addr != get_destaddr(client)->sin_addr.s_addr) { char buf[RED_INET_ADDRSTRLEN]; struct sockaddr_in pktaddr = { @@ -472,8 +607,18 @@ static void redudp_pkt_from_socks(int fd, short what, void *_arg) redsocks_time(&client->last_relay_event); redudp_bump_timeout(client); + if (do_tproxy(client->instance) && client->sender_fd == -1) { + client->sender_fd = bound_udp4_get(&client->destaddr); + if (client->sender_fd == -1) { + redudp_log_error(client, LOG_WARNING, "bound_udp4_get failure"); + return; + } + } + fwdlen = pktlen - sizeof(pkt.header); - outgoing = sendto(EVENT_FD(&client->instance->listener), + outgoing = sendto(do_tproxy(client->instance) + ? client->sender_fd + : EVENT_FD(&client->instance->listener), pkt.buf + sizeof(pkt.header), fwdlen, 0, (struct sockaddr*)&client->clientaddr, sizeof(client->clientaddr)); if (outgoing != fwdlen) { @@ -486,18 +631,21 @@ static void redudp_pkt_from_socks(int fd, short what, void *_arg) static void redudp_pkt_from_client(int fd, short what, void *_arg) { redudp_instance *self = _arg; - struct sockaddr_in clientaddr; + struct sockaddr_in clientaddr, destaddr, *pdestaddr; char buf[0xFFFF]; // UDP packet can't be larger then that ssize_t pktlen; redudp_client *tmp, *client = NULL; + pdestaddr = do_tproxy(self) ? &destaddr : NULL; + assert(fd == EVENT_FD(&self->listener)); - pktlen = red_recv_udp_pkt(fd, buf, sizeof(buf), &clientaddr); + pktlen = red_recv_udp_pkt(fd, buf, sizeof(buf), &clientaddr, pdestaddr); if (pktlen == -1) return; // TODO: this lookup may be SLOOOOOW. list_for_each_entry(tmp, &self->clients, list) { + // TODO: check destaddr if (0 == memcmp(&clientaddr, &tmp->clientaddr, sizeof(clientaddr))) { client = tmp; break; @@ -515,7 +663,7 @@ static void redudp_pkt_from_client(int fd, short what, void *_arg) } } else { - redudp_first_pkt_from_client(self, &clientaddr, buf, pktlen); + redudp_first_pkt_from_client(self, &clientaddr, pdestaddr, buf, pktlen); } } @@ -554,7 +702,6 @@ static int redudp_onenter(parser_section *section) instance->config.relayaddr.sin_family = AF_INET; instance->config.relayaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); instance->config.destaddr.sin_family = AF_INET; - instance->config.destaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); instance->config.max_pktqueue = 5; instance->config.udp_timeout = 30; instance->config.udp_timeout_stream = 180; @@ -614,6 +761,28 @@ static int redudp_init_instance(redudp_instance *instance) goto fail; } + if (do_tproxy(instance)) { + int on = 1; + char buf[RED_INET_ADDRSTRLEN]; + // iptables TPROXY target does not send packets to non-transparent sockets + if (0 != redudp_transparent(fd)) + goto fail; + + error = setsockopt(fd, SOL_IP, IP_RECVORIGDSTADDR, &on, sizeof(on)); + if (error) { + log_errno(LOG_ERR, "setsockopt(listener, SOL_IP, IP_RECVORIGDSTADDR)"); + goto fail; + } + + log_error(LOG_DEBUG, "redudp @ %s: TPROXY", red_inet_ntop(&instance->config.bindaddr, buf, sizeof(buf))); + } + else { + char buf1[RED_INET_ADDRSTRLEN], buf2[RED_INET_ADDRSTRLEN]; + log_error(LOG_DEBUG, "redudp @ %s: destaddr=%s", + red_inet_ntop(&instance->config.bindaddr, buf1, sizeof(buf1)), + red_inet_ntop(&instance->config.destaddr, buf2, sizeof(buf2))); + } + error = bind(fd, (struct sockaddr*)&instance->config.bindaddr, sizeof(instance->config.bindaddr)); if (error) { log_errno(LOG_ERR, "bind"); diff --git a/redudp.h b/redudp.h index 308bd33c..3f1d9d10 100644 --- a/redudp.h +++ b/redudp.h @@ -24,6 +24,8 @@ typedef struct redudp_client_t { list_head list; redudp_instance *instance; struct sockaddr_in clientaddr; + struct sockaddr_in destaddr; + int sender_fd; // shared between several clients socket (bound to `destaddr`) struct event timeout; struct bufferevent *relay; struct event udprelay; diff --git a/utils.c b/utils.c index 6e1f3af6..afdeea87 100644 --- a/utils.c +++ b/utils.c @@ -26,17 +26,54 @@ #include "utils.h" #include "redsocks.h" // for redsocks_close -int red_recv_udp_pkt(int fd, char *buf, size_t buflen, struct sockaddr_in *inaddr) +int red_recv_udp_pkt(int fd, char *buf, size_t buflen, struct sockaddr_in *inaddr, struct sockaddr_in *toaddr) { socklen_t addrlen = sizeof(*inaddr); ssize_t pktlen; - - pktlen = recvfrom(fd, buf, buflen, 0, (struct sockaddr*)inaddr, &addrlen); + struct msghdr msg; + struct iovec io; + char control[1024]; + + memset(&msg, 0, sizeof(msg)); + msg.msg_name = inaddr; + msg.msg_namelen = sizeof(*inaddr); + msg.msg_iov = &io; + msg.msg_iovlen = 1; + msg.msg_control = control; + msg.msg_controllen = sizeof(control); + io.iov_base = buf; + io.iov_len = buflen; + + pktlen = recvmsg(fd, &msg, 0); if (pktlen == -1) { log_errno(LOG_WARNING, "recvfrom"); return -1; } + if (toaddr) { + memset(toaddr, 0, sizeof(*toaddr)); + for (struct cmsghdr* cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) { + if ( + cmsg->cmsg_level == SOL_IP && + cmsg->cmsg_type == IP_ORIGDSTADDR && + cmsg->cmsg_len >= CMSG_LEN(sizeof(*toaddr)) + ) { + struct sockaddr_in* cmsgaddr = (struct sockaddr_in*)CMSG_DATA(cmsg); + char buf[RED_INET_ADDRSTRLEN]; + log_error(LOG_DEBUG, "IP_ORIGDSTADDR: %s", red_inet_ntop(cmsgaddr, buf, sizeof(buf))); + memcpy(toaddr, cmsgaddr, sizeof(*toaddr)); + } + else { + log_error(LOG_WARNING, "unexepcted cmsg (level,type) = (%d,%d)", + cmsg->cmsg_level, cmsg->cmsg_type); + } + } + if (toaddr->sin_family != AF_INET) { + log_error(LOG_WARNING, "(SOL_IP, IP_ORIGDSTADDR) not found"); + return -1; + } + } + if (addrlen != sizeof(*inaddr)) { log_error(LOG_WARNING, "unexpected address length %u instead of %zu", addrlen, sizeof(*inaddr)); return -1; diff --git a/utils.h b/utils.h index d3af00f2..c2277e91 100644 --- a/utils.h +++ b/utils.h @@ -44,7 +44,7 @@ char *redsocks_evbuffer_readline(struct evbuffer *buf); struct bufferevent* red_connect_relay(struct sockaddr_in *addr, evbuffercb writecb, everrorcb errorcb, void *cbarg); int red_socket_geterrno(struct bufferevent *buffev); int red_is_socket_connected_ok(struct bufferevent *buffev); -int red_recv_udp_pkt(int fd, char *buf, size_t buflen, struct sockaddr_in *inaddr); +int red_recv_udp_pkt(int fd, char *buf, size_t buflen, struct sockaddr_in *fromaddr, struct sockaddr_in *toaddr); int fcntl_nonblock(int fd);