diff --git a/.gitignore b/.gitignore index bc801315..bfdf091f 100644 --- a/.gitignore +++ b/.gitignore @@ -104,3 +104,6 @@ lint/tmp/ .DS_Store /crowdin.properties + +# CodeQL temporary files +_codeql_detected_source_root diff --git a/app/src/main/jni/netguard/ip.c b/app/src/main/jni/netguard/ip.c index 0ac7f0d8..eb56554d 100644 --- a/app/src/main/jni/netguard/ip.c +++ b/app/src/main/jni/netguard/ip.c @@ -384,6 +384,37 @@ void handle_ip(const struct arguments *args, } else { if (protocol == IPPROTO_UDP) block_udp(args, pkt, length, payload, uid); + else if (protocol == IPPROTO_TCP) { + // For TLS connections with SNI_PENDING state, send RST to close the fake connection + // This ensures the app knows the connection is blocked, without ever connecting to tracker + const uint8_t pkt_version = (*pkt) >> 4; + const struct iphdr *ip4 = (struct iphdr *) pkt; + const struct ip6_hdr *ip6 = (struct ip6_hdr *) pkt; + const struct tcphdr *tcphdr = (struct tcphdr *) payload; + + // Search for SNI_PENDING session to send RST + struct ng_session *cur = args->ctx->ng_session; + while (cur != NULL && + !(cur->protocol == IPPROTO_TCP && + cur->tcp.version == pkt_version && + cur->tcp.source == tcphdr->source && cur->tcp.dest == tcphdr->dest && + (pkt_version == 4 ? cur->tcp.saddr.ip4 == ip4->saddr && + cur->tcp.daddr.ip4 == ip4->daddr + : memcmp(&cur->tcp.saddr.ip6, &ip6->ip6_src, 16) == 0 && + memcmp(&cur->tcp.daddr.ip6, &ip6->ip6_dst, 16) == 0))) + cur = cur->next; + + if (cur != NULL && cur->tcp.sni_state == SNI_PENDING) { + log_android(ANDROID_LOG_WARN, "Blocked tracker via SNI - no connection made to %s/%u", + dest, dport); + // Account for received data in sequence numbers before RST + const uint8_t tcpoptlen = (uint8_t) ((tcphdr->doff - 5) * 4); + const uint8_t *data_ptr = payload + sizeof(struct tcphdr) + tcpoptlen; + const uint16_t datalen = (const uint16_t) (length - (data_ptr - pkt)); + cur->tcp.remote_seq += datalen; + write_rst(args, &cur->tcp); + } + } log_android(ANDROID_LOG_WARN, "Address v%d p%d %s/%u syn %d not allowed", version, protocol, dest, dport, syn); diff --git a/app/src/main/jni/netguard/netguard.h b/app/src/main/jni/netguard/netguard.h index 3d7898c6..afb26760 100644 --- a/app/src/main/jni/netguard/netguard.h +++ b/app/src/main/jni/netguard/netguard.h @@ -75,6 +75,9 @@ #define SOCKS5_CONNECT 4 #define SOCKS5_CONNECTED 5 +// SNI interception state - used to catch TLS SNI without connecting to remote server +#define SNI_PENDING 1 // Waiting for ClientHello with SNI + struct context { pthread_mutex_t lock; int pipefds[2]; @@ -191,6 +194,7 @@ struct tcp_session { uint8_t state; uint8_t socks5; + uint8_t sni_state; // SNI interception state for TLS connections struct segment *forward; int checkedHostname; diff --git a/app/src/main/jni/netguard/tcp.c b/app/src/main/jni/netguard/tcp.c index a0c76ede..e66cab8c 100644 --- a/app/src/main/jni/netguard/tcp.c +++ b/app/src/main/jni/netguard/tcp.c @@ -689,6 +689,9 @@ jboolean handle_tcp(const struct arguments *args, if (tcphdr->urg) return 1; + // Check if this is a TLS connection (port 443) that needs SNI interception + int is_tls_sni = (ntohs(tcphdr->dest) == 443); + // Check session if (cur == NULL) { if (tcphdr->syn) { @@ -755,6 +758,7 @@ jboolean handle_tcp(const struct arguments *args, s->tcp.dest = tcphdr->dest; s->tcp.state = TCP_LISTEN; s->tcp.socks5 = SOCKS5_NONE; + s->tcp.sni_state = 0; s->tcp.forward = NULL; s->next = NULL; @@ -770,33 +774,60 @@ jboolean handle_tcp(const struct arguments *args, s->tcp.forward->next = NULL; } - // Open socket - s->socket = open_tcp_socket(args, &s->tcp, redirect); - if (s->socket < 0) { - // Remote might retry - ng_free(s, __FILE__, __LINE__); - return 0; - } + // For TLS connections: delay socket opening until SNI is extracted + // This allows us to detect trackers before connecting to them + if (is_tls_sni && allowed) { + log_android(ANDROID_LOG_INFO, "%s SNI pending - fake SYN-ACK without connecting", packet); + s->socket = -1; // No socket yet + s->tcp.sni_state = SNI_PENDING; + s->tcp.recv_window = s->tcp.send_window; + + // Send fake SYN-ACK to the app + s->tcp.remote_seq++; // remote SYN + if (write_syn_ack(args, &s->tcp) >= 0) { + s->tcp.time = time(NULL); + s->tcp.local_seq++; // local SYN + s->tcp.state = TCP_SYN_RECV; + + // Add session to list only on success + s->next = args->ctx->ng_session; + args->ctx->ng_session = s; + } else { + // Failed to send fake SYN-ACK, clean up + log_android(ANDROID_LOG_ERROR, "%s failed to send fake SYN-ACK", packet); + clear_tcp_data(&s->tcp); + ng_free(s, __FILE__, __LINE__); + return 0; + } + } else { + // Normal flow: open socket immediately + s->socket = open_tcp_socket(args, &s->tcp, redirect); + if (s->socket < 0) { + // Remote might retry + ng_free(s, __FILE__, __LINE__); + return 0; + } - s->tcp.recv_window = get_receive_window(s); + s->tcp.recv_window = get_receive_window(s); - log_android(ANDROID_LOG_DEBUG, "TCP socket %d lport %d", - s->socket, get_local_port(s->socket)); + log_android(ANDROID_LOG_DEBUG, "TCP socket %d lport %d", + s->socket, get_local_port(s->socket)); - // Monitor events - memset(&s->ev, 0, sizeof(struct epoll_event)); - s->ev.events = EPOLLOUT | EPOLLERR; - s->ev.data.ptr = s; - if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, s->socket, &s->ev)) - log_android(ANDROID_LOG_ERROR, "epoll add tcp error %d: %s", - errno, strerror(errno)); + // Monitor events + memset(&s->ev, 0, sizeof(struct epoll_event)); + s->ev.events = EPOLLOUT | EPOLLERR; + s->ev.data.ptr = s; + if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, s->socket, &s->ev)) + log_android(ANDROID_LOG_ERROR, "epoll add tcp error %d: %s", + errno, strerror(errno)); - s->next = args->ctx->ng_session; - args->ctx->ng_session = s; + s->next = args->ctx->ng_session; + args->ctx->ng_session = s; - if (!allowed) { - log_android(ANDROID_LOG_WARN, "%s resetting blocked session", packet); - write_rst(args, &s->tcp); + if (!allowed) { + log_android(ANDROID_LOG_WARN, "%s resetting blocked session", packet); + write_rst(args, &s->tcp); + } } } else { log_android(ANDROID_LOG_WARN, "%s unknown session", packet); @@ -850,7 +881,47 @@ jboolean handle_tcp(const struct arguments *args, // Do not change the order of the conditions - // Queue data to forward + // Handle SNI interception for TLS connections + // When in SNI_PENDING state and allowed (SNI was checked in ip.c), open real socket + if (cur->tcp.sni_state == SNI_PENDING && datalen > 0) { + // SNI was allowed in ip.c (otherwise we wouldn't get here) + // Now open the real connection to the server + log_android(ANDROID_LOG_INFO, "%s SNI allowed - connecting to real server after fake handshake", + session); + + cur->socket = open_tcp_socket(args, &cur->tcp, redirect); + if (cur->socket < 0) { + log_android(ANDROID_LOG_ERROR, "%s failed to open socket after SNI check", session); + write_rst(args, &cur->tcp); + return 0; + } + + cur->tcp.recv_window = get_receive_window(cur); + log_android(ANDROID_LOG_DEBUG, "TCP socket %d lport %d (after SNI)", + cur->socket, get_local_port(cur->socket)); + + // Queue the ClientHello data to be sent when socket connects + // Note: queue_tcp uses the TCP sequence number from tcphdr + // remote_seq should NOT be updated here - it will be updated when data is forwarded + queue_tcp(args, tcphdr, session, &cur->tcp, data, datalen); + + // Monitor events for the new socket + memset(&cur->ev, 0, sizeof(struct epoll_event)); + cur->ev.events = EPOLLOUT | EPOLLERR; + cur->ev.data.ptr = cur; + if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, cur->socket, &cur->ev)) + log_android(ANDROID_LOG_ERROR, "epoll add tcp error %d: %s (after SNI)", + errno, strerror(errno)); + + // Clear SNI state - connection will proceed as normal + // State remains TCP_ESTABLISHED, queued data will be sent when socket connects + // remote_seq will be updated and ACK sent when data is forwarded in check_tcp_socket + cur->tcp.sni_state = 0; + + return 1; + } + + // Queue data to forward (only for non-SNI_PENDING sessions) if (datalen) { if (cur->socket < 0) { log_android(ANDROID_LOG_ERROR, "%s data while local closed", session);