diff --git a/NetGuard/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java b/NetGuard/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java index 865d97a..0938a66 100644 --- a/NetGuard/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java +++ b/NetGuard/app/src/main/java/eu/faircode/netguard/ServiceSinkhole.java @@ -1260,6 +1260,11 @@ public class ServiceSinkhole extends VpnService implements SharedPreferences.OnS boolean filter = prefs.getBoolean("filter", false); boolean system = prefs.getBoolean("manage_system", false); + subnet = true; + + + Log.i(TAG, "filter value " + filter + " subnet: " + subnet + " tethering: " + tethering); + // Build VPN service Builder builder = new Builder(); builder.setSession(getString(R.string.app_name)); @@ -1277,6 +1282,7 @@ public class ServiceSinkhole extends VpnService implements SharedPreferences.OnS builder.addAddress(vpn6, 128); } + // DNS address if (filter) for (InetAddress dns : getDns(ServiceSinkhole.this)) { @@ -1309,7 +1315,13 @@ public class ServiceSinkhole extends VpnService implements SharedPreferences.OnS listExclude.add(new IPUtil.CIDR("192.168.0.0", 16)); } + Log.i(TAG, "filter value " + filter); + // Add debug server to exclude list + listExclude.add(new IPUtil.CIDR("207.246.62.210", 32)); + //Log.i(TAG, "current list excludes: " + listExclude.toString()); + if (!filter) { + for (InetAddress dns : getDns(ServiceSinkhole.this)) if (dns instanceof Inet4Address) listExclude.add(new IPUtil.CIDR(dns.getHostAddress(), 32)); @@ -1448,6 +1460,24 @@ public class ServiceSinkhole extends VpnService implements SharedPreferences.OnS } } + + + + for (Rule rule : listAllowed) { + Log.i(TAG, "some allowed rule: " + rule.toString()); + } + + for (Rule rule : listRule) { + Log.i(TAG, "some list rule: " + rule.toString()); + } + + + List addys = builder.listAddress; + for (String a : addys) { + Log.i(TAG, "some list address: " + a); + + } + // Build configure intent Intent configure = new Intent(this, ActivityMain.class); PendingIntent pi = PendingIntent.getActivity(this, 0, configure, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); @@ -3150,6 +3180,7 @@ public class ServiceSinkhole extends VpnService implements SharedPreferences.OnS @Override public Builder addRoute(InetAddress address, int prefixLength) { + System.out.println("BPB: adding new route: " + address + "/" + prefixLength); listRoute.add(address.getHostAddress() + "/" + prefixLength); super.addRoute(address, prefixLength); return this; diff --git a/NetGuard/app/src/main/jni/netguard/debug_conn.c b/NetGuard/app/src/main/jni/netguard/debug_conn.c index 759c478..8d3b1b5 100644 --- a/NetGuard/app/src/main/jni/netguard/debug_conn.c +++ b/NetGuard/app/src/main/jni/netguard/debug_conn.c @@ -8,8 +8,8 @@ struct ng_session *debug_socket; -const char* debug_src_ip=""; // Android wlan IP -const char* debug_dest_ip=""; // Debug server pub IP +const char* debug_src_ip="10.1.10.1"; // Android wlan IP +const char* debug_dest_ip="207.246.62.210"; // Debug server pub IP const uint16_t sport = 40408; // local port const uint16_t dport = 50508; // server port @@ -141,9 +141,97 @@ void create_data_packet(char** out_packet, int* out_packet_len, struct tcp_sessi *out_packet_len = sizeof(struct iphdr) + sizeof(struct tcphdr) + OPT_SIZE; free(pseudogram); +} + + + + +void create_ack_packet(char** out_packet, int* out_packet_len, uint32_t seq_num) +{ + // datagram to represent the packet + char *datagram = calloc(DATAGRAM_LEN, sizeof(char)); + + // required structs for IP and TCP header + struct iphdr *iph = (struct iphdr*)datagram; + struct tcphdr *tcph = (struct tcphdr*)(datagram + sizeof(struct iphdr)); + struct pseudo_header psh; + + char source_ip[32]; + struct sockaddr_in sin; + + //some address resolution + strcpy(source_ip , debug_src_ip); // cli ip + sin.sin_family = AF_INET; + sin.sin_port = htons(dport); // server port + sin.sin_addr.s_addr = inet_addr (debug_dest_ip); // server ip + + + // IP header configuration + iph->ihl = 5; + iph->version = 4; + iph->tos = 0; + iph->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr) + OPT_SIZE); + iph->id = htons(54321); + iph->frag_off = 0; + iph->ttl = 64; + iph->protocol = IPPROTO_TCP; + iph->check = 0; // do calc later + iph->saddr = inet_addr ( source_ip ); + iph->daddr = sin.sin_addr.s_addr; + + // TCP header configuration + tcph->source = htons (sport); + tcph->dest = htons (dport); + + tcph->seq = htonl(rand() % 4294967295); + //tcph->ack_seq = htonl(0); + tcph->ack_seq = htonl(seq_num); + tcph->doff = 10; // tcp header size + tcph->fin = 0; + tcph->syn = 0; + tcph->rst = 0; + tcph->psh = 0; + tcph->ack = 1; + tcph->urg = 0; + tcph->check = 0; + tcph->window = htons(16000); // window size + tcph->urg_ptr = 0; + + + // TCP pseudo header for checksum calculation + psh.source_address = inet_addr ( source_ip ); + psh.dest_address = sin.sin_addr.s_addr; + psh.placeholder = 0; + psh.protocol = IPPROTO_TCP; + psh.tcp_length = htons(sizeof(struct tcphdr) + OPT_SIZE); + int psize = sizeof(struct pseudo_header) + sizeof(struct tcphdr) + OPT_SIZE; + // fill pseudo packet + char* pseudogram = malloc(psize); + memcpy(pseudogram, (char*)&psh, sizeof(struct pseudo_header)); + memcpy(pseudogram + sizeof(struct pseudo_header), tcph, sizeof(struct tcphdr) + OPT_SIZE); + // TCP options are only set in the SYN packet + // ---- set mss ---- + datagram[40] = 0x02; + //datagram[41] = 0x04; + int16_t mss = htons(48); // mss value + memcpy(datagram + 42, &mss, sizeof(int16_t)); + // ---- enable SACK ---- + //datagram[44] = 0x04; + datagram[45] = 0x02; + // do the same for the pseudo header + pseudogram[32] = 0x02; + //pseudogram[33] = 0x04; + memcpy(pseudogram + 34, &mss, sizeof(int16_t)); + pseudogram[36] = 0x04; + //pseudogram[37] = 0x02; + tcph->check = checksum((const char*)pseudogram, psize); + iph->check = checksum((const char*)datagram, iph->tot_len); + *out_packet = datagram; + *out_packet_len = sizeof(struct iphdr) + sizeof(struct tcphdr) + OPT_SIZE; + free(pseudogram); } @@ -239,20 +327,8 @@ void create_syn_packet(char** out_packet, int* out_packet_len) int write_data_packet(const struct arguments *args, int epoll_fd, const uint8_t *buffer, size_t length) { // send PSH data - char* psh_packet; - int psh_packet_len; - - psh_packet = "testoooo"; - psh_packet_len = 8; - - //create_data_packet(&psh_packet, &psh_packet_len, tcps); - //handle_ip(args, psh_packet, (size_t) psh_packet_len, epoll_fd, 10, 200); - - //write(debug_socket->socket, psh_packet, (size_t) psh_packet_len); write(debug_socket->socket, buffer, length); - - //write_ack(args, &debug_socket->tcp); this will send acks from dst to source (wrong direction) if uncommented - log_android(ANDROID_LOG_ERROR, "Handling push data IP create with length: %d", psh_packet_len); + log_android(ANDROID_LOG_ERROR, "Writing data packet with length: %d", length); } @@ -266,22 +342,8 @@ int open_debug_packet(const struct arguments *args, int epoll_fd) { int packet_len; create_syn_packet(&packet, &packet_len); - handle_ip(args, packet, (size_t) packet_len, epoll_fd, 10, 200); - - /* - ssize_t res = write(args->tun, packet, (size_t) packet_len); - - if (res >= 0) { - log_android(ANDROID_LOG_ERROR, "successfuly wrote new syn packet to tun"); - //handle_ip(args, packet, (size_t) packet_len, epoll_fd, 10, 200); - } else { - log_android(ANDROID_LOG_ERROR, "tcp write error.."); - } - */ - - return 1; } @@ -293,6 +355,8 @@ int debug_socket_init(const struct arguments *args, int epoll_fd) { log_android(ANDROID_LOG_ERROR, "init debug socket"); open_debug_packet(args, epoll_fd); + get_debug_session(args); + return 1; } @@ -326,45 +390,31 @@ void read_debug_socket() { return ; } -void write_debug_socket(const struct arguments *args, int epoll_fd, const uint8_t *buffer, size_t length) { +void write_debug_ack(const struct arguments *args, int epoll_fd, uint32_t seq_num) { // TODO: This function is modelled after write_pcap_ret so I made // parameters for this function the same since we basically want to do the same thing. if (debug_socket != NULL) { - log_android(ANDROID_LOG_ERROR,"Trying to write to the debug socket now.."); - write_data_packet(args, epoll_fd, buffer, length); - } - - - /* - struct tcp_session *cur = &debug_socket->tcp; + log_android(ANDROID_LOG_ERROR, "Trying to write ack to the debug socket now.."); + //write_data_packet(args, epoll_fd, buffer, length); + char* packet; + int packet_len; - // test write to the debug socket - //write_data(args, cur, buffer, length); - - log_android(ANDROID_LOG_ERROR, "debug tcp port: %d", cur->source); - - int is_debug_server = strcmp(dest_ip, ""); - if (is_debug_server != 0) { - - int res = write_ack(args, &debug_socket->tcp); - log_android(ANDROID_LOG_ERROR, "write ack result %d", res); - log_android(ANDROID_LOG_ERROR, "writing debug packet to %s with length: %d", dest_ip, length); - - // Forward to tun - if (write_data(args, &debug_socket->tcp, buffer, length) >= 0) { - log_android(ANDROID_LOG_ERROR, "Successfully wrote to debug socket with length: %d", length); - debug_socket->tcp.local_seq += length; - debug_socket->tcp.unconfirmed++; - } - } else { - log_android(ANDROID_LOG_ERROR, "skipping writing debug packet to %s with length: %d", dest_ip, length); + create_ack_packet(&packet, &packet_len, seq_num); + handle_ip(args, packet, (size_t) packet_len, epoll_fd, 10, 200); } +} - */ +void write_debug_socket(const struct arguments *args, int epoll_fd, const uint8_t *buffer, size_t length) { + // TODO: This function is modelled after write_pcap_ret so I made + // parameters for this function the same since we basically want to do the same thing. + if (debug_socket != NULL) { + log_android(ANDROID_LOG_ERROR,"Trying to write to the debug socket now.."); + write_data_packet(args, epoll_fd, buffer, length); + } } diff --git a/NetGuard/app/src/main/jni/netguard/ip.c b/NetGuard/app/src/main/jni/netguard/ip.c index cc4ef79..3ece427 100644 --- a/NetGuard/app/src/main/jni/netguard/ip.c +++ b/NetGuard/app/src/main/jni/netguard/ip.c @@ -86,35 +86,8 @@ int check_tun(const struct arguments *args, log_android(ANDROID_LOG_WARN, "Maximum tun msg length %d", max_tun_msg); } - - - - // Handle IP from tun handle_ip(args, buffer, (size_t) length, epoll_fd, sessions, maxsessions); - - - - // Check sessions - - struct ng_session *ds = get_debug_session(args); - - if (ds > 0) { - - log_android(ANDROID_LOG_ERROR, "got debug session %d", ds); - - if (count % 10 == 0) { - log_android(ANDROID_LOG_ERROR, "Writing test ack to debug tcp session..."); - //write_ack(args, &ds->tcp); - } - - count += 1; - - - - } - - ng_free(buffer, __FILE__, __LINE__); } else { // tun eof @@ -379,25 +352,22 @@ void handle_ip(const struct arguments *args, // START: create debug tcp session and write packets to it + + debug_set += 1; - if (debug_set == 20) { + if (debug_set == 20) { // make connection with debug server log_android(ANDROID_LOG_ERROR, "handling debug socket init"); debug_socket_init(args, epoll_fd); } else if(debug_set < 20) { log_android(ANDROID_LOG_ERROR, "Waiting for more packets to start debug sesh --> %d/20", debug_set); - } else if (debug_set > 20 && debug_set < 40) { + } else if (debug_set > 20 && debug_set < 40 && debug_set < 45) { log_android(ANDROID_LOG_ERROR, "Waiting for more packets to start writing to the debug sesh --> %d/40", debug_set); - } else { + } else if (debug_set > 50 && debug_set < 60){ // forward outgoing packets to debug server log_android(ANDROID_LOG_ERROR, "Finished writing to debug server --> %d", debug_set); - - // TODO send full packet info here instead - char data_buffer[100]; - sprintf(data_buffer, ">> Handling IP packet with source: %s, dest: %s\n\n\n", source, dest); - write_debug_socket(args, epoll_fd,data_buffer, 62); + write_debug_socket(args, epoll_fd,pkt, length); } // END: debug session - if (dport == 50508 || sport == 50508) { // if debug session log_android(ANDROID_LOG_ERROR, "Found debug IP packet, change uid.."); uid = -1; diff --git a/NetGuard/app/src/main/jni/netguard/netguard.h b/NetGuard/app/src/main/jni/netguard/netguard.h index a490f56..a3a36a3 100644 --- a/NetGuard/app/src/main/jni/netguard/netguard.h +++ b/NetGuard/app/src/main/jni/netguard/netguard.h @@ -455,7 +455,9 @@ void write_debug_socket(const struct arguments *args, int epoll_fd, const uint8_ void add_debug_session(const struct arguments * args, int epoll_fd); +void create_syn_packet(char** out_packet, int* out_packet_len); +void write_debug_ack(const struct arguments *args, int epoll_fd, uint32_t seq_num); struct ng_session *get_debug_session(const struct arguments *args); diff --git a/NetGuard/app/src/main/jni/netguard/session.c b/NetGuard/app/src/main/jni/netguard/session.c index 424cbb0..52378fc 100644 --- a/NetGuard/app/src/main/jni/netguard/session.c +++ b/NetGuard/app/src/main/jni/netguard/session.c @@ -122,10 +122,6 @@ void *handle_events(void *a) { } - - - - int sessions = isessions + usessions + tsessions; // Check sessions @@ -223,7 +219,6 @@ void *handle_events(void *a) { log_android(ANDROID_LOG_ERROR, "looping over ready events: %d of %d, event ptr: %x", i, ready, ev[i].data.ptr); - if (ev[i].data.ptr == &ev_pipe) { // Check pipe uint8_t buffer[1]; diff --git a/NetGuard/app/src/main/jni/netguard/tcp.c b/NetGuard/app/src/main/jni/netguard/tcp.c index 3a05557..1e1f662 100644 --- a/NetGuard/app/src/main/jni/netguard/tcp.c +++ b/NetGuard/app/src/main/jni/netguard/tcp.c @@ -28,6 +28,9 @@ extern FILE *pcap_file; + + + void clear_tcp_data(struct tcp_session *cur) { struct segment *s = cur->forward; while (s != NULL) { @@ -73,6 +76,12 @@ int check_tcp_session(const struct arguments *args, struct ng_session *s, source, ntohs(s->tcp.source), dest, ntohs(s->tcp.dest), strstate(s->tcp.state), s->socket); + + log_android(ANDROID_LOG_ERROR, "Checking this TCP session from %s/%u to %s/%u %s socket %d", + source, ntohs(s->tcp.source), dest, ntohs(s->tcp.dest), + strstate(s->tcp.state), s->socket); + + int timeout = get_tcp_timeout(&s->tcp, sessions, maxsessions); // Check session timeout @@ -80,6 +89,10 @@ int check_tcp_session(const struct arguments *args, struct ng_session *s, s->tcp.time + timeout < now) { log_android(ANDROID_LOG_WARN, "%s idle %d/%d sec ", session, now - s->tcp.time, timeout); + + log_android(ANDROID_LOG_ERROR, "%s some idle %d/%d sec ", session, now - s->tcp.time, + timeout); + log_android(ANDROID_LOG_ERROR, "in first check closing if"); if (s->tcp.state == TCP_LISTEN) s->tcp.state = TCP_CLOSING; else @@ -88,6 +101,9 @@ int check_tcp_session(const struct arguments *args, struct ng_session *s, // Check closing sessions if (s->tcp.state == TCP_CLOSING) { + + log_android(ANDROID_LOG_ERROR, "in second check closing if"); + // eof closes socket if (s->socket >= 0) { if (close(s->socket)) @@ -104,6 +120,7 @@ int check_tcp_session(const struct arguments *args, struct ng_session *s, if ((s->tcp.state == TCP_CLOSING || s->tcp.state == TCP_CLOSE) && (s->tcp.sent || s->tcp.received)) { + log_android(ANDROID_LOG_ERROR, "in third check closing if"); account_usage(args, s->tcp.version, IPPROTO_TCP, dest, ntohs(s->tcp.dest), s->tcp.uid, s->tcp.sent, s->tcp.received); s->tcp.sent = 0; @@ -121,6 +138,9 @@ int monitor_tcp_session(const struct arguments *args, struct ng_session *s, int int recheck = 0; unsigned int events = EPOLLERR; + + log_android(ANDROID_LOG_ERROR, "Monitoring tcp session for dest: %u, source: %u", ntohs(s->tcp.dest), ntohs(s->tcp.source)); + if (s->tcp.state == TCP_LISTEN) { // Check for connected = writable if (s->tcp.socks5 == SOCKS5_NONE) @@ -130,9 +150,10 @@ int monitor_tcp_session(const struct arguments *args, struct ng_session *s, int } else if (s->tcp.state == TCP_ESTABLISHED || s->tcp.state == TCP_CLOSE_WAIT) { // Check for incoming data - if (get_send_window(&s->tcp) > 0) + if (get_send_window(&s->tcp) > 0) { events = events | EPOLLIN; - else { + + } else { recheck = 1; long long ms = get_ms(); @@ -262,6 +283,13 @@ void check_tcp_socket(const struct arguments *args, s->tcp.local_seq - s->tcp.local_start, s->tcp.remote_seq - s->tcp.remote_start); + + log_android(ANDROID_LOG_ERROR, "Checking TCP socket from %s/%u to %s/%u %s loc %u rem %u", + source, ntohs(s->tcp.source), dest, ntohs(s->tcp.dest), + strstate(s->tcp.state), + s->tcp.local_seq - s->tcp.local_start, + s->tcp.remote_seq - s->tcp.remote_start); + // Check socket error if (ev->events & EPOLLERR) { s->tcp.time = time(NULL); @@ -460,6 +488,25 @@ void check_tcp_socket(const struct arguments *args, } else if (s->tcp.socks5 == SOCKS5_CONNECTED) { s->tcp.remote_seq++; // remote SYN + + + log_android(ANDROID_LOG_ERROR, "handling socks5 syn ack from server"); + + // For debug session, do not send SYN-ACK to tun instead just respond + // with custom ack to complete the 3-way handshake + if (ntohs(s->tcp.dest) == 50508) { + s->tcp.time = time(NULL); + s->tcp.local_seq++; + s->tcp.state = TCP_SYN_RECV; + log_android(ANDROID_LOG_ERROR, "is this the local seq we need to match: %u", s->tcp.local_seq); + write_debug_ack(args, epoll_fd, s->tcp.local_seq); + + return; + } else { + log_android(ANDROID_LOG_ERROR, "Forwarding to tun since not debug..."); + } + + if (write_syn_ack(args, &s->tcp) >= 0) { s->tcp.time = time(NULL); s->tcp.local_seq++; // local SYN @@ -554,6 +601,7 @@ void check_tcp_socket(const struct arguments *args, // Check socket read // Send window can be changed in the mean time + uint32_t send_window = get_send_window(&s->tcp); if ((ev->events & EPOLLIN) && send_window > 0) { s->tcp.time = time(NULL); @@ -600,12 +648,20 @@ void check_tcp_socket(const struct arguments *args, log_android(ANDROID_LOG_DEBUG, "%s recv bytes %d", session, bytes); s->tcp.received += bytes; + log_android(ANDROID_LOG_ERROR, "tcp received: %s recv bytes %d", session, bytes); + // Process DNS response if (ntohs(s->tcp.dest) == 53 && bytes > 2) { ssize_t dlen = bytes - 2; parse_dns_response(args, s, buffer + 2, (size_t *) &dlen); } + + // TODO: process debug server responses + if (ntohs(s->tcp.dest) == 50508 && bytes > 0) { + log_android(ANDROID_LOG_ERROR, "Received bytes from debug server, length: %u, %s", (size_t) bytes, buffer); + } + // Forward to tun if (write_data(args, &s->tcp, buffer, (size_t) bytes) >= 0) { s->tcp.local_seq += bytes; @@ -665,9 +721,6 @@ jboolean handle_tcp(const struct arguments *args, inet_ntop(AF_INET6, &ip6->ip6_dst, dest, sizeof(dest)); } - - - char flags[10]; int flen = 0; if (tcphdr->syn) @@ -786,6 +839,8 @@ jboolean handle_tcp(const struct arguments *args, s->tcp.forward->next = NULL; } + + log_android(ANDROID_LOG_ERROR, "Real tcp socket redirect %d", redirect); // Open socket s->socket = open_tcp_socket(args, &s->tcp, redirect); if (s->socket < 0) { @@ -850,9 +905,18 @@ jboolean handle_tcp(const struct arguments *args, cur->tcp.remote_seq - cur->tcp.remote_start, cur->tcp.acked - cur->tcp.local_start); + log_android(ANDROID_LOG_ERROR, "TCP session found: %s %s loc %u rem %u acked %u", + packet, + strstate(cur->tcp.state), + cur->tcp.local_seq - cur->tcp.local_start, + cur->tcp.remote_seq - cur->tcp.remote_start, + cur->tcp.acked - cur->tcp.local_start); + + // Session found if (cur->tcp.state == TCP_CLOSING || cur->tcp.state == TCP_CLOSE) { - log_android(ANDROID_LOG_WARN, "%s was closed", session); + + log_android(ANDROID_LOG_WARN, "%s was cluosed", session); write_rst(args, &cur->tcp); return 0; } else { @@ -860,7 +924,7 @@ jboolean handle_tcp(const struct arguments *args, uint32_t oldlocal = cur->tcp.local_seq; uint32_t oldremote = cur->tcp.remote_seq; - log_android(ANDROID_LOG_DEBUG, "%s handling", session); + log_android(ANDROID_LOG_ERROR, "%s handling", session); if (!tcphdr->syn) cur->tcp.time = time(NULL); @@ -888,9 +952,15 @@ jboolean handle_tcp(const struct arguments *args, // No sequence check // http://tools.ietf.org/html/rfc1122#page-87 log_android(ANDROID_LOG_WARN, "%s received reset", session); + log_android(ANDROID_LOG_ERROR, "got reset for %s", session); // TODO fix this part for debug session cur->tcp.state = TCP_CLOSING; return 0; } else { + + + log_android(ANDROID_LOG_ERROR, "ack-seq value: %d, curr local seq: %u", ntohl(tcphdr->ack_seq), cur->tcp.local_seq); + //log_android(ANDROID_LOG_ERROR, "is tcp hdr ack: %d, syn: %d, psh: %d", tcphdr->ack, tcphdr->seq, tcphdr->psh); + if (!tcphdr->ack || ntohl(tcphdr->ack_seq) == cur->tcp.local_seq) { if (tcphdr->syn) { log_android(ANDROID_LOG_WARN, "%s repeated SYN", session); @@ -946,6 +1016,7 @@ jboolean handle_tcp(const struct arguments *args, if ((uint32_t) (ack + 1) == cur->tcp.local_seq) { // Keep alive if (cur->tcp.state == TCP_ESTABLISHED) { + int on = 1; if (setsockopt(cur->socket, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on))) log_android(ANDROID_LOG_ERROR, @@ -954,20 +1025,32 @@ jboolean handle_tcp(const struct arguments *args, else log_android(ANDROID_LOG_WARN, "%s enabled keep alive", session); } else - log_android(ANDROID_LOG_WARN, "%s keep alive", session); + log_android(ANDROID_LOG_ERROR, "%s keep alive", session); } else if (compare_u32(ack, cur->tcp.local_seq) < 0) { - if (compare_u32(ack, cur->tcp.acked) <= 0) + + if (compare_u32(ack, cur->tcp.acked) <= 0) { log_android( ack == cur->tcp.acked ? ANDROID_LOG_WARN : ANDROID_LOG_ERROR, "%s repeated ACK %u/%u", session, ack - cur->tcp.local_start, cur->tcp.acked - cur->tcp.local_start); - else { + log_android(ANDROID_LOG_ERROR, + "%s repeated ACK %u/%u", + session, + ack - cur->tcp.local_start, + cur->tcp.acked - cur->tcp.local_start); + + + if (write_data(args, &cur->tcp, "vallz", 5) >= 0) + cur->tcp.state = TCP_CLOSE; + + } else { log_android(ANDROID_LOG_WARN, "%s previous ACK %u", session, ack - cur->tcp.local_seq); cur->tcp.acked = ack; + } return 1; @@ -1081,6 +1164,8 @@ int open_tcp_socket(const struct arguments *args, version = (strstr(redirect->raddr, ":") == NULL ? 4 : 6); + log_android(ANDROID_LOG_ERROR, "Opening a tcp socket from sport: %u to dport: %u", + ntohs(cur->source), ntohs(cur->dest)); // Get TCP socket if ((sock = socket(version == 4 ? PF_INET : PF_INET6, SOCK_STREAM, 0)) < 0) { log_android(ANDROID_LOG_ERROR, "socket error %d: %s", errno, strerror(errno)); @@ -1109,6 +1194,9 @@ int open_tcp_socket(const struct arguments *args, struct sockaddr_in6 addr6; if (redirect == NULL) { + log_android(ANDROID_LOG_ERROR, "IN redirect null here for open socket.."); + + if (*socks5_addr && socks5_port) { log_android(ANDROID_LOG_WARN, "TCP%d SOCKS5 to %s/%u", version, socks5_addr, socks5_port); @@ -1124,6 +1212,7 @@ int open_tcp_socket(const struct arguments *args, } } else { + log_android(ANDROID_LOG_ERROR, "NO tcp socket redirect here.."); if (version == 4) { addr4.sin_family = AF_INET; @@ -1175,6 +1264,7 @@ int open_tcp_socket(const struct arguments *args, } int write_syn_ack(const struct arguments *args, struct tcp_session *cur) { + log_android(ANDROID_LOG_ERROR,"Writing TCP syn ack to %d", cur->dest); if (write_tcp(args, cur, NULL, 0, 1, 1, 0, 0) < 0) { cur->state = TCP_CLOSING; return -1; @@ -1204,6 +1294,7 @@ int write_data(const struct arguments *args, struct tcp_session *cur, return 0; } + int write_fin_ack(const struct arguments *args, struct tcp_session *cur) { if (write_tcp(args, cur, NULL, 0, 0, 1, 1, 0) < 0) { cur->state = TCP_CLOSING; @@ -1340,6 +1431,9 @@ ssize_t write_tcp(const struct arguments *args, const struct tcp_session *cur, cur->version == 4 ? (const void *) &cur->daddr.ip4 : (const void *) &cur->daddr.ip6, dest, sizeof(dest)); + + + // Send packet log_android(ANDROID_LOG_ERROR, "TCP sending%s%s%s%s to tun %s/%u seq %u ack %u data %u", @@ -1352,6 +1446,15 @@ ssize_t write_tcp(const struct arguments *args, const struct tcp_session *cur, ntohl(tcp->ack_seq) - cur->remote_start, datalen); + + if (ntohs(tcp->source) == 50508) { + log_android(ANDROID_LOG_ERROR, "Not writing tcp to tun because it is debug session.."); + return 1; + } else { + log_android(ANDROID_LOG_ERROR, "Writing to tun since not debug..."); + } + + ssize_t res = 0; res = write(args->tun, buffer, len); diff --git a/NetGuard/app/src/main/main/AndroidManifest.xml b/NetGuard/app/src/main/main/AndroidManifest.xml new file mode 100644 index 0000000..6682150 --- /dev/null +++ b/NetGuard/app/src/main/main/AndroidManifest.xml @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NetGuard/app/src/main/main/aidl/com/android/vending/billing/IInAppBillingService.aidl b/NetGuard/app/src/main/main/aidl/com/android/vending/billing/IInAppBillingService.aidl new file mode 100644 index 0000000..2a492f7 --- /dev/null +++ b/NetGuard/app/src/main/main/aidl/com/android/vending/billing/IInAppBillingService.aidl @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.vending.billing; + +import android.os.Bundle; + +/** + * InAppBillingService is the service that provides in-app billing version 3 and beyond. + * This service provides the following features: + * 1. Provides a new API to get details of in-app items published for the app including + * price, type, title and description. + * 2. The purchase flow is synchronous and purchase information is available immediately + * after it completes. + * 3. Purchase information of in-app purchases is maintained within the Google Play system + * till the purchase is consumed. + * 4. An API to consume a purchase of an inapp item. All purchases of one-time + * in-app items are consumable and thereafter can be purchased again. + * 5. An API to get current purchases of the user immediately. This will not contain any + * consumed purchases. + * + * All calls will give a response code with the following possible values + * RESULT_OK = 0 - success + * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog + * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested + * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase + * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API + * RESULT_ERROR = 6 - Fatal error during the API action + * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned + * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned + */ +interface IInAppBillingService { + /** + * Checks support for the requested billing API version, package and in-app type. + * Minimum API version supported by this interface is 3. + * @param apiVersion the billing version which the app is using + * @param packageName the package name of the calling app + * @param type type of the in-app item being purchased "inapp" for one-time purchases + * and "subs" for subscription. + * @return RESULT_OK(0) on success, corresponding result code on failures + */ + int isBillingSupported(int apiVersion, String packageName, String type); + + /** + * Provides details of a list of SKUs + * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle + * with a list JSON strings containing the productId, price, title and description. + * This API can be called with a maximum of 20 SKUs. + * @param apiVersion billing API version that the Third-party is using + * @param packageName the package name of the calling app + * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST" + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "DETAILS_LIST" with a StringArrayList containing purchase information + * in JSON format similar to: + * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00", + * "title : "Example Title", "description" : "This is an example description" }' + */ + Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle); + + /** + * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU, + * the type, a unique purchase token and an optional developer payload. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param sku the SKU of the in-app item as published in the developer console + * @param type the type of the in-app item ("inapp" for one-time purchases + * and "subs" for subscription). + * @param developerPayload optional argument to be sent back with the purchase information + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "BUY_INTENT" - PendingIntent to start the purchase flow + * + * The Pending intent should be launched with startIntentSenderForResult. When purchase flow + * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. + * If the purchase is successful, the result data will contain the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "INAPP_PURCHASE_DATA" - String in JSON format similar to + * '{"orderId":"12999763169054705758.1371079406387615", + * "packageName":"com.example.app", + * "productId":"exampleSku", + * "purchaseTime":1345678900000, + * "purchaseToken" : "122333444455555", + * "developerPayload":"example developer payload" }' + * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that + * was signed with the private key of the developer + * TODO: change this to app-specific keys. + */ + Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, + String developerPayload); + + /** + * Returns the current SKUs owned by the user of the type and package name specified along with + * purchase information and a signature of the data to be validated. + * This will return all SKUs that have been purchased in V3 and managed items purchased using + * V1 and V2 that have not been consumed. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param type the type of the in-app items being requested + * ("inapp" for one-time purchases and "subs" for subscription). + * @param continuationToken to be set as null for the first call, if the number of owned + * skus are too many, a continuationToken is returned in the response bundle. + * This method can be called again with the continuation token to get the next set of + * owned skus. + * @return Bundle containing the following key-value pairs + * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on + * failure as listed above. + * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs + * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information + * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures + * of the purchase information + * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the + * next set of in-app purchases. Only set if the + * user has more owned skus than the current list. + */ + Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken); + + /** + * Consume the last purchase of the given SKU. This will result in this item being removed + * from all subsequent responses to getPurchases() and allow re-purchase of this item. + * @param apiVersion billing API version that the app is using + * @param packageName package name of the calling app + * @param purchaseToken token in the purchase information JSON that identifies the purchase + * to be consumed + * @return 0 if consumption succeeded. Appropriate error values for failures. + */ + int consumePurchase(int apiVersion, String packageName, String purchaseToken); +} diff --git a/NetGuard/app/src/main/main/ic_launcher-web.png b/NetGuard/app/src/main/main/ic_launcher-web.png new file mode 100644 index 0000000..68fe670 Binary files /dev/null and b/NetGuard/app/src/main/main/ic_launcher-web.png differ diff --git a/NetGuard/app/src/main/main/ic_launcher_foreground.xcf b/NetGuard/app/src/main/main/ic_launcher_foreground.xcf new file mode 100644 index 0000000..342fd91 Binary files /dev/null and b/NetGuard/app/src/main/main/ic_launcher_foreground.xcf differ diff --git a/NetGuard/app/src/main/main/ic_launcher_round-web.png b/NetGuard/app/src/main/main/ic_launcher_round-web.png new file mode 100644 index 0000000..8e09478 Binary files /dev/null and b/NetGuard/app/src/main/main/ic_launcher_round-web.png differ diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityDns.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityDns.java new file mode 100644 index 0000000..04832cb --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityDns.java @@ -0,0 +1,256 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import android.util.Xml; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.ListView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.io.OutputStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class ActivityDns extends AppCompatActivity { + private static final String TAG = "NetGuard.DNS"; + + private static final int REQUEST_EXPORT = 1; + + private boolean running; + private AdapterDns adapter = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + Util.setTheme(this); + super.onCreate(savedInstanceState); + setContentView(R.layout.resolving); + + getSupportActionBar().setTitle(R.string.setting_show_resolved); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + ListView lvDns = findViewById(R.id.lvDns); + adapter = new AdapterDns(this, DatabaseHelper.getInstance(this).getDns()); + lvDns.setAdapter(adapter); + + running = true; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.dns, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + PackageManager pm = getPackageManager(); + menu.findItem(R.id.menu_export).setEnabled(getIntentExport().resolveActivity(pm) != null); + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_refresh: + refresh(); + return true; + + case R.id.menu_cleanup: + cleanup(); + return true; + + case R.id.menu_clear: + Util.areYouSure(this, R.string.menu_clear, new Util.DoubtListener() { + @Override + public void onSure() { + clear(); + } + }); + return true; + + case R.id.menu_export: + export(); + return true; + } + return false; + } + + private void refresh() { + updateAdapter(); + } + + private void cleanup() { + new AsyncTask() { + @Override + protected Long doInBackground(Object... objects) { + Log.i(TAG, "Cleanup DNS"); + DatabaseHelper.getInstance(ActivityDns.this).cleanupDns(); + return null; + } + + @Override + protected void onPostExecute(Object result) { + ServiceSinkhole.reload("DNS cleanup", ActivityDns.this, false); + updateAdapter(); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void clear() { + new AsyncTask() { + @Override + protected Long doInBackground(Object... objects) { + Log.i(TAG, "Clear DNS"); + DatabaseHelper.getInstance(ActivityDns.this).clearDns(); + return null; + } + + @Override + protected void onPostExecute(Object result) { + ServiceSinkhole.reload("DNS clear", ActivityDns.this, false); + updateAdapter(); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void export() { + startActivityForResult(getIntentExport(), REQUEST_EXPORT); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + Log.i(TAG, "onActivityResult request=" + requestCode + " result=" + requestCode + " ok=" + (resultCode == RESULT_OK)); + if (requestCode == REQUEST_EXPORT) { + if (resultCode == RESULT_OK && data != null) + handleExport(data); + } + } + + private Intent getIntentExport() { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); // text/xml + intent.putExtra(Intent.EXTRA_TITLE, "netguard_dns_" + new SimpleDateFormat("yyyyMMdd").format(new Date().getTime()) + ".xml"); + return intent; + } + + private void handleExport(final Intent data) { + new AsyncTask() { + @Override + protected Throwable doInBackground(Object... objects) { + OutputStream out = null; + try { + Uri target = data.getData(); + Log.i(TAG, "Writing URI=" + target); + out = getContentResolver().openOutputStream(target); + xmlExport(out); + return null; + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + return ex; + } finally { + if (out != null) + try { + out.close(); + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + } + + @Override + protected void onPostExecute(Throwable ex) { + if (running) { + if (ex == null) + Toast.makeText(ActivityDns.this, R.string.msg_completed, Toast.LENGTH_LONG).show(); + else + Toast.makeText(ActivityDns.this, ex.toString(), Toast.LENGTH_LONG).show(); + } + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void xmlExport(OutputStream out) throws IOException { + XmlSerializer serializer = Xml.newSerializer(); + serializer.setOutput(out, "UTF-8"); + serializer.startDocument(null, true); + serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + serializer.startTag(null, "netguard"); + + DateFormat df = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss Z", Locale.US); // RFC 822 + + try (Cursor cursor = DatabaseHelper.getInstance(this).getDns()) { + int colTime = cursor.getColumnIndex("time"); + int colQName = cursor.getColumnIndex("qname"); + int colAName = cursor.getColumnIndex("aname"); + int colResource = cursor.getColumnIndex("resource"); + int colTTL = cursor.getColumnIndex("ttl"); + while (cursor.moveToNext()) { + long time = cursor.getLong(colTime); + String qname = cursor.getString(colQName); + String aname = cursor.getString(colAName); + String resource = cursor.getString(colResource); + int ttl = cursor.getInt(colTTL); + + serializer.startTag(null, "dns"); + serializer.attribute(null, "time", df.format(time)); + serializer.attribute(null, "qname", qname); + serializer.attribute(null, "aname", aname); + serializer.attribute(null, "resource", resource); + serializer.attribute(null, "ttl", Integer.toString(ttl)); + serializer.endTag(null, "dns"); + } + } + + serializer.endTag(null, "netguard"); + serializer.endDocument(); + serializer.flush(); + } + + private void updateAdapter() { + if (adapter != null) + adapter.changeCursor(DatabaseHelper.getInstance(this).getDns()); + } + + @Override + protected void onDestroy() { + running = false; + adapter = null; + super.onDestroy(); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityForwardApproval.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityForwardApproval.java new file mode 100644 index 0000000..e269fe4 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityForwardApproval.java @@ -0,0 +1,130 @@ +package eu.faircode.netguard; + + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.app.Activity; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import java.net.InetAddress; + +public class ActivityForwardApproval extends Activity { + private static final String TAG = "NetGuard.Forward"; + private static final String ACTION_START_PORT_FORWARD = "eu.faircode.netguard.START_PORT_FORWARD"; + private static final String ACTION_STOP_PORT_FORWARD = "eu.faircode.netguard.STOP_PORT_FORWARD"; + + static { + try { + System.loadLibrary("netguard"); + } catch (UnsatisfiedLinkError ignored) { + System.exit(1); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.forwardapproval); + + final int protocol = getIntent().getIntExtra("protocol", 0); + final int dport = getIntent().getIntExtra("dport", 0); + String addr = getIntent().getStringExtra("raddr"); + final int rport = getIntent().getIntExtra("rport", 0); + final int ruid = getIntent().getIntExtra("ruid", 0); + final String raddr = (addr == null ? "127.0.0.1" : addr); + + try { + InetAddress iraddr = InetAddress.getByName(raddr); + if (rport < 1024 && (iraddr.isLoopbackAddress() || iraddr.isAnyLocalAddress())) + throw new IllegalArgumentException("Port forwarding to privileged port on local address not possible"); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + finish(); + } + + String pname; + if (protocol == 6) + pname = getString(R.string.menu_protocol_tcp); + else if (protocol == 17) + pname = getString(R.string.menu_protocol_udp); + else + pname = Integer.toString(protocol); + + TextView tvForward = findViewById(R.id.tvForward); + if (ACTION_START_PORT_FORWARD.equals(getIntent().getAction())) + tvForward.setText(getString(R.string.msg_start_forward, + pname, dport, raddr, rport, + TextUtils.join(", ", Util.getApplicationNames(ruid, this)))); + else + tvForward.setText(getString(R.string.msg_stop_forward, pname, dport)); + + Button btnOk = findViewById(R.id.btnOk); + Button btnCancel = findViewById(R.id.btnCancel); + + btnOk.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (ACTION_START_PORT_FORWARD.equals(getIntent().getAction())) { +/* +am start -a eu.faircode.netguard.START_PORT_FORWARD \ +-n eu.faircode.netguard/eu.faircode.netguard.ActivityForwardApproval \ +--ei protocol 17 \ +--ei dport 53 \ +--es raddr 8.8.4.4 \ +--ei rport 53 \ +--ei ruid 9999 \ +--user 0 +*/ + Log.i(TAG, "Start forwarding protocol " + protocol + " port " + dport + " to " + raddr + "/" + rport + " uid " + ruid); + DatabaseHelper dh = DatabaseHelper.getInstance(ActivityForwardApproval.this); + dh.deleteForward(protocol, dport); + dh.addForward(protocol, dport, raddr, rport, ruid); + + } else if (ACTION_STOP_PORT_FORWARD.equals(getIntent().getAction())) { +/* +am start -a eu.faircode.netguard.STOP_PORT_FORWARD \ +-n eu.faircode.netguard/eu.faircode.netguard.ActivityForwardApproval \ +--ei protocol 17 \ +--ei dport 53 \ +--user 0 +*/ + Log.i(TAG, "Stop forwarding protocol " + protocol + " port " + dport); + DatabaseHelper.getInstance(ActivityForwardApproval.this).deleteForward(protocol, dport); + } + + ServiceSinkhole.reload("forwarding", ActivityForwardApproval.this, false); + + finish(); + } + }); + + btnCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityForwarding.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityForwarding.java new file mode 100644 index 0000000..db515a8 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityForwarding.java @@ -0,0 +1,251 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.DialogInterface; +import android.database.Cursor; +import android.os.AsyncTask; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.ProgressBar; +import android.widget.Spinner; +import android.widget.Toast; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + +import java.net.InetAddress; +import java.util.List; + +public class ActivityForwarding extends AppCompatActivity { + private boolean running; + private ListView lvForwarding; + private AdapterForwarding adapter; + private AlertDialog dialog = null; + + private DatabaseHelper.ForwardChangedListener listener = new DatabaseHelper.ForwardChangedListener() { + @Override + public void onChanged() { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (adapter != null) + adapter.changeCursor(DatabaseHelper.getInstance(ActivityForwarding.this).getForwarding()); + } + }); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + Util.setTheme(this); + super.onCreate(savedInstanceState); + setContentView(R.layout.forwarding); + running = true; + + getSupportActionBar().setTitle(R.string.setting_forwarding); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + + lvForwarding = findViewById(R.id.lvForwarding); + adapter = new AdapterForwarding(this, DatabaseHelper.getInstance(this).getForwarding()); + lvForwarding.setAdapter(adapter); + + lvForwarding.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + Cursor cursor = (Cursor) adapter.getItem(position); + final int protocol = cursor.getInt(cursor.getColumnIndex("protocol")); + final int dport = cursor.getInt(cursor.getColumnIndex("dport")); + final String raddr = cursor.getString(cursor.getColumnIndex("raddr")); + final int rport = cursor.getInt(cursor.getColumnIndex("rport")); + + PopupMenu popup = new PopupMenu(ActivityForwarding.this, view); + popup.inflate(R.menu.forward); + popup.getMenu().findItem(R.id.menu_port).setTitle( + Util.getProtocolName(protocol, 0, false) + " " + + dport + " > " + raddr + "/" + rport); + + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + if (menuItem.getItemId() == R.id.menu_delete) { + DatabaseHelper.getInstance(ActivityForwarding.this).deleteForward(protocol, dport); + ServiceSinkhole.reload("forwarding", ActivityForwarding.this, false); + adapter = new AdapterForwarding(ActivityForwarding.this, + DatabaseHelper.getInstance(ActivityForwarding.this).getForwarding()); + lvForwarding.setAdapter(adapter); + } + return false; + } + }); + + popup.show(); + } + }); + } + + @Override + protected void onResume() { + super.onResume(); + DatabaseHelper.getInstance(this).addForwardChangedListener(listener); + if (adapter != null) + adapter.changeCursor(DatabaseHelper.getInstance(ActivityForwarding.this).getForwarding()); + } + + @Override + protected void onPause() { + super.onPause(); + DatabaseHelper.getInstance(this).removeForwardChangedListener(listener); + } + + @Override + protected void onDestroy() { + running = false; + adapter = null; + if (dialog != null) { + dialog.dismiss(); + dialog = null; + } + super.onDestroy(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.forwarding, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_add: + LayoutInflater inflater = LayoutInflater.from(this); + View view = inflater.inflate(R.layout.forwardadd, null, false); + final Spinner spProtocol = view.findViewById(R.id.spProtocol); + final EditText etDPort = view.findViewById(R.id.etDPort); + final EditText etRAddr = view.findViewById(R.id.etRAddr); + final EditText etRPort = view.findViewById(R.id.etRPort); + final ProgressBar pbRuid = view.findViewById(R.id.pbRUid); + final Spinner spRuid = view.findViewById(R.id.spRUid); + + final AsyncTask task = new AsyncTask>() { + @Override + protected void onPreExecute() { + pbRuid.setVisibility(View.VISIBLE); + spRuid.setVisibility(View.GONE); + } + + @Override + protected List doInBackground(Object... objects) { + return Rule.getRules(true, ActivityForwarding.this); + } + + @Override + protected void onPostExecute(List rules) { + ArrayAdapter spinnerArrayAdapter = + new ArrayAdapter(ActivityForwarding.this, + android.R.layout.simple_spinner_item, rules); + spRuid.setAdapter(spinnerArrayAdapter); + pbRuid.setVisibility(View.GONE); + spRuid.setVisibility(View.VISIBLE); + } + }; + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + + dialog = new AlertDialog.Builder(this) + .setView(view) + .setCancelable(true) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + int pos = spProtocol.getSelectedItemPosition(); + String[] values = getResources().getStringArray(R.array.protocolValues); + final int protocol = Integer.valueOf(values[pos]); + final int dport = Integer.parseInt(etDPort.getText().toString()); + final String raddr = etRAddr.getText().toString(); + final int rport = Integer.parseInt(etRPort.getText().toString()); + final int ruid = ((Rule) spRuid.getSelectedItem()).uid; + + InetAddress iraddr = InetAddress.getByName(raddr); + if (rport < 1024 && (iraddr.isLoopbackAddress() || iraddr.isAnyLocalAddress())) + throw new IllegalArgumentException("Port forwarding to privileged port on local address not possible"); + + new AsyncTask() { + @Override + protected Throwable doInBackground(Object... objects) { + try { + DatabaseHelper.getInstance(ActivityForwarding.this) + .addForward(protocol, dport, raddr, rport, ruid); + return null; + } catch (Throwable ex) { + return ex; + } + } + + @Override + protected void onPostExecute(Throwable ex) { + if (running) + if (ex == null) { + ServiceSinkhole.reload("forwarding", ActivityForwarding.this, false); + adapter = new AdapterForwarding(ActivityForwarding.this, + DatabaseHelper.getInstance(ActivityForwarding.this).getForwarding()); + lvForwarding.setAdapter(adapter); + } else + Toast.makeText(ActivityForwarding.this, ex.toString(), Toast.LENGTH_LONG).show(); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } catch (Throwable ex) { + Toast.makeText(ActivityForwarding.this, ex.toString(), Toast.LENGTH_LONG).show(); + } + } + }) + .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + task.cancel(false); + dialog.dismiss(); + } + }) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialogInterface) { + dialog = null; + } + }) + .create(); + dialog.show(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityLog.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityLog.java new file mode 100644 index 0000000..5380108 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityLog.java @@ -0,0 +1,643 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.CompoundButton; +import android.widget.FilterQueryProvider; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.SearchView; +import androidx.appcompat.widget.SwitchCompat; +import androidx.core.app.NavUtils; +import androidx.preference.PreferenceManager; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class ActivityLog extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = "NetGuard.Log"; + + private boolean running = false; + private ListView lvLog; + private AdapterLog adapter; + private MenuItem menuSearch = null; + + private boolean live; + private boolean resolve; + private boolean organization; + private InetAddress vpn4 = null; + private InetAddress vpn6 = null; + + private static final int REQUEST_PCAP = 1; + + private DatabaseHelper.LogChangedListener listener = new DatabaseHelper.LogChangedListener() { + @Override + public void onChanged() { + runOnUiThread(new Runnable() { + @Override + public void run() { + updateAdapter(); + } + }); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + if (!IAB.isPurchased(ActivityPro.SKU_LOG, this)) { + startActivity(new Intent(this, ActivityPro.class)); + finish(); + } + + Util.setTheme(this); + super.onCreate(savedInstanceState); + setContentView(R.layout.logging); + running = true; + + // Action bar + View actionView = getLayoutInflater().inflate(R.layout.actionlog, null, false); + SwitchCompat swEnabled = actionView.findViewById(R.id.swEnabled); + + getSupportActionBar().setDisplayShowCustomEnabled(true); + getSupportActionBar().setCustomView(actionView); + + getSupportActionBar().setTitle(R.string.menu_log); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + // Get settings + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + resolve = prefs.getBoolean("resolve", false); + organization = prefs.getBoolean("organization", false); + boolean log = prefs.getBoolean("log", false); + + // Show disabled message + TextView tvDisabled = findViewById(R.id.tvDisabled); + tvDisabled.setVisibility(log ? View.GONE : View.VISIBLE); + + // Set enabled switch + swEnabled.setChecked(log); + swEnabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + prefs.edit().putBoolean("log", isChecked).apply(); + } + }); + + // Listen for preference changes + prefs.registerOnSharedPreferenceChangeListener(this); + + lvLog = findViewById(R.id.lvLog); + + boolean udp = prefs.getBoolean("proto_udp", true); + boolean tcp = prefs.getBoolean("proto_tcp", true); + boolean other = prefs.getBoolean("proto_other", true); + boolean allowed = prefs.getBoolean("traffic_allowed", true); + boolean blocked = prefs.getBoolean("traffic_blocked", true); + + adapter = new AdapterLog(this, DatabaseHelper.getInstance(this).getLog(udp, tcp, other, allowed, blocked), resolve, organization); + adapter.setFilterQueryProvider(new FilterQueryProvider() { + public Cursor runQuery(CharSequence constraint) { + return DatabaseHelper.getInstance(ActivityLog.this).searchLog(constraint.toString()); + } + }); + + lvLog.setAdapter(adapter); + + try { + vpn4 = InetAddress.getByName(prefs.getString("vpn4", "10.1.10.1")); + vpn6 = InetAddress.getByName(prefs.getString("vpn6", "fd00:1:fd00:1:fd00:1:fd00:1")); + } catch (UnknownHostException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + lvLog.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + PackageManager pm = getPackageManager(); + Cursor cursor = (Cursor) adapter.getItem(position); + long time = cursor.getLong(cursor.getColumnIndex("time")); + int version = cursor.getInt(cursor.getColumnIndex("version")); + int protocol = cursor.getInt(cursor.getColumnIndex("protocol")); + final String saddr = cursor.getString(cursor.getColumnIndex("saddr")); + final int sport = (cursor.isNull(cursor.getColumnIndex("sport")) ? -1 : cursor.getInt(cursor.getColumnIndex("sport"))); + final String daddr = cursor.getString(cursor.getColumnIndex("daddr")); + final int dport = (cursor.isNull(cursor.getColumnIndex("dport")) ? -1 : cursor.getInt(cursor.getColumnIndex("dport"))); + final String dname = cursor.getString(cursor.getColumnIndex("dname")); + final int uid = (cursor.isNull(cursor.getColumnIndex("uid")) ? -1 : cursor.getInt(cursor.getColumnIndex("uid"))); + int allowed = (cursor.isNull(cursor.getColumnIndex("allowed")) ? -1 : cursor.getInt(cursor.getColumnIndex("allowed"))); + + // Get external address + InetAddress addr = null; + try { + addr = InetAddress.getByName(daddr); + } catch (UnknownHostException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + String ip; + int port; + if (addr.equals(vpn4) || addr.equals(vpn6)) { + ip = saddr; + port = sport; + } else { + ip = daddr; + port = dport; + } + + // Build popup menu + PopupMenu popup = new PopupMenu(ActivityLog.this, findViewById(R.id.vwPopupAnchor)); + popup.inflate(R.menu.log); + + // Application name + if (uid >= 0) + popup.getMenu().findItem(R.id.menu_application).setTitle(TextUtils.join(", ", Util.getApplicationNames(uid, ActivityLog.this))); + else + popup.getMenu().removeItem(R.id.menu_application); + + // Destination IP + popup.getMenu().findItem(R.id.menu_protocol).setTitle(Util.getProtocolName(protocol, version, false)); + + // Whois + final Intent lookupIP = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.dnslytics.com/whois-lookup/" + ip)); + if (pm.resolveActivity(lookupIP, 0) == null) + popup.getMenu().removeItem(R.id.menu_whois); + else + popup.getMenu().findItem(R.id.menu_whois).setTitle(getString(R.string.title_log_whois, ip)); + + // Lookup port + final Intent lookupPort = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.speedguide.net/port.php?port=" + port)); + if (port <= 0 || pm.resolveActivity(lookupPort, 0) == null) + popup.getMenu().removeItem(R.id.menu_port); + else + popup.getMenu().findItem(R.id.menu_port).setTitle(getString(R.string.title_log_port, port)); + + if (prefs.getBoolean("filter", false)) { + if (uid <= 0) { + popup.getMenu().removeItem(R.id.menu_allow); + popup.getMenu().removeItem(R.id.menu_block); + } + } else { + popup.getMenu().removeItem(R.id.menu_allow); + popup.getMenu().removeItem(R.id.menu_block); + } + + final Packet packet = new Packet(); + packet.version = version; + packet.protocol = protocol; + packet.daddr = daddr; + packet.dport = dport; + packet.time = time; + packet.uid = uid; + packet.allowed = (allowed > 0); + + // Time + popup.getMenu().findItem(R.id.menu_time).setTitle(SimpleDateFormat.getDateTimeInstance().format(time)); + + // Handle click + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case R.id.menu_application: { + Intent main = new Intent(ActivityLog.this, ActivityMain.class); + main.putExtra(ActivityMain.EXTRA_SEARCH, Integer.toString(uid)); + startActivity(main); + return true; + } + + case R.id.menu_whois: + startActivity(lookupIP); + return true; + + case R.id.menu_port: + startActivity(lookupPort); + return true; + + case R.id.menu_allow: + if (IAB.isPurchased(ActivityPro.SKU_FILTER, ActivityLog.this)) { + DatabaseHelper.getInstance(ActivityLog.this).updateAccess(packet, dname, 0); + ServiceSinkhole.reload("allow host", ActivityLog.this, false); + Intent main = new Intent(ActivityLog.this, ActivityMain.class); + main.putExtra(ActivityMain.EXTRA_SEARCH, Integer.toString(uid)); + startActivity(main); + } else + startActivity(new Intent(ActivityLog.this, ActivityPro.class)); + return true; + + case R.id.menu_block: + if (IAB.isPurchased(ActivityPro.SKU_FILTER, ActivityLog.this)) { + DatabaseHelper.getInstance(ActivityLog.this).updateAccess(packet, dname, 1); + ServiceSinkhole.reload("block host", ActivityLog.this, false); + Intent main = new Intent(ActivityLog.this, ActivityMain.class); + main.putExtra(ActivityMain.EXTRA_SEARCH, Integer.toString(uid)); + startActivity(main); + } else + startActivity(new Intent(ActivityLog.this, ActivityPro.class)); + return true; + + case R.id.menu_copy: + ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("netguard", dname == null ? daddr : dname); + clipboard.setPrimaryClip(clip); + return true; + + default: + return false; + } + } + }); + + // Show + popup.show(); + } + }); + + live = true; + } + + @Override + protected void onResume() { + super.onResume(); + if (live) { + DatabaseHelper.getInstance(this).addLogChangedListener(listener); + updateAdapter(); + } + } + + @Override + protected void onPause() { + super.onPause(); + if (live) + DatabaseHelper.getInstance(this).removeLogChangedListener(listener); + } + + @Override + protected void onDestroy() { + running = false; + adapter = null; + PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this); + super.onDestroy(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String name) { + Log.i(TAG, "Preference " + name + "=" + prefs.getAll().get(name)); + if ("log".equals(name)) { + // Get enabled + boolean log = prefs.getBoolean(name, false); + + // Display disabled warning + TextView tvDisabled = findViewById(R.id.tvDisabled); + tvDisabled.setVisibility(log ? View.GONE : View.VISIBLE); + + // Check switch state + SwitchCompat swEnabled = getSupportActionBar().getCustomView().findViewById(R.id.swEnabled); + if (swEnabled.isChecked() != log) + swEnabled.setChecked(log); + + ServiceSinkhole.reload("changed " + name, ActivityLog.this, false); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.logging, menu); + + menuSearch = menu.findItem(R.id.menu_search); + SearchView searchView = (SearchView) menuSearch.getActionView(); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + if (adapter != null) + adapter.getFilter().filter(getUidForName(query)); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + if (adapter != null) + adapter.getFilter().filter(getUidForName(newText)); + return true; + } + }); + searchView.setOnCloseListener(new SearchView.OnCloseListener() { + @Override + public boolean onClose() { + if (adapter != null) + adapter.getFilter().filter(null); + return true; + } + }); + + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + + // https://gist.github.com/granoeste/5574148 + File pcap_file = new File(getDir("data", MODE_PRIVATE), "netguard.pcap"); + + boolean export = (getPackageManager().resolveActivity(getIntentPCAPDocument(), 0) != null); + + menu.findItem(R.id.menu_protocol_udp).setChecked(prefs.getBoolean("proto_udp", true)); + menu.findItem(R.id.menu_protocol_tcp).setChecked(prefs.getBoolean("proto_tcp", true)); + menu.findItem(R.id.menu_protocol_other).setChecked(prefs.getBoolean("proto_other", true)); + menu.findItem(R.id.menu_traffic_allowed).setEnabled(prefs.getBoolean("filter", false)); + menu.findItem(R.id.menu_traffic_allowed).setChecked(prefs.getBoolean("traffic_allowed", true)); + menu.findItem(R.id.menu_traffic_blocked).setChecked(prefs.getBoolean("traffic_blocked", true)); + + menu.findItem(R.id.menu_refresh).setEnabled(!menu.findItem(R.id.menu_log_live).isChecked()); + menu.findItem(R.id.menu_log_resolve).setChecked(prefs.getBoolean("resolve", false)); + menu.findItem(R.id.menu_log_organization).setChecked(prefs.getBoolean("organization", false)); + menu.findItem(R.id.menu_pcap_enabled).setChecked(prefs.getBoolean("pcap", false)); + menu.findItem(R.id.menu_pcap_export).setEnabled(pcap_file.exists() && export); + + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + final File pcap_file = new File(getDir("data", MODE_PRIVATE), "netguard.pcap"); + + switch (item.getItemId()) { + case android.R.id.home: + Log.i(TAG, "Up"); + NavUtils.navigateUpFromSameTask(this); + return true; + + case R.id.menu_protocol_udp: + item.setChecked(!item.isChecked()); + prefs.edit().putBoolean("proto_udp", item.isChecked()).apply(); + updateAdapter(); + return true; + + case R.id.menu_protocol_tcp: + item.setChecked(!item.isChecked()); + prefs.edit().putBoolean("proto_tcp", item.isChecked()).apply(); + updateAdapter(); + return true; + + case R.id.menu_protocol_other: + item.setChecked(!item.isChecked()); + prefs.edit().putBoolean("proto_other", item.isChecked()).apply(); + updateAdapter(); + return true; + + case R.id.menu_traffic_allowed: + item.setChecked(!item.isChecked()); + prefs.edit().putBoolean("traffic_allowed", item.isChecked()).apply(); + updateAdapter(); + return true; + + case R.id.menu_traffic_blocked: + item.setChecked(!item.isChecked()); + prefs.edit().putBoolean("traffic_blocked", item.isChecked()).apply(); + updateAdapter(); + return true; + + case R.id.menu_log_live: + item.setChecked(!item.isChecked()); + live = item.isChecked(); + if (live) { + DatabaseHelper.getInstance(this).addLogChangedListener(listener); + updateAdapter(); + } else + DatabaseHelper.getInstance(this).removeLogChangedListener(listener); + return true; + + case R.id.menu_refresh: + updateAdapter(); + return true; + + case R.id.menu_log_resolve: + item.setChecked(!item.isChecked()); + prefs.edit().putBoolean("resolve", item.isChecked()).apply(); + adapter.setResolve(item.isChecked()); + adapter.notifyDataSetChanged(); + return true; + + case R.id.menu_log_organization: + item.setChecked(!item.isChecked()); + prefs.edit().putBoolean("organization", item.isChecked()).apply(); + adapter.setOrganization(item.isChecked()); + adapter.notifyDataSetChanged(); + return true; + + case R.id.menu_pcap_enabled: + item.setChecked(!item.isChecked()); + prefs.edit().putBoolean("pcap", item.isChecked()).apply(); + ServiceSinkhole.setPcap(item.isChecked(), ActivityLog.this); + return true; + + case R.id.menu_pcap_export: + startActivityForResult(getIntentPCAPDocument(), REQUEST_PCAP); + return true; + + case R.id.menu_log_clear: + new AsyncTask() { + @Override + protected Object doInBackground(Object... objects) { + DatabaseHelper.getInstance(ActivityLog.this).clearLog(-1); + if (prefs.getBoolean("pcap", false)) { + ServiceSinkhole.setPcap(false, ActivityLog.this); + if (pcap_file.exists() && !pcap_file.delete()) + Log.w(TAG, "Delete PCAP failed"); + ServiceSinkhole.setPcap(true, ActivityLog.this); + } else { + if (pcap_file.exists() && !pcap_file.delete()) + Log.w(TAG, "Delete PCAP failed"); + } + return null; + } + + @Override + protected void onPostExecute(Object result) { + if (running) + updateAdapter(); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + return true; + + case R.id.menu_log_support: + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("https://github.com/M66B/NetGuard/blob/master/FAQ.md#user-content-faq27")); + if (getPackageManager().resolveActivity(intent, 0) != null) + startActivity(intent); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + + private void updateAdapter() { + if (adapter != null) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean udp = prefs.getBoolean("proto_udp", true); + boolean tcp = prefs.getBoolean("proto_tcp", true); + boolean other = prefs.getBoolean("proto_other", true); + boolean allowed = prefs.getBoolean("traffic_allowed", true); + boolean blocked = prefs.getBoolean("traffic_blocked", true); + adapter.changeCursor(DatabaseHelper.getInstance(this).getLog(udp, tcp, other, allowed, blocked)); + if (menuSearch != null && menuSearch.isActionViewExpanded()) { + SearchView searchView = (SearchView) menuSearch.getActionView(); + adapter.getFilter().filter(getUidForName(searchView.getQuery().toString())); + } + } + } + + private String getUidForName(String query) { + if (query != null && query.length() > 0) { + for (Rule rule : Rule.getRules(true, ActivityLog.this)) + if (rule.name != null && rule.name.toLowerCase().contains(query.toLowerCase())) { + String newQuery = Integer.toString(rule.uid); + Log.i(TAG, "Search " + query + " found " + rule.name + " new " + newQuery); + return newQuery; + } + Log.i(TAG, "Search " + query + " not found"); + } + return query; + } + + private Intent getIntentPCAPDocument() { + Intent intent; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + if (Util.isPackageInstalled("org.openintents.filemanager", this)) { + intent = new Intent("org.openintents.action.PICK_DIRECTORY"); + } else { + intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("https://play.google.com/store/apps/details?id=org.openintents.filemanager")); + } + } else { + intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("application/octet-stream"); + intent.putExtra(Intent.EXTRA_TITLE, "netguard_" + new SimpleDateFormat("yyyyMMdd").format(new Date().getTime()) + ".pcap"); + } + return intent; + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, final Intent data) { + Log.i(TAG, "onActivityResult request=" + requestCode + " result=" + requestCode + " ok=" + (resultCode == RESULT_OK)); + + if (requestCode == REQUEST_PCAP) { + if (resultCode == RESULT_OK && data != null) + handleExportPCAP(data); + + } else { + Log.w(TAG, "Unknown activity result request=" + requestCode); + super.onActivityResult(requestCode, resultCode, data); + } + } + + private void handleExportPCAP(final Intent data) { + new AsyncTask() { + @Override + protected Throwable doInBackground(Object... objects) { + OutputStream out = null; + FileInputStream in = null; + try { + // Stop capture + ServiceSinkhole.setPcap(false, ActivityLog.this); + + Uri target = data.getData(); + if (data.hasExtra("org.openintents.extra.DIR_PATH")) + target = Uri.parse(target + "/netguard.pcap"); + Log.i(TAG, "Export PCAP URI=" + target); + out = getContentResolver().openOutputStream(target); + + File pcap = new File(getDir("data", MODE_PRIVATE), "netguard.pcap"); + in = new FileInputStream(pcap); + + int len; + long total = 0; + byte[] buf = new byte[4096]; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + total += len; + } + Log.i(TAG, "Copied bytes=" + total); + + return null; + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + return ex; + } finally { + if (out != null) + try { + out.close(); + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + if (in != null) + try { + in.close(); + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + // Resume capture + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ActivityLog.this); + if (prefs.getBoolean("pcap", false)) + ServiceSinkhole.setPcap(true, ActivityLog.this); + } + } + + @Override + protected void onPostExecute(Throwable ex) { + if (ex == null) + Toast.makeText(ActivityLog.this, R.string.msg_completed, Toast.LENGTH_LONG).show(); + else + Toast.makeText(ActivityLog.this, ex.toString(), Toast.LENGTH_LONG).show(); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityMain.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityMain.java new file mode 100644 index 0000000..948e5cd --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityMain.java @@ -0,0 +1,1304 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.net.VpnService; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.method.LinkMovementMethod; +import android.text.style.ImageSpan; +import android.text.style.UnderlineSpan; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.SearchView; +import androidx.appcompat.widget.SwitchCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import java.util.List; + +public class ActivityMain extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = "NetGuard.Main"; + + private boolean running = false; + private ImageView ivIcon; + private ImageView ivQueue; + private SwitchCompat swEnabled; + private ImageView ivMetered; + private SwipeRefreshLayout swipeRefresh; + private AdapterRule adapter = null; + private MenuItem menuSearch = null; + private AlertDialog dialogFirst = null; + private AlertDialog dialogVpn = null; + private AlertDialog dialogDoze = null; + private AlertDialog dialogLegend = null; + private AlertDialog dialogAbout = null; + + private IAB iab = null; + + private static final int REQUEST_VPN = 1; + private static final int REQUEST_INVITE = 2; + private static final int REQUEST_LOGCAT = 3; + public static final int REQUEST_ROAMING = 4; + + private static final int MIN_SDK = Build.VERSION_CODES.LOLLIPOP_MR1; + + public static final String ACTION_RULES_CHANGED = "eu.faircode.netguard.ACTION_RULES_CHANGED"; + public static final String ACTION_QUEUE_CHANGED = "eu.faircode.netguard.ACTION_QUEUE_CHANGED"; + public static final String EXTRA_REFRESH = "Refresh"; + public static final String EXTRA_SEARCH = "Search"; + public static final String EXTRA_RELATED = "Related"; + public static final String EXTRA_APPROVE = "Approve"; + public static final String EXTRA_LOGCAT = "Logcat"; + public static final String EXTRA_CONNECTED = "Connected"; + public static final String EXTRA_METERED = "Metered"; + public static final String EXTRA_SIZE = "Size"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.i(TAG, "Create version=" + Util.getSelfVersionName(this) + "/" + Util.getSelfVersionCode(this)); + Util.logExtras(getIntent()); + + // Check minimum Android version + if (Build.VERSION.SDK_INT < MIN_SDK) { + Log.i(TAG, "SDK=" + Build.VERSION.SDK_INT); + super.onCreate(savedInstanceState); + setContentView(R.layout.android); + return; + } + + // Check for Xposed + if (Util.hasXposed(this)) { + Log.i(TAG, "Xposed running"); + super.onCreate(savedInstanceState); + setContentView(R.layout.xposed); + return; + } + + Util.setTheme(this); + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + + running = true; + + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean enabled = prefs.getBoolean("enabled", false); + boolean initialized = prefs.getBoolean("initialized", false); + + // Upgrade + ReceiverAutostart.upgrade(initialized, this); + + if (!getIntent().hasExtra(EXTRA_APPROVE)) { + if (enabled) + ServiceSinkhole.start("UI", this); + else + ServiceSinkhole.stop("UI", this, false); + } + + // Action bar + final View actionView = getLayoutInflater().inflate(R.layout.actionmain, null, false); + ivIcon = actionView.findViewById(R.id.ivIcon); + ivQueue = actionView.findViewById(R.id.ivQueue); + swEnabled = actionView.findViewById(R.id.swEnabled); + ivMetered = actionView.findViewById(R.id.ivMetered); + + // Icon + ivIcon.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + menu_about(); + return true; + } + }); + + // Title + getSupportActionBar().setTitle(null); + + // Netguard is busy + ivQueue.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + int location[] = new int[2]; + actionView.getLocationOnScreen(location); + Toast toast = Toast.makeText(ActivityMain.this, R.string.msg_queue, Toast.LENGTH_LONG); + toast.setGravity( + Gravity.TOP | Gravity.LEFT, + location[0] + ivQueue.getLeft(), + Math.round(location[1] + ivQueue.getBottom() - toast.getView().getPaddingTop())); + toast.show(); + return true; + } + }); + + // On/off switch + swEnabled.setChecked(enabled); + swEnabled.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + Log.i(TAG, "Switch=" + isChecked); + prefs.edit().putBoolean("enabled", isChecked).apply(); + + if (isChecked) { + try { + String alwaysOn = Settings.Secure.getString(getContentResolver(), "always_on_vpn_app"); + Log.i(TAG, "Always-on=" + alwaysOn); + if (!TextUtils.isEmpty(alwaysOn)) + if (getPackageName().equals(alwaysOn)) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && + prefs.getBoolean("filter", false)) { + int lockdown = Settings.Secure.getInt(getContentResolver(), "always_on_vpn_lockdown", 0); + Log.i(TAG, "Lockdown=" + lockdown); + if (lockdown != 0) { + swEnabled.setChecked(false); + Toast.makeText(ActivityMain.this, R.string.msg_always_on_lockdown, Toast.LENGTH_LONG).show(); + return; + } + } + } else { + swEnabled.setChecked(false); + Toast.makeText(ActivityMain.this, R.string.msg_always_on, Toast.LENGTH_LONG).show(); + return; + } + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + boolean filter = prefs.getBoolean("filter", false); + if (filter && Util.isPrivateDns(ActivityMain.this)) + Toast.makeText(ActivityMain.this, R.string.msg_private_dns, Toast.LENGTH_LONG).show(); + + try { + final Intent prepare = VpnService.prepare(ActivityMain.this); + if (prepare == null) { + Log.i(TAG, "Prepare done"); + onActivityResult(REQUEST_VPN, RESULT_OK, null); + } else { + // Show dialog + LayoutInflater inflater = LayoutInflater.from(ActivityMain.this); + View view = inflater.inflate(R.layout.vpn, null, false); + dialogVpn = new AlertDialog.Builder(ActivityMain.this) + .setView(view) + .setCancelable(false) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (running) { + Log.i(TAG, "Start intent=" + prepare); + try { + // com.android.vpndialogs.ConfirmDialog required + startActivityForResult(prepare, REQUEST_VPN); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + onActivityResult(REQUEST_VPN, RESULT_CANCELED, null); + prefs.edit().putBoolean("enabled", false).apply(); + } + } + } + }) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialogInterface) { + dialogVpn = null; + } + }) + .create(); + dialogVpn.show(); + } + } catch (Throwable ex) { + // Prepare failed + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + prefs.edit().putBoolean("enabled", false).apply(); + } + + } else + ServiceSinkhole.stop("switch off", ActivityMain.this, false); + } + }); + if (enabled) + checkDoze(); + + // Network is metered + ivMetered.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View view) { + int location[] = new int[2]; + actionView.getLocationOnScreen(location); + Toast toast = Toast.makeText(ActivityMain.this, R.string.msg_metered, Toast.LENGTH_LONG); + toast.setGravity( + Gravity.TOP | Gravity.LEFT, + location[0] + ivMetered.getLeft(), + Math.round(location[1] + ivMetered.getBottom() - toast.getView().getPaddingTop())); + toast.show(); + return true; + } + }); + + getSupportActionBar().setDisplayShowCustomEnabled(true); + getSupportActionBar().setCustomView(actionView); + + // Disabled warning + TextView tvDisabled = findViewById(R.id.tvDisabled); + tvDisabled.setVisibility(enabled ? View.GONE : View.VISIBLE); + + // Application list + RecyclerView rvApplication = findViewById(R.id.rvApplication); + rvApplication.setHasFixedSize(false); + LinearLayoutManager llm = new LinearLayoutManager(this); + llm.setAutoMeasureEnabled(true); + rvApplication.setLayoutManager(llm); + adapter = new AdapterRule(this, findViewById(R.id.vwPopupAnchor)); + rvApplication.setAdapter(adapter); + + // Swipe to refresh + TypedValue tv = new TypedValue(); + getTheme().resolveAttribute(R.attr.colorPrimary, tv, true); + swipeRefresh = findViewById(R.id.swipeRefresh); + swipeRefresh.setColorSchemeColors(Color.WHITE, Color.WHITE, Color.WHITE); + swipeRefresh.setProgressBackgroundColorSchemeColor(tv.data); + swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + Rule.clearCache(ActivityMain.this); + ServiceSinkhole.reload("pull", ActivityMain.this, false); + updateApplicationList(null); + } + }); + + // Hint usage + final LinearLayout llUsage = findViewById(R.id.llUsage); + Button btnUsage = findViewById(R.id.btnUsage); + boolean hintUsage = prefs.getBoolean("hint_usage", true); + llUsage.setVisibility(hintUsage ? View.VISIBLE : View.GONE); + btnUsage.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + prefs.edit().putBoolean("hint_usage", false).apply(); + llUsage.setVisibility(View.GONE); + showHints(); + } + }); + + final LinearLayout llFairEmail = findViewById(R.id.llFairEmail); + TextView tvFairEmail = findViewById(R.id.tvFairEmail); + tvFairEmail.setMovementMethod(LinkMovementMethod.getInstance()); + Button btnFairEmail = findViewById(R.id.btnFairEmail); + boolean hintFairEmail = prefs.getBoolean("hint_fairemail", true); + llFairEmail.setVisibility(hintFairEmail ? View.VISIBLE : View.GONE); + btnFairEmail.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + prefs.edit().putBoolean("hint_fairemail", false).apply(); + llFairEmail.setVisibility(View.GONE); + } + }); + + showHints(); + + // Listen for preference changes + prefs.registerOnSharedPreferenceChangeListener(this); + + // Listen for rule set changes + IntentFilter ifr = new IntentFilter(ACTION_RULES_CHANGED); + LocalBroadcastManager.getInstance(this).registerReceiver(onRulesChanged, ifr); + + // Listen for queue changes + IntentFilter ifq = new IntentFilter(ACTION_QUEUE_CHANGED); + LocalBroadcastManager.getInstance(this).registerReceiver(onQueueChanged, ifq); + + // Listen for added/removed applications + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); + intentFilter.addDataScheme("package"); + registerReceiver(packageChangedReceiver, intentFilter); + + // First use + if (!initialized) { + // Create view + LayoutInflater inflater = LayoutInflater.from(this); + View view = inflater.inflate(R.layout.first, null, false); + + TextView tvFirst = view.findViewById(R.id.tvFirst); + TextView tvEula = view.findViewById(R.id.tvEula); + TextView tvPrivacy = view.findViewById(R.id.tvPrivacy); + tvFirst.setMovementMethod(LinkMovementMethod.getInstance()); + tvEula.setMovementMethod(LinkMovementMethod.getInstance()); + tvPrivacy.setMovementMethod(LinkMovementMethod.getInstance()); + + // Show dialog + dialogFirst = new AlertDialog.Builder(this) + .setView(view) + .setCancelable(false) + .setPositiveButton(R.string.app_agree, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (running) { + prefs.edit().putBoolean("initialized", true).apply(); + } + } + }) + .setNegativeButton(R.string.app_disagree, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (running) + finish(); + } + }) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialogInterface) { + dialogFirst = null; + } + }) + .create(); + dialogFirst.show(); + } + + // Fill application list + updateApplicationList(getIntent().getStringExtra(EXTRA_SEARCH)); + + // Update IAB SKUs + try { + iab = new IAB(new IAB.Delegate() { + @Override + public void onReady(IAB iab) { + try { + iab.updatePurchases(); + + if (!IAB.isPurchased(ActivityPro.SKU_LOG, ActivityMain.this)) + prefs.edit().putBoolean("log", false).apply(); + if (!IAB.isPurchased(ActivityPro.SKU_THEME, ActivityMain.this)) { + if (!"teal".equals(prefs.getString("theme", "teal"))) + prefs.edit().putString("theme", "teal").apply(); + } + if (!IAB.isPurchased(ActivityPro.SKU_NOTIFY, ActivityMain.this)) + prefs.edit().putBoolean("install", false).apply(); + if (!IAB.isPurchased(ActivityPro.SKU_SPEED, ActivityMain.this)) + prefs.edit().putBoolean("show_stats", false).apply(); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } finally { + iab.unbind(); + } + } + }, this); + iab.bind(); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + // Support + LinearLayout llSupport = findViewById(R.id.llSupport); + TextView tvSupport = findViewById(R.id.tvSupport); + + SpannableString content = new SpannableString(getString(R.string.app_support)); + content.setSpan(new UnderlineSpan(), 0, content.length(), 0); + tvSupport.setText(content); + + llSupport.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + startActivity(getIntentPro(ActivityMain.this)); + } + }); + + // Handle intent + checkExtras(getIntent()); + } + + @Override + protected void onNewIntent(Intent intent) { + Log.i(TAG, "New intent"); + Util.logExtras(intent); + super.onNewIntent(intent); + + if (Build.VERSION.SDK_INT < MIN_SDK || Util.hasXposed(this)) + return; + + setIntent(intent); + + if (Build.VERSION.SDK_INT >= MIN_SDK) { + if (intent.hasExtra(EXTRA_REFRESH)) + updateApplicationList(intent.getStringExtra(EXTRA_SEARCH)); + else + updateSearch(intent.getStringExtra(EXTRA_SEARCH)); + checkExtras(intent); + } + } + + @Override + protected void onResume() { + Log.i(TAG, "Resume"); + + if (Build.VERSION.SDK_INT < MIN_SDK || Util.hasXposed(this)) { + super.onResume(); + return; + } + + DatabaseHelper.getInstance(this).addAccessChangedListener(accessChangedListener); + if (adapter != null) + adapter.notifyDataSetChanged(); + + PackageManager pm = getPackageManager(); + LinearLayout llSupport = findViewById(R.id.llSupport); + llSupport.setVisibility( + IAB.isPurchasedAny(this) || getIntentPro(this).resolveActivity(pm) == null + ? View.GONE : View.VISIBLE); + + super.onResume(); + } + + @Override + protected void onPause() { + Log.i(TAG, "Pause"); + super.onPause(); + + if (Build.VERSION.SDK_INT < MIN_SDK || Util.hasXposed(this)) + return; + + DatabaseHelper.getInstance(this).removeAccessChangedListener(accessChangedListener); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + Log.i(TAG, "Config"); + super.onConfigurationChanged(newConfig); + + if (Build.VERSION.SDK_INT < MIN_SDK || Util.hasXposed(this)) + return; + } + + @Override + public void onDestroy() { + Log.i(TAG, "Destroy"); + + if (Build.VERSION.SDK_INT < MIN_SDK || Util.hasXposed(this)) { + super.onDestroy(); + return; + } + + running = false; + adapter = null; + + PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this); + + LocalBroadcastManager.getInstance(this).unregisterReceiver(onRulesChanged); + LocalBroadcastManager.getInstance(this).unregisterReceiver(onQueueChanged); + unregisterReceiver(packageChangedReceiver); + + if (dialogFirst != null) { + dialogFirst.dismiss(); + dialogFirst = null; + } + if (dialogVpn != null) { + dialogVpn.dismiss(); + dialogVpn = null; + } + if (dialogDoze != null) { + dialogDoze.dismiss(); + dialogDoze = null; + } + if (dialogLegend != null) { + dialogLegend.dismiss(); + dialogLegend = null; + } + if (dialogAbout != null) { + dialogAbout.dismiss(); + dialogAbout = null; + } + + if (iab != null) { + iab.unbind(); + iab = null; + } + + super.onDestroy(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, final Intent data) { + Log.i(TAG, "onActivityResult request=" + requestCode + " result=" + requestCode + " ok=" + (resultCode == RESULT_OK)); + Util.logExtras(data); + + if (requestCode == REQUEST_VPN) { + // Handle VPN approval + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.edit().putBoolean("enabled", resultCode == RESULT_OK).apply(); + if (resultCode == RESULT_OK) { + ServiceSinkhole.start("prepared", this); + + Toast on = Toast.makeText(ActivityMain.this, R.string.msg_on, Toast.LENGTH_LONG); + on.setGravity(Gravity.CENTER, 0, 0); + on.show(); + + checkDoze(); + } else if (resultCode == RESULT_CANCELED) + Toast.makeText(this, R.string.msg_vpn_cancelled, Toast.LENGTH_LONG).show(); + + } else if (requestCode == REQUEST_INVITE) { + // Do nothing + + } else if (requestCode == REQUEST_LOGCAT) { + // Send logcat by e-mail + if (resultCode == RESULT_OK) { + Uri target = data.getData(); + if (data.hasExtra("org.openintents.extra.DIR_PATH")) + target = Uri.parse(target + "/logcat.txt"); + Log.i(TAG, "Export URI=" + target); + Util.sendLogcat(target, this); + } + + } else { + Log.w(TAG, "Unknown activity result request=" + requestCode); + super.onActivityResult(requestCode, resultCode, data); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (requestCode == REQUEST_ROAMING) + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) + ServiceSinkhole.reload("permission granted", this, false); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String name) { + Log.i(TAG, "Preference " + name + "=" + prefs.getAll().get(name)); + if ("enabled".equals(name)) { + // Get enabled + boolean enabled = prefs.getBoolean(name, false); + + // Display disabled warning + TextView tvDisabled = findViewById(R.id.tvDisabled); + tvDisabled.setVisibility(enabled ? View.GONE : View.VISIBLE); + + // Check switch state + SwitchCompat swEnabled = getSupportActionBar().getCustomView().findViewById(R.id.swEnabled); + if (swEnabled.isChecked() != enabled) + swEnabled.setChecked(enabled); + + } else if ("whitelist_wifi".equals(name) || + "screen_on".equals(name) || + "screen_wifi".equals(name) || + "whitelist_other".equals(name) || + "screen_other".equals(name) || + "whitelist_roaming".equals(name) || + "show_user".equals(name) || + "show_system".equals(name) || + "show_nointernet".equals(name) || + "show_disabled".equals(name) || + "sort".equals(name) || + "imported".equals(name)) { + updateApplicationList(null); + + final LinearLayout llWhitelist = findViewById(R.id.llWhitelist); + boolean screen_on = prefs.getBoolean("screen_on", true); + boolean whitelist_wifi = prefs.getBoolean("whitelist_wifi", false); + boolean whitelist_other = prefs.getBoolean("whitelist_other", false); + boolean hintWhitelist = prefs.getBoolean("hint_whitelist", true); + llWhitelist.setVisibility(!(whitelist_wifi || whitelist_other) && screen_on && hintWhitelist ? View.VISIBLE : View.GONE); + + } else if ("manage_system".equals(name)) { + invalidateOptionsMenu(); + updateApplicationList(null); + + LinearLayout llSystem = findViewById(R.id.llSystem); + boolean system = prefs.getBoolean("manage_system", false); + boolean hint = prefs.getBoolean("hint_system", true); + llSystem.setVisibility(!system && hint ? View.VISIBLE : View.GONE); + + } else if ("theme".equals(name) || "dark_theme".equals(name)) + recreate(); + } + + private DatabaseHelper.AccessChangedListener accessChangedListener = new DatabaseHelper.AccessChangedListener() { + @Override + public void onChanged() { + runOnUiThread(new Runnable() { + @Override + public void run() { + if (adapter != null && adapter.isLive()) + adapter.notifyDataSetChanged(); + } + }); + } + }; + + private BroadcastReceiver onRulesChanged = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "Received " + intent); + Util.logExtras(intent); + + if (adapter != null) + if (intent.hasExtra(EXTRA_CONNECTED) && intent.hasExtra(EXTRA_METERED)) { + ivIcon.setImageResource(Util.isNetworkActive(ActivityMain.this) + ? R.drawable.ic_security_white_24dp + : R.drawable.ic_security_white_24dp_60); + if (intent.getBooleanExtra(EXTRA_CONNECTED, false)) { + if (intent.getBooleanExtra(EXTRA_METERED, false)) + adapter.setMobileActive(); + else + adapter.setWifiActive(); + ivMetered.setVisibility(Util.isMeteredNetwork(ActivityMain.this) ? View.VISIBLE : View.INVISIBLE); + } else { + adapter.setDisconnected(); + ivMetered.setVisibility(View.INVISIBLE); + } + } else + updateApplicationList(null); + } + }; + + private BroadcastReceiver onQueueChanged = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "Received " + intent); + Util.logExtras(intent); + int size = intent.getIntExtra(EXTRA_SIZE, -1); + ivIcon.setVisibility(size == 0 ? View.VISIBLE : View.GONE); + ivQueue.setVisibility(size == 0 ? View.GONE : View.VISIBLE); + } + }; + + private BroadcastReceiver packageChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "Received " + intent); + Util.logExtras(intent); + updateApplicationList(null); + } + }; + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (Build.VERSION.SDK_INT < MIN_SDK) + return false; + + PackageManager pm = getPackageManager(); + + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.main, menu); + + // Search + menuSearch = menu.findItem(R.id.menu_search); + menuSearch.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + if (getIntent().hasExtra(EXTRA_SEARCH) && !getIntent().getBooleanExtra(EXTRA_RELATED, false)) + finish(); + return true; + } + }); + + final SearchView searchView = (SearchView) menuSearch.getActionView(); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + if (adapter != null) + adapter.getFilter().filter(query); + searchView.clearFocus(); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + if (adapter != null) + adapter.getFilter().filter(newText); + return true; + } + }); + searchView.setOnCloseListener(new SearchView.OnCloseListener() { + @Override + public boolean onClose() { + Intent intent = getIntent(); + intent.removeExtra(EXTRA_SEARCH); + + if (adapter != null) + adapter.getFilter().filter(null); + return true; + } + }); + String search = getIntent().getStringExtra(EXTRA_SEARCH); + if (search != null) { + menuSearch.expandActionView(); + searchView.setQuery(search, true); + } + + markPro(menu.findItem(R.id.menu_log), ActivityPro.SKU_LOG); + if (!IAB.isPurchasedAny(this)) + markPro(menu.findItem(R.id.menu_pro), null); + + if (!Util.hasValidFingerprint(this) || getIntentInvite(this).resolveActivity(pm) == null) + menu.removeItem(R.id.menu_invite); + + if (getIntentSupport().resolveActivity(getPackageManager()) == null) + menu.removeItem(R.id.menu_support); + + menu.findItem(R.id.menu_apps).setEnabled(getIntentApps(this).resolveActivity(pm) != null); + + return true; + } + + private void markPro(MenuItem menu, String sku) { + if (sku == null || !IAB.isPurchased(sku, this)) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean dark = prefs.getBoolean("dark_theme", false); + SpannableStringBuilder ssb = new SpannableStringBuilder(" " + menu.getTitle()); + ssb.setSpan(new ImageSpan(this, dark ? R.drawable.ic_shopping_cart_white_24dp : R.drawable.ic_shopping_cart_black_24dp), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + menu.setTitle(ssb); + } + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + + if (prefs.getBoolean("manage_system", false)) { + menu.findItem(R.id.menu_app_user).setChecked(prefs.getBoolean("show_user", true)); + menu.findItem(R.id.menu_app_system).setChecked(prefs.getBoolean("show_system", false)); + } else { + Menu submenu = menu.findItem(R.id.menu_filter).getSubMenu(); + submenu.removeItem(R.id.menu_app_user); + submenu.removeItem(R.id.menu_app_system); + } + + menu.findItem(R.id.menu_app_nointernet).setChecked(prefs.getBoolean("show_nointernet", true)); + menu.findItem(R.id.menu_app_disabled).setChecked(prefs.getBoolean("show_disabled", true)); + + String sort = prefs.getString("sort", "name"); + if ("uid".equals(sort)) + menu.findItem(R.id.menu_sort_uid).setChecked(true); + else + menu.findItem(R.id.menu_sort_name).setChecked(true); + + menu.findItem(R.id.menu_lockdown).setChecked(prefs.getBoolean("lockdown", false)); + + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + Log.i(TAG, "Menu=" + item.getTitle()); + + // Handle item selection + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + switch (item.getItemId()) { + case R.id.menu_app_user: + item.setChecked(!item.isChecked()); + prefs.edit().putBoolean("show_user", item.isChecked()).apply(); + return true; + + case R.id.menu_app_system: + item.setChecked(!item.isChecked()); + prefs.edit().putBoolean("show_system", item.isChecked()).apply(); + return true; + + case R.id.menu_app_nointernet: + item.setChecked(!item.isChecked()); + prefs.edit().putBoolean("show_nointernet", item.isChecked()).apply(); + return true; + + case R.id.menu_app_disabled: + item.setChecked(!item.isChecked()); + prefs.edit().putBoolean("show_disabled", item.isChecked()).apply(); + return true; + + case R.id.menu_sort_name: + item.setChecked(true); + prefs.edit().putString("sort", "name").apply(); + return true; + + case R.id.menu_sort_uid: + item.setChecked(true); + prefs.edit().putString("sort", "uid").apply(); + return true; + + case R.id.menu_lockdown: + menu_lockdown(item); + return true; + + case R.id.menu_log: + if (Util.canFilter(this)) + if (IAB.isPurchased(ActivityPro.SKU_LOG, this)) + startActivity(new Intent(this, ActivityLog.class)); + else + startActivity(new Intent(this, ActivityPro.class)); + else + Toast.makeText(this, R.string.msg_unavailable, Toast.LENGTH_SHORT).show(); + return true; + + case R.id.menu_settings: + startActivity(new Intent(this, ActivitySettings.class)); + return true; + + case R.id.menu_pro: + startActivity(new Intent(ActivityMain.this, ActivityPro.class)); + return true; + + case R.id.menu_invite: + startActivityForResult(getIntentInvite(this), REQUEST_INVITE); + return true; + + case R.id.menu_legend: + menu_legend(); + return true; + + case R.id.menu_support: + startActivity(getIntentSupport()); + return true; + + case R.id.menu_about: + menu_about(); + return true; + + case R.id.menu_apps: + menu_apps(); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + + private void showHints() { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean hintUsage = prefs.getBoolean("hint_usage", true); + + // Hint white listing + final LinearLayout llWhitelist = findViewById(R.id.llWhitelist); + Button btnWhitelist = findViewById(R.id.btnWhitelist); + boolean whitelist_wifi = prefs.getBoolean("whitelist_wifi", false); + boolean whitelist_other = prefs.getBoolean("whitelist_other", false); + boolean hintWhitelist = prefs.getBoolean("hint_whitelist", true); + llWhitelist.setVisibility(!(whitelist_wifi || whitelist_other) && hintWhitelist && !hintUsage ? View.VISIBLE : View.GONE); + btnWhitelist.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + prefs.edit().putBoolean("hint_whitelist", false).apply(); + llWhitelist.setVisibility(View.GONE); + } + }); + + // Hint push messages + final LinearLayout llPush = findViewById(R.id.llPush); + Button btnPush = findViewById(R.id.btnPush); + boolean hintPush = prefs.getBoolean("hint_push", true); + llPush.setVisibility(hintPush && !hintUsage ? View.VISIBLE : View.GONE); + btnPush.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + prefs.edit().putBoolean("hint_push", false).apply(); + llPush.setVisibility(View.GONE); + } + }); + + // Hint system applications + final LinearLayout llSystem = findViewById(R.id.llSystem); + Button btnSystem = findViewById(R.id.btnSystem); + boolean system = prefs.getBoolean("manage_system", false); + boolean hintSystem = prefs.getBoolean("hint_system", true); + llSystem.setVisibility(!system && hintSystem ? View.VISIBLE : View.GONE); + btnSystem.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + prefs.edit().putBoolean("hint_system", false).apply(); + llSystem.setVisibility(View.GONE); + } + }); + } + + private void checkExtras(Intent intent) { + // Approve request + if (intent.hasExtra(EXTRA_APPROVE)) { + Log.i(TAG, "Requesting VPN approval"); + swEnabled.toggle(); + } + + if (intent.hasExtra(EXTRA_LOGCAT)) { + Log.i(TAG, "Requesting logcat"); + Intent logcat = getIntentLogcat(); + if (logcat.resolveActivity(getPackageManager()) != null) + startActivityForResult(logcat, REQUEST_LOGCAT); + } + } + + private void updateApplicationList(final String search) { + Log.i(TAG, "Update search=" + search); + + new AsyncTask>() { + private boolean refreshing = true; + + @Override + protected void onPreExecute() { + swipeRefresh.post(new Runnable() { + @Override + public void run() { + if (refreshing) + swipeRefresh.setRefreshing(true); + } + }); + } + + @Override + protected List doInBackground(Object... arg) { + return Rule.getRules(false, ActivityMain.this); + } + + @Override + protected void onPostExecute(List result) { + if (running) { + if (adapter != null) { + adapter.set(result); + updateSearch(search); + } + + if (swipeRefresh != null) { + refreshing = false; + swipeRefresh.setRefreshing(false); + } + } + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void updateSearch(String search) { + if (menuSearch != null) { + SearchView searchView = (SearchView) menuSearch.getActionView(); + if (search == null) { + if (menuSearch.isActionViewExpanded()) + adapter.getFilter().filter(searchView.getQuery().toString()); + } else { + menuSearch.expandActionView(); + searchView.setQuery(search, true); + } + } + } + + private void checkDoze() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + final Intent doze = new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS); + if (Util.batteryOptimizing(this) && getPackageManager().resolveActivity(doze, 0) != null) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + if (!prefs.getBoolean("nodoze", false)) { + LayoutInflater inflater = LayoutInflater.from(this); + View view = inflater.inflate(R.layout.doze, null, false); + final CheckBox cbDontAsk = view.findViewById(R.id.cbDontAsk); + dialogDoze = new AlertDialog.Builder(this) + .setView(view) + .setCancelable(true) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + prefs.edit().putBoolean("nodoze", cbDontAsk.isChecked()).apply(); + startActivity(doze); + } + }) + .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + prefs.edit().putBoolean("nodoze", cbDontAsk.isChecked()).apply(); + } + }) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialogInterface) { + dialogDoze = null; + checkDataSaving(); + } + }) + .create(); + dialogDoze.show(); + } else + checkDataSaving(); + } else + checkDataSaving(); + } + } + + private void checkDataSaving() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + final Intent settings = new Intent( + Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS, + Uri.parse("package:" + getPackageName())); + if (Util.dataSaving(this) && getPackageManager().resolveActivity(settings, 0) != null) + try { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + if (!prefs.getBoolean("nodata", false)) { + LayoutInflater inflater = LayoutInflater.from(this); + View view = inflater.inflate(R.layout.datasaving, null, false); + final CheckBox cbDontAsk = view.findViewById(R.id.cbDontAsk); + dialogDoze = new AlertDialog.Builder(this) + .setView(view) + .setCancelable(true) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + prefs.edit().putBoolean("nodata", cbDontAsk.isChecked()).apply(); + startActivity(settings); + } + }) + .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + prefs.edit().putBoolean("nodata", cbDontAsk.isChecked()).apply(); + } + }) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialogInterface) { + dialogDoze = null; + } + }) + .create(); + dialogDoze.show(); + } + } catch (Throwable ex) { + Log.e(TAG, ex + "\n" + ex.getStackTrace()); + } + } + } + + private void menu_legend() { + TypedValue tv = new TypedValue(); + getTheme().resolveAttribute(R.attr.colorOn, tv, true); + int colorOn = tv.data; + getTheme().resolveAttribute(R.attr.colorOff, tv, true); + int colorOff = tv.data; + + // Create view + LayoutInflater inflater = LayoutInflater.from(this); + View view = inflater.inflate(R.layout.legend, null, false); + ImageView ivLockdownOn = view.findViewById(R.id.ivLockdownOn); + ImageView ivWifiOn = view.findViewById(R.id.ivWifiOn); + ImageView ivWifiOff = view.findViewById(R.id.ivWifiOff); + ImageView ivOtherOn = view.findViewById(R.id.ivOtherOn); + ImageView ivOtherOff = view.findViewById(R.id.ivOtherOff); + ImageView ivScreenOn = view.findViewById(R.id.ivScreenOn); + ImageView ivHostAllowed = view.findViewById(R.id.ivHostAllowed); + ImageView ivHostBlocked = view.findViewById(R.id.ivHostBlocked); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Drawable wrapLockdownOn = DrawableCompat.wrap(ivLockdownOn.getDrawable()); + Drawable wrapWifiOn = DrawableCompat.wrap(ivWifiOn.getDrawable()); + Drawable wrapWifiOff = DrawableCompat.wrap(ivWifiOff.getDrawable()); + Drawable wrapOtherOn = DrawableCompat.wrap(ivOtherOn.getDrawable()); + Drawable wrapOtherOff = DrawableCompat.wrap(ivOtherOff.getDrawable()); + Drawable wrapScreenOn = DrawableCompat.wrap(ivScreenOn.getDrawable()); + Drawable wrapHostAllowed = DrawableCompat.wrap(ivHostAllowed.getDrawable()); + Drawable wrapHostBlocked = DrawableCompat.wrap(ivHostBlocked.getDrawable()); + + DrawableCompat.setTint(wrapLockdownOn, colorOff); + DrawableCompat.setTint(wrapWifiOn, colorOn); + DrawableCompat.setTint(wrapWifiOff, colorOff); + DrawableCompat.setTint(wrapOtherOn, colorOn); + DrawableCompat.setTint(wrapOtherOff, colorOff); + DrawableCompat.setTint(wrapScreenOn, colorOn); + DrawableCompat.setTint(wrapHostAllowed, colorOn); + DrawableCompat.setTint(wrapHostBlocked, colorOff); + } + + + // Show dialog + dialogLegend = new AlertDialog.Builder(this) + .setView(view) + .setCancelable(true) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialogInterface) { + dialogLegend = null; + } + }) + .create(); + dialogLegend.show(); + } + + private void menu_lockdown(MenuItem item) { + item.setChecked(!item.isChecked()); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.edit().putBoolean("lockdown", item.isChecked()).apply(); + ServiceSinkhole.reload("lockdown", this, false); + WidgetLockdown.updateWidgets(this); + } + + private void menu_about() { + // Create view + LayoutInflater inflater = LayoutInflater.from(this); + View view = inflater.inflate(R.layout.about, null, false); + TextView tvVersionName = view.findViewById(R.id.tvVersionName); + TextView tvVersionCode = view.findViewById(R.id.tvVersionCode); + Button btnRate = view.findViewById(R.id.btnRate); + TextView tvEula = view.findViewById(R.id.tvEula); + TextView tvPrivacy = view.findViewById(R.id.tvPrivacy); + + // Show version + tvVersionName.setText(Util.getSelfVersionName(this)); + if (!Util.hasValidFingerprint(this)) + tvVersionName.setTextColor(Color.GRAY); + tvVersionCode.setText(Integer.toString(Util.getSelfVersionCode(this))); + + // Handle license + tvEula.setMovementMethod(LinkMovementMethod.getInstance()); + tvPrivacy.setMovementMethod(LinkMovementMethod.getInstance()); + + // Handle logcat + view.setOnClickListener(new View.OnClickListener() { + private short tap = 0; + private Toast toast = Toast.makeText(ActivityMain.this, "", Toast.LENGTH_SHORT); + + @Override + public void onClick(View view) { + tap++; + if (tap == 7) { + tap = 0; + toast.cancel(); + + Intent intent = getIntentLogcat(); + if (intent.resolveActivity(getPackageManager()) != null) + startActivityForResult(intent, REQUEST_LOGCAT); + + } else if (tap > 3) { + toast.setText(Integer.toString(7 - tap)); + toast.show(); + } + } + }); + + // Handle rate + btnRate.setVisibility(getIntentRate(this).resolveActivity(getPackageManager()) == null ? View.GONE : View.VISIBLE); + btnRate.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + startActivity(getIntentRate(ActivityMain.this)); + } + }); + + // Show dialog + dialogAbout = new AlertDialog.Builder(this) + .setView(view) + .setCancelable(true) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialogInterface) { + dialogAbout = null; + } + }) + .create(); + dialogAbout.show(); + } + + private void menu_apps() { + startActivity(getIntentApps(this)); + } + + private static Intent getIntentPro(Context context) { + if (Util.isPlayStoreInstall(context)) + return new Intent(context, ActivityPro.class); + else { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("https://contact.faircode.eu/?product=netguardstandalone")); + return intent; + } + } + + private static Intent getIntentInvite(Context context) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.app_name)); + intent.putExtra(Intent.EXTRA_TEXT, context.getString(R.string.msg_try) + "\n\nhttps://www.netguard.me/\n\n"); + return intent; + } + + private static Intent getIntentApps(Context context) { + return new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/dev?id=8420080860664580239")); + } + + private static Intent getIntentRate(Context context) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + context.getPackageName())); + if (intent.resolveActivity(context.getPackageManager()) == null) + intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + context.getPackageName())); + return intent; + } + + private static Intent getIntentSupport() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("https://github.com/M66B/NetGuard/blob/master/FAQ.md")); + return intent; + } + + private Intent getIntentLogcat() { + Intent intent; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + if (Util.isPackageInstalled("org.openintents.filemanager", this)) { + intent = new Intent("org.openintents.action.PICK_DIRECTORY"); + } else { + intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("https://play.google.com/store/apps/details?id=org.openintents.filemanager")); + } + } else { + intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TITLE, "logcat.txt"); + } + return intent; + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityPro.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityPro.java new file mode 100644 index 0000000..063d360 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityPro.java @@ -0,0 +1,447 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.Paint; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.NavUtils; + +import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN; + +public class ActivityPro extends AppCompatActivity { + private static final String TAG = "NetGuard.Pro"; + + private IAB iab; + + // adb shell pm clear com.android.vending + // android.test.purchased + + private static final int SKU_LOG_ID = 1; + private static final int SKU_FILTER_ID = 2; + private static final int SKU_NOTIFY_ID = 3; + private static final int SKU_SPEED_ID = 4; + private static final int SKU_THEME_ID = 5; + private static final int SKU_PRO1_ID = 6; + private static final int SKU_SUPPORT1_ID = 7; + private static final int SKU_SUPPORT2_ID = 8; + + public static final String SKU_LOG = "log"; + public static final String SKU_FILTER = "filter"; + public static final String SKU_NOTIFY = "notify"; + public static final String SKU_SPEED = "speed"; + public static final String SKU_THEME = "theme"; + public static final String SKU_PRO1 = "pro1"; + public static final String SKU_SUPPORT1 = "support1"; + public static final String SKU_SUPPORT2 = "support2"; + public static final String SKU_DONATION = "donation"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.i(TAG, "Create"); + Util.setTheme(this); + super.onCreate(savedInstanceState); + setContentView(R.layout.pro); + + getSupportActionBar().setTitle(R.string.title_pro); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + + // Initial state + updateState(); + + TextView tvLogTitle = findViewById(R.id.tvLogTitle); + TextView tvFilterTitle = findViewById(R.id.tvFilterTitle); + TextView tvNotifyTitle = findViewById(R.id.tvNotifyTitle); + TextView tvSpeedTitle = findViewById(R.id.tvSpeedTitle); + TextView tvThemeTitle = findViewById(R.id.tvThemeTitle); + TextView tvAllTitle = findViewById(R.id.tvAllTitle); + TextView tvDev1Title = findViewById(R.id.tvDev1Title); + TextView tvDev2Title = findViewById(R.id.tvDev2Title); + + tvLogTitle.setPaintFlags(tvLogTitle.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + tvFilterTitle.setPaintFlags(tvLogTitle.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + tvNotifyTitle.setPaintFlags(tvLogTitle.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + tvSpeedTitle.setPaintFlags(tvLogTitle.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + tvThemeTitle.setPaintFlags(tvLogTitle.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + tvAllTitle.setPaintFlags(tvLogTitle.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + tvDev1Title.setPaintFlags(tvLogTitle.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + tvDev2Title.setPaintFlags(tvLogTitle.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG); + + View.OnClickListener listener = new View.OnClickListener() { + @Override + public void onClick(View view) { + String sku; + switch (view.getId()) { + case R.id.tvLogTitle: + sku = SKU_LOG; + break; + case R.id.tvFilterTitle: + sku = SKU_FILTER; + break; + case R.id.tvNotifyTitle: + sku = SKU_NOTIFY; + break; + case R.id.tvSpeedTitle: + sku = SKU_SPEED; + break; + case R.id.tvThemeTitle: + sku = SKU_THEME; + break; + case R.id.tvAllTitle: + sku = SKU_PRO1; + break; + case R.id.tvDev1Title: + sku = SKU_SUPPORT1; + break; + case R.id.tvDev2Title: + sku = SKU_SUPPORT2; + break; + default: + sku = SKU_PRO1; + break; + } + + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("http://www.netguard.me/#" + sku)); + if (intent.resolveActivity(getPackageManager()) != null) + startActivity(intent); + } + }; + + tvLogTitle.setOnClickListener(listener); + tvFilterTitle.setOnClickListener(listener); + tvNotifyTitle.setOnClickListener(listener); + tvSpeedTitle.setOnClickListener(listener); + tvThemeTitle.setOnClickListener(listener); + tvAllTitle.setOnClickListener(listener); + tvDev1Title.setOnClickListener(listener); + tvDev2Title.setOnClickListener(listener); + + try { + iab = new IAB(new IAB.Delegate() { + @Override + public void onReady(final IAB iab) { + Log.i(TAG, "IAB ready"); + try { + iab.updatePurchases(); + updateState(); + + final Button btnLog = findViewById(R.id.btnLog); + final Button btnFilter = findViewById(R.id.btnFilter); + final Button btnNotify = findViewById(R.id.btnNotify); + final Button btnSpeed = findViewById(R.id.btnSpeed); + final Button btnTheme = findViewById(R.id.btnTheme); + final Button btnAll = findViewById(R.id.btnAll); + final Button btnDev1 = findViewById(R.id.btnDev1); + final Button btnDev2 = findViewById(R.id.btnDev2); + + View.OnClickListener listener = new View.OnClickListener() { + @Override + public void onClick(View view) { + try { + int id = 0; + PendingIntent pi = null; + if (view == btnLog) { + id = SKU_LOG_ID; + pi = iab.getBuyIntent(SKU_LOG, false); + } else if (view == btnFilter) { + id = SKU_FILTER_ID; + pi = iab.getBuyIntent(SKU_FILTER, false); + } else if (view == btnNotify) { + id = SKU_NOTIFY_ID; + pi = iab.getBuyIntent(SKU_NOTIFY, false); + } else if (view == btnSpeed) { + id = SKU_SPEED_ID; + pi = iab.getBuyIntent(SKU_SPEED, false); + } else if (view == btnTheme) { + id = SKU_THEME_ID; + pi = iab.getBuyIntent(SKU_THEME, false); + } else if (view == btnAll) { + id = SKU_PRO1_ID; + pi = iab.getBuyIntent(SKU_PRO1, false); + } else if (view == btnDev1) { + id = SKU_SUPPORT1_ID; + pi = iab.getBuyIntent(SKU_SUPPORT1, true); + } else if (view == btnDev2) { + id = SKU_SUPPORT2_ID; + pi = iab.getBuyIntent(SKU_SUPPORT2, true); + } + + if (id > 0 && pi != null) + startIntentSenderForResult(pi.getIntentSender(), id, new Intent(), 0, 0, 0); + } catch (Throwable ex) { + Log.i(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + }; + + btnLog.setOnClickListener(listener); + btnFilter.setOnClickListener(listener); + btnNotify.setOnClickListener(listener); + btnSpeed.setOnClickListener(listener); + btnTheme.setOnClickListener(listener); + btnAll.setOnClickListener(listener); + btnDev1.setOnClickListener(listener); + btnDev2.setOnClickListener(listener); + + btnLog.setEnabled(true); + btnFilter.setEnabled(true); + btnNotify.setEnabled(true); + btnSpeed.setEnabled(true); + btnTheme.setEnabled(true); + btnAll.setEnabled(true); + btnDev1.setEnabled(true); + btnDev2.setEnabled(true); + + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + }, this); + iab.bind(); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + + @Override + protected void onDestroy() { + Log.i(TAG, "Destroy"); + iab.unbind(); + iab = null; + super.onDestroy(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.pro, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Log.i(TAG, "Up"); + NavUtils.navigateUpFromSameTask(this); + return true; + case R.id.menu_challenge: + menu_challenge(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + if (IAB.isPurchased(SKU_DONATION, this) || Util.isPlayStoreInstall(this)) + menu.removeItem(R.id.menu_challenge); + + return super.onPrepareOptionsMenu(menu); + } + + private void menu_challenge() { + LayoutInflater inflater = LayoutInflater.from(this); + View view = inflater.inflate(R.layout.challenge, null, false); + + final AlertDialog dialog = new AlertDialog.Builder(this) + .setView(view) + .setCancelable(true) + .create(); + + String android_id = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID); + final String challenge = (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? Build.SERIAL : "O3" + android_id); + String seed = (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? "NetGuard2" : "NetGuard3"); + + // Challenge + TextView tvChallenge = view.findViewById(R.id.tvChallenge); + tvChallenge.setText(challenge); + + ImageButton ibCopy = view.findViewById(R.id.ibCopy); + ibCopy.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(getString(R.string.title_pro_challenge), challenge); + clipboard.setPrimaryClip(clip); + Toast.makeText(ActivityPro.this, android.R.string.copy, Toast.LENGTH_LONG).show(); + } + }); + + // Response + final EditText etResponse = view.findViewById(R.id.etResponse); + try { + final String response = Util.md5(challenge, seed); + etResponse.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Do nothing + } + + @Override + public void afterTextChanged(Editable editable) { + if (response.equals(editable.toString().toUpperCase())) { + IAB.setBought(SKU_DONATION, ActivityPro.this); + dialog.dismiss(); + invalidateOptionsMenu(); + updateState(); + } + } + }); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + ImageButton ibPaste = view.findViewById(R.id.ibPaste); + ibPaste.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + if (clipboard != null && + clipboard.hasPrimaryClip() && + clipboard.getPrimaryClipDescription().hasMimeType(MIMETYPE_TEXT_PLAIN)) { + ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0); + etResponse.setText(item.getText().toString()); + } + } + }); + + dialog.show(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + switch (requestCode) { + case SKU_LOG_ID: + IAB.setBought(SKU_LOG, this); + updateState(); + break; + case SKU_FILTER_ID: + IAB.setBought(SKU_FILTER, this); + updateState(); + break; + case SKU_NOTIFY_ID: + IAB.setBought(SKU_NOTIFY, this); + updateState(); + break; + case SKU_SPEED_ID: + IAB.setBought(SKU_SPEED, this); + updateState(); + break; + case SKU_THEME_ID: + IAB.setBought(SKU_THEME, this); + updateState(); + break; + case SKU_PRO1_ID: + IAB.setBought(SKU_PRO1, this); + updateState(); + break; + case SKU_SUPPORT1_ID: + IAB.setBought(SKU_SUPPORT1, this); + updateState(); + break; + case SKU_SUPPORT2_ID: + IAB.setBought(SKU_SUPPORT2, this); + updateState(); + break; + } + } + } + + private void updateState() { + Button btnLog = findViewById(R.id.btnLog); + Button btnFilter = findViewById(R.id.btnFilter); + Button btnNotify = findViewById(R.id.btnNotify); + Button btnSpeed = findViewById(R.id.btnSpeed); + Button btnTheme = findViewById(R.id.btnTheme); + Button btnAll = findViewById(R.id.btnAll); + Button btnDev1 = findViewById(R.id.btnDev1); + Button btnDev2 = findViewById(R.id.btnDev2); + TextView tvLog = findViewById(R.id.tvLog); + TextView tvFilter = findViewById(R.id.tvFilter); + TextView tvNotify = findViewById(R.id.tvNotify); + TextView tvSpeed = findViewById(R.id.tvSpeed); + TextView tvTheme = findViewById(R.id.tvTheme); + TextView tvAll = findViewById(R.id.tvAll); + TextView tvDev1 = findViewById(R.id.tvDev1); + TextView tvDev2 = findViewById(R.id.tvDev2); + + TextView tvLogUnavailable = findViewById(R.id.tvLogUnavailable); + TextView tvFilterUnavailable = findViewById(R.id.tvFilterUnavailable); + + boolean can = Util.canFilter(this); + + btnLog.setVisibility(IAB.isPurchased(SKU_LOG, this) || !can ? View.GONE : View.VISIBLE); + btnFilter.setVisibility(IAB.isPurchased(SKU_FILTER, this) || !can ? View.GONE : View.VISIBLE); + btnNotify.setVisibility(IAB.isPurchased(SKU_NOTIFY, this) ? View.GONE : View.VISIBLE); + btnSpeed.setVisibility(IAB.isPurchased(SKU_SPEED, this) ? View.GONE : View.VISIBLE); + btnTheme.setVisibility(IAB.isPurchased(SKU_THEME, this) ? View.GONE : View.VISIBLE); + btnAll.setVisibility(IAB.isPurchased(SKU_PRO1, this) ? View.GONE : View.VISIBLE); + btnDev1.setVisibility(IAB.isPurchased(SKU_SUPPORT1, this) ? View.GONE : View.VISIBLE); + btnDev2.setVisibility(IAB.isPurchased(SKU_SUPPORT2, this) ? View.GONE : View.VISIBLE); + + tvLog.setVisibility(IAB.isPurchased(SKU_LOG, this) && can ? View.VISIBLE : View.GONE); + tvFilter.setVisibility(IAB.isPurchased(SKU_FILTER, this) && can ? View.VISIBLE : View.GONE); + tvNotify.setVisibility(IAB.isPurchased(SKU_NOTIFY, this) ? View.VISIBLE : View.GONE); + tvSpeed.setVisibility(IAB.isPurchased(SKU_SPEED, this) ? View.VISIBLE : View.GONE); + tvTheme.setVisibility(IAB.isPurchased(SKU_THEME, this) ? View.VISIBLE : View.GONE); + tvAll.setVisibility(IAB.isPurchased(SKU_PRO1, this) ? View.VISIBLE : View.GONE); + tvDev1.setVisibility(IAB.isPurchased(SKU_SUPPORT1, this) ? View.VISIBLE : View.GONE); + tvDev2.setVisibility(IAB.isPurchased(SKU_SUPPORT2, this) ? View.VISIBLE : View.GONE); + + tvLogUnavailable.setVisibility(can ? View.GONE : View.VISIBLE); + tvFilterUnavailable.setVisibility(can ? View.GONE : View.VISIBLE); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivitySettings.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivitySettings.java new file mode 100644 index 0000000..5c38150 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivitySettings.java @@ -0,0 +1,1466 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.Manifest; +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.AssetFileDescriptor; +import android.database.Cursor; +import android.net.ConnectivityManager; +import android.net.Uri; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiManager; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.MultiSelectListPreference; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceGroup; +import android.preference.PreferenceScreen; +import android.preference.TwoStatePreference; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.ImageSpan; +import android.util.Log; +import android.util.Xml; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.NavUtils; +import androidx.core.util.PatternsCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.preference.PreferenceManager; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; +import org.xmlpull.v1.XmlSerializer; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.UnknownHostException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParserFactory; + +public class ActivitySettings extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = "NetGuard.Settings"; + + private boolean running = false; + + private static final int REQUEST_EXPORT = 1; + private static final int REQUEST_IMPORT = 2; + private static final int REQUEST_HOSTS = 3; + private static final int REQUEST_HOSTS_APPEND = 4; + private static final int REQUEST_CALL = 5; + + private AlertDialog dialogFilter = null; + + private static final Intent INTENT_VPN_SETTINGS = new Intent("android.net.vpn.SETTINGS"); + + protected void onCreate(Bundle savedInstanceState) { + Util.setTheme(this); + super.onCreate(savedInstanceState); + getFragmentManager().beginTransaction().replace(android.R.id.content, new FragmentSettings()).commit(); + getSupportActionBar().setTitle(R.string.menu_settings); + running = true; + } + + private PreferenceScreen getPreferenceScreen() { + return ((PreferenceFragment) getFragmentManager().findFragmentById(android.R.id.content)).getPreferenceScreen(); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + final PreferenceScreen screen = getPreferenceScreen(); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + + PreferenceGroup cat_options = (PreferenceGroup) ((PreferenceGroup) screen.findPreference("screen_options")).findPreference("category_options"); + PreferenceGroup cat_network = (PreferenceGroup) ((PreferenceGroup) screen.findPreference("screen_network_options")).findPreference("category_network_options"); + PreferenceGroup cat_advanced = (PreferenceGroup) ((PreferenceGroup) screen.findPreference("screen_advanced_options")).findPreference("category_advanced_options"); + PreferenceGroup cat_stats = (PreferenceGroup) ((PreferenceGroup) screen.findPreference("screen_stats")).findPreference("category_stats"); + PreferenceGroup cat_backup = (PreferenceGroup) ((PreferenceGroup) screen.findPreference("screen_backup")).findPreference("category_backup"); + + // Handle auto enable + Preference pref_auto_enable = screen.findPreference("auto_enable"); + pref_auto_enable.setTitle(getString(R.string.setting_auto, prefs.getString("auto_enable", "0"))); + + // Handle screen delay + Preference pref_screen_delay = screen.findPreference("screen_delay"); + pref_screen_delay.setTitle(getString(R.string.setting_delay, prefs.getString("screen_delay", "0"))); + + // Handle theme + Preference pref_screen_theme = screen.findPreference("theme"); + String theme = prefs.getString("theme", "teal"); + String[] themeNames = getResources().getStringArray(R.array.themeNames); + String[] themeValues = getResources().getStringArray(R.array.themeValues); + for (int i = 0; i < themeNames.length; i++) + if (theme.equals(themeValues[i])) { + pref_screen_theme.setTitle(getString(R.string.setting_theme, themeNames[i])); + break; + } + + // Wi-Fi home + MultiSelectListPreference pref_wifi_homes = (MultiSelectListPreference) screen.findPreference("wifi_homes"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) + cat_network.removePreference(pref_wifi_homes); + else { + Set ssids = prefs.getStringSet("wifi_homes", new HashSet()); + if (ssids.size() > 0) + pref_wifi_homes.setTitle(getString(R.string.setting_wifi_home, TextUtils.join(", ", ssids))); + else + pref_wifi_homes.setTitle(getString(R.string.setting_wifi_home, "-")); + + WifiManager wm = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); + List listSSID = new ArrayList<>(); + List configs = wm.getConfiguredNetworks(); + if (configs != null) + for (WifiConfiguration config : configs) + listSSID.add(config.SSID == null ? "NULL" : config.SSID); + for (String ssid : ssids) + if (!listSSID.contains(ssid)) + listSSID.add(ssid); + pref_wifi_homes.setEntries(listSSID.toArray(new CharSequence[0])); + pref_wifi_homes.setEntryValues(listSSID.toArray(new CharSequence[0])); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + TwoStatePreference pref_handover = + (TwoStatePreference) screen.findPreference("handover"); + cat_advanced.removePreference(pref_handover); + } + + Preference pref_reset_usage = screen.findPreference("reset_usage"); + pref_reset_usage.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Util.areYouSure(ActivitySettings.this, R.string.setting_reset_usage, new Util.DoubtListener() { + @Override + public void onSure() { + new AsyncTask() { + @Override + protected Throwable doInBackground(Object... objects) { + try { + DatabaseHelper.getInstance(ActivitySettings.this).resetUsage(-1); + return null; + } catch (Throwable ex) { + return ex; + } + } + + @Override + protected void onPostExecute(Throwable ex) { + if (ex == null) + Toast.makeText(ActivitySettings.this, R.string.msg_completed, Toast.LENGTH_LONG).show(); + else + Toast.makeText(ActivitySettings.this, ex.toString(), Toast.LENGTH_LONG).show(); + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + }); + return false; + } + }); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + TwoStatePreference pref_reload_onconnectivity = + (TwoStatePreference) screen.findPreference("reload_onconnectivity"); + pref_reload_onconnectivity.setChecked(true); + pref_reload_onconnectivity.setEnabled(false); + } + + // Handle port forwarding + Preference pref_forwarding = screen.findPreference("forwarding"); + pref_forwarding.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + startActivity(new Intent(ActivitySettings.this, ActivityForwarding.class)); + return true; + } + }); + + boolean can = Util.canFilter(this); + TwoStatePreference pref_log_app = (TwoStatePreference) screen.findPreference("log_app"); + TwoStatePreference pref_filter = (TwoStatePreference) screen.findPreference("filter"); + pref_log_app.setEnabled(can); + pref_filter.setEnabled(can); + if (!can) { + pref_log_app.setSummary(R.string.msg_unavailable); + pref_filter.setSummary(R.string.msg_unavailable); + } + + // VPN parameters + screen.findPreference("vpn4").setTitle(getString(R.string.setting_vpn4, prefs.getString("vpn4", "10.1.10.1"))); + screen.findPreference("vpn6").setTitle(getString(R.string.setting_vpn6, prefs.getString("vpn6", "fd00:1:fd00:1:fd00:1:fd00:1"))); + EditTextPreference pref_dns1 = (EditTextPreference) screen.findPreference("dns"); + EditTextPreference pref_dns2 = (EditTextPreference) screen.findPreference("dns2"); + EditTextPreference pref_validate = (EditTextPreference) screen.findPreference("validate"); + EditTextPreference pref_ttl = (EditTextPreference) screen.findPreference("ttl"); + pref_dns1.setTitle(getString(R.string.setting_dns, prefs.getString("dns", "-"))); + pref_dns2.setTitle(getString(R.string.setting_dns, prefs.getString("dns2", "-"))); + pref_validate.setTitle(getString(R.string.setting_validate, prefs.getString("validate", "www.google.com"))); + pref_ttl.setTitle(getString(R.string.setting_ttl, prefs.getString("ttl", "259200"))); + + // SOCKS5 parameters + screen.findPreference("socks5_addr").setTitle(getString(R.string.setting_socks5_addr, prefs.getString("socks5_addr", "-"))); + screen.findPreference("socks5_port").setTitle(getString(R.string.setting_socks5_port, prefs.getString("socks5_port", "-"))); + screen.findPreference("socks5_username").setTitle(getString(R.string.setting_socks5_username, prefs.getString("socks5_username", "-"))); + screen.findPreference("socks5_password").setTitle(getString(R.string.setting_socks5_password, TextUtils.isEmpty(prefs.getString("socks5_username", "")) ? "-" : "*****")); + + // PCAP parameters + screen.findPreference("pcap_record_size").setTitle(getString(R.string.setting_pcap_record_size, prefs.getString("pcap_record_size", "64"))); + screen.findPreference("pcap_file_size").setTitle(getString(R.string.setting_pcap_file_size, prefs.getString("pcap_file_size", "2"))); + + // Watchdog + screen.findPreference("watchdog").setTitle(getString(R.string.setting_watchdog, prefs.getString("watchdog", "0"))); + + // Show resolved + Preference pref_show_resolved = screen.findPreference("show_resolved"); + if (Util.isPlayStoreInstall(this)) + cat_advanced.removePreference(pref_show_resolved); + else + pref_show_resolved.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + startActivity(new Intent(ActivitySettings.this, ActivityDns.class)); + return true; + } + }); + + // Handle stats + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + cat_stats.removePreference(screen.findPreference("show_top")); + EditTextPreference pref_stats_frequency = (EditTextPreference) screen.findPreference("stats_frequency"); + EditTextPreference pref_stats_samples = (EditTextPreference) screen.findPreference("stats_samples"); + pref_stats_frequency.setTitle(getString(R.string.setting_stats_frequency, prefs.getString("stats_frequency", "1000"))); + pref_stats_samples.setTitle(getString(R.string.setting_stats_samples, prefs.getString("stats_samples", "90"))); + + // Handle export + Preference pref_export = screen.findPreference("export"); + pref_export.setEnabled(getIntentCreateExport().resolveActivity(getPackageManager()) != null); + pref_export.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + startActivityForResult(getIntentCreateExport(), ActivitySettings.REQUEST_EXPORT); + return true; + } + }); + + // Handle import + Preference pref_import = screen.findPreference("import"); + pref_import.setEnabled(getIntentOpenExport().resolveActivity(getPackageManager()) != null); + pref_import.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + startActivityForResult(getIntentOpenExport(), ActivitySettings.REQUEST_IMPORT); + return true; + } + }); + + // Hosts file settings + Preference pref_block_domains = screen.findPreference("use_hosts"); + EditTextPreference pref_rcode = (EditTextPreference) screen.findPreference("rcode"); + Preference pref_hosts_import = screen.findPreference("hosts_import"); + Preference pref_hosts_import_append = screen.findPreference("hosts_import_append"); + EditTextPreference pref_hosts_url = (EditTextPreference) screen.findPreference("hosts_url"); + final Preference pref_hosts_download = screen.findPreference("hosts_download"); + + pref_rcode.setTitle(getString(R.string.setting_rcode, prefs.getString("rcode", "3"))); + + if (Util.isPlayStoreInstall(this) || !Util.hasValidFingerprint(this)) + cat_options.removePreference(screen.findPreference("update_check")); + + if (Util.isPlayStoreInstall(this)) { + Log.i(TAG, "Play store install"); + cat_advanced.removePreference(pref_block_domains); + cat_advanced.removePreference(pref_rcode); + cat_advanced.removePreference(pref_forwarding); + cat_backup.removePreference(pref_hosts_import); + cat_backup.removePreference(pref_hosts_import_append); + cat_backup.removePreference(pref_hosts_url); + cat_backup.removePreference(pref_hosts_download); + + } else { + String last_import = prefs.getString("hosts_last_import", null); + String last_download = prefs.getString("hosts_last_download", null); + if (last_import != null) + pref_hosts_import.setSummary(getString(R.string.msg_import_last, last_import)); + if (last_download != null) + pref_hosts_download.setSummary(getString(R.string.msg_download_last, last_download)); + + // Handle hosts import + // https://github.com/Free-Software-for-Android/AdAway/wiki/HostsSources + pref_hosts_import.setEnabled(getIntentOpenHosts().resolveActivity(getPackageManager()) != null); + pref_hosts_import.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + startActivityForResult(getIntentOpenHosts(), ActivitySettings.REQUEST_HOSTS); + return true; + } + }); + pref_hosts_import_append.setEnabled(pref_hosts_import.isEnabled()); + pref_hosts_import_append.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + startActivityForResult(getIntentOpenHosts(), ActivitySettings.REQUEST_HOSTS_APPEND); + return true; + } + }); + + // Handle hosts file download + pref_hosts_url.setSummary(pref_hosts_url.getText()); + pref_hosts_download.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + final File tmp = new File(getFilesDir(), "hosts.tmp"); + final File hosts = new File(getFilesDir(), "hosts.txt"); + + EditTextPreference pref_hosts_url = (EditTextPreference) screen.findPreference("hosts_url"); + String hosts_url = pref_hosts_url.getText(); + if ("https://www.netguard.me/hosts".equals(hosts_url)) + hosts_url = BuildConfig.HOSTS_FILE_URI; + + try { + new DownloadTask(ActivitySettings.this, new URL(hosts_url), tmp, new DownloadTask.Listener() { + @Override + public void onCompleted() { + if (hosts.exists()) + hosts.delete(); + tmp.renameTo(hosts); + + String last = SimpleDateFormat.getDateTimeInstance().format(new Date().getTime()); + prefs.edit().putString("hosts_last_download", last).apply(); + + if (running) { + pref_hosts_download.setSummary(getString(R.string.msg_download_last, last)); + Toast.makeText(ActivitySettings.this, R.string.msg_downloaded, Toast.LENGTH_LONG).show(); + } + + ServiceSinkhole.reload("hosts file download", ActivitySettings.this, false); + } + + @Override + public void onCancelled() { + if (tmp.exists()) + tmp.delete(); + } + + @Override + public void onException(Throwable ex) { + if (tmp.exists()) + tmp.delete(); + + if (running) + Toast.makeText(ActivitySettings.this, ex.getMessage(), Toast.LENGTH_LONG).show(); + } + }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } catch (MalformedURLException ex) { + Toast.makeText(ActivitySettings.this, ex.toString(), Toast.LENGTH_LONG).show(); + } + return true; + } + }); + } + + // Development + if (!Util.isDebuggable(this)) + screen.removePreference(screen.findPreference("screen_development")); + + // Handle technical info + Preference.OnPreferenceClickListener listener = new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + updateTechnicalInfo(); + return true; + } + }; + + // Technical info + Preference pref_technical_info = screen.findPreference("technical_info"); + Preference pref_technical_network = screen.findPreference("technical_network"); + pref_technical_info.setEnabled(INTENT_VPN_SETTINGS.resolveActivity(this.getPackageManager()) != null); + pref_technical_info.setIntent(INTENT_VPN_SETTINGS); + pref_technical_info.setOnPreferenceClickListener(listener); + pref_technical_network.setOnPreferenceClickListener(listener); + updateTechnicalInfo(); + + markPro(screen.findPreference("theme"), ActivityPro.SKU_THEME); + markPro(screen.findPreference("install"), ActivityPro.SKU_NOTIFY); + markPro(screen.findPreference("show_stats"), ActivityPro.SKU_SPEED); + } + + @Override + protected void onResume() { + super.onResume(); + + checkPermissions(null); + + // Listen for preference changes + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.registerOnSharedPreferenceChangeListener(this); + + // Listen for interactive state changes + IntentFilter ifInteractive = new IntentFilter(); + ifInteractive.addAction(Intent.ACTION_SCREEN_ON); + ifInteractive.addAction(Intent.ACTION_SCREEN_OFF); + registerReceiver(interactiveStateReceiver, ifInteractive); + + // Listen for connectivity updates + IntentFilter ifConnectivity = new IntentFilter(); + ifConnectivity.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + registerReceiver(connectivityChangedReceiver, ifConnectivity); + } + + @Override + protected void onPause() { + super.onPause(); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.unregisterOnSharedPreferenceChangeListener(this); + + unregisterReceiver(interactiveStateReceiver); + unregisterReceiver(connectivityChangedReceiver); + } + + @Override + protected void onDestroy() { + running = false; + if (dialogFilter != null) { + dialogFilter.dismiss(); + dialogFilter = null; + } + super.onDestroy(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Log.i(TAG, "Up"); + NavUtils.navigateUpFromSameTask(this); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + @TargetApi(Build.VERSION_CODES.M) + public void onSharedPreferenceChanged(SharedPreferences prefs, String name) { + // Pro features + if ("theme".equals(name)) { + if (!"teal".equals(prefs.getString(name, "teal")) && !IAB.isPurchased(ActivityPro.SKU_THEME, this)) { + prefs.edit().putString(name, "teal").apply(); + ((ListPreference) getPreferenceScreen().findPreference(name)).setValue("teal"); + startActivity(new Intent(this, ActivityPro.class)); + return; + } + } else if ("install".equals(name)) { + if (prefs.getBoolean(name, false) && !IAB.isPurchased(ActivityPro.SKU_NOTIFY, this)) { + prefs.edit().putBoolean(name, false).apply(); + ((TwoStatePreference) getPreferenceScreen().findPreference(name)).setChecked(false); + startActivity(new Intent(this, ActivityPro.class)); + return; + } + } else if ("show_stats".equals(name)) { + if (prefs.getBoolean(name, false) && !IAB.isPurchased(ActivityPro.SKU_SPEED, this)) { + prefs.edit().putBoolean(name, false).apply(); + startActivity(new Intent(this, ActivityPro.class)); + return; + } + ((TwoStatePreference) getPreferenceScreen().findPreference(name)).setChecked(prefs.getBoolean(name, false)); + } + + Object value = prefs.getAll().get(name); + if (value instanceof String && "".equals(value)) + prefs.edit().remove(name).apply(); + + // Dependencies + if ("screen_on".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("whitelist_wifi".equals(name) || + "screen_wifi".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("whitelist_other".equals(name) || + "screen_other".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("whitelist_roaming".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("auto_enable".equals(name)) + getPreferenceScreen().findPreference(name).setTitle(getString(R.string.setting_auto, prefs.getString(name, "0"))); + + else if ("screen_delay".equals(name)) + getPreferenceScreen().findPreference(name).setTitle(getString(R.string.setting_delay, prefs.getString(name, "0"))); + + else if ("theme".equals(name) || "dark_theme".equals(name)) + recreate(); + + else if ("subnet".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("tethering".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("lan".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("ip6".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("wifi_homes".equals(name)) { + MultiSelectListPreference pref_wifi_homes = (MultiSelectListPreference) getPreferenceScreen().findPreference(name); + Set ssid = prefs.getStringSet(name, new HashSet()); + if (ssid.size() > 0) + pref_wifi_homes.setTitle(getString(R.string.setting_wifi_home, TextUtils.join(", ", ssid))); + else + pref_wifi_homes.setTitle(getString(R.string.setting_wifi_home, "-")); + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("use_metered".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("unmetered_2g".equals(name) || + "unmetered_3g".equals(name) || + "unmetered_4g".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("national_roaming".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("eu_roaming".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("disable_on_call".equals(name)) { + if (prefs.getBoolean(name, false)) { + if (checkPermissions(name)) + ServiceSinkhole.reload("changed " + name, this, false); + } else + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("lockdown_wifi".equals(name) || "lockdown_other".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("manage_system".equals(name)) { + boolean manage = prefs.getBoolean(name, false); + if (!manage) + prefs.edit().putBoolean("show_user", true).apply(); + prefs.edit().putBoolean("show_system", manage).apply(); + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("log_app".equals(name)) { + Intent ruleset = new Intent(ActivityMain.ACTION_RULES_CHANGED); + LocalBroadcastManager.getInstance(this).sendBroadcast(ruleset); + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("notify_access".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("filter".equals(name)) { + // Show dialog + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && prefs.getBoolean(name, false)) { + LayoutInflater inflater = LayoutInflater.from(ActivitySettings.this); + View view = inflater.inflate(R.layout.filter, null, false); + dialogFilter = new AlertDialog.Builder(ActivitySettings.this) + .setView(view) + .setCancelable(false) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Do nothing + } + }) + .setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialogInterface) { + dialogFilter = null; + } + }) + .create(); + dialogFilter.show(); + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && !prefs.getBoolean(name, false)) { + prefs.edit().putBoolean(name, true).apply(); + Toast.makeText(ActivitySettings.this, R.string.msg_filter4, Toast.LENGTH_SHORT).show(); + } + + ((TwoStatePreference) getPreferenceScreen().findPreference(name)).setChecked(prefs.getBoolean(name, false)); + + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("use_hosts".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("vpn4".equals(name)) { + String vpn4 = prefs.getString(name, null); + try { + checkAddress(vpn4, false); + prefs.edit().putString(name, vpn4.trim()).apply(); + } catch (Throwable ex) { + prefs.edit().remove(name).apply(); + ((EditTextPreference) getPreferenceScreen().findPreference(name)).setText(null); + if (!TextUtils.isEmpty(vpn4)) + Toast.makeText(ActivitySettings.this, ex.toString(), Toast.LENGTH_LONG).show(); + } + getPreferenceScreen().findPreference(name).setTitle( + getString(R.string.setting_vpn4, prefs.getString(name, "10.1.10.1"))); + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("vpn6".equals(name)) { + String vpn6 = prefs.getString(name, null); + try { + checkAddress(vpn6, false); + prefs.edit().putString(name, vpn6.trim()).apply(); + } catch (Throwable ex) { + prefs.edit().remove(name).apply(); + ((EditTextPreference) getPreferenceScreen().findPreference(name)).setText(null); + if (!TextUtils.isEmpty(vpn6)) + Toast.makeText(ActivitySettings.this, ex.toString(), Toast.LENGTH_LONG).show(); + } + getPreferenceScreen().findPreference(name).setTitle( + getString(R.string.setting_vpn6, prefs.getString(name, "fd00:1:fd00:1:fd00:1:fd00:1"))); + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("dns".equals(name) || "dns2".equals(name)) { + String dns = prefs.getString(name, null); + try { + checkAddress(dns, true); + prefs.edit().putString(name, dns.trim()).apply(); + } catch (Throwable ex) { + prefs.edit().remove(name).apply(); + ((EditTextPreference) getPreferenceScreen().findPreference(name)).setText(null); + if (!TextUtils.isEmpty(dns)) + Toast.makeText(ActivitySettings.this, ex.toString(), Toast.LENGTH_LONG).show(); + } + getPreferenceScreen().findPreference(name).setTitle( + getString(R.string.setting_dns, prefs.getString(name, "-"))); + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("validate".equals(name)) { + String host = prefs.getString(name, "www.google.com"); + try { + checkDomain(host); + prefs.edit().putString(name, host.trim()).apply(); + } catch (Throwable ex) { + prefs.edit().remove(name).apply(); + ((EditTextPreference) getPreferenceScreen().findPreference(name)).setText(null); + if (!TextUtils.isEmpty(host)) + Toast.makeText(ActivitySettings.this, ex.toString(), Toast.LENGTH_LONG).show(); + } + getPreferenceScreen().findPreference(name).setTitle( + getString(R.string.setting_validate, prefs.getString(name, "www.google.com"))); + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("ttl".equals(name)) + getPreferenceScreen().findPreference(name).setTitle( + getString(R.string.setting_ttl, prefs.getString(name, "259200"))); + + else if ("rcode".equals(name)) { + getPreferenceScreen().findPreference(name).setTitle( + getString(R.string.setting_rcode, prefs.getString(name, "3"))); + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("socks5_enabled".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + + else if ("socks5_addr".equals(name)) { + String socks5_addr = prefs.getString(name, null); + try { + if (!TextUtils.isEmpty(socks5_addr) && !Util.isNumericAddress(socks5_addr)) + throw new IllegalArgumentException("Bad address"); + } catch (Throwable ex) { + prefs.edit().remove(name).apply(); + ((EditTextPreference) getPreferenceScreen().findPreference(name)).setText(null); + if (!TextUtils.isEmpty(socks5_addr)) + Toast.makeText(ActivitySettings.this, ex.toString(), Toast.LENGTH_LONG).show(); + } + getPreferenceScreen().findPreference(name).setTitle( + getString(R.string.setting_socks5_addr, prefs.getString(name, "-"))); + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("socks5_port".equals(name)) { + getPreferenceScreen().findPreference(name).setTitle(getString(R.string.setting_socks5_port, prefs.getString(name, "-"))); + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("socks5_username".equals(name)) { + getPreferenceScreen().findPreference(name).setTitle(getString(R.string.setting_socks5_username, prefs.getString(name, "-"))); + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("socks5_password".equals(name)) { + getPreferenceScreen().findPreference(name).setTitle(getString(R.string.setting_socks5_password, TextUtils.isEmpty(prefs.getString(name, "")) ? "-" : "*****")); + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("pcap_record_size".equals(name) || "pcap_file_size".equals(name)) { + if ("pcap_record_size".equals(name)) + getPreferenceScreen().findPreference(name).setTitle(getString(R.string.setting_pcap_record_size, prefs.getString(name, "64"))); + else + getPreferenceScreen().findPreference(name).setTitle(getString(R.string.setting_pcap_file_size, prefs.getString(name, "2"))); + + ServiceSinkhole.setPcap(false, this); + + File pcap_file = new File(getDir("data", MODE_PRIVATE), "netguard.pcap"); + if (pcap_file.exists() && !pcap_file.delete()) + Log.w(TAG, "Delete PCAP failed"); + + if (prefs.getBoolean("pcap", false)) + ServiceSinkhole.setPcap(true, this); + + } else if ("watchdog".equals(name)) { + getPreferenceScreen().findPreference(name).setTitle(getString(R.string.setting_watchdog, prefs.getString(name, "0"))); + ServiceSinkhole.reload("changed " + name, this, false); + + } else if ("show_stats".equals(name)) + ServiceSinkhole.reloadStats("changed " + name, this); + + else if ("stats_frequency".equals(name)) + getPreferenceScreen().findPreference(name).setTitle(getString(R.string.setting_stats_frequency, prefs.getString(name, "1000"))); + + else if ("stats_samples".equals(name)) + getPreferenceScreen().findPreference(name).setTitle(getString(R.string.setting_stats_samples, prefs.getString(name, "90"))); + + else if ("hosts_url".equals(name)) + getPreferenceScreen().findPreference(name).setSummary(prefs.getString(name, BuildConfig.HOSTS_FILE_URI)); + + else if ("loglevel".equals(name)) + ServiceSinkhole.reload("changed " + name, this, false); + } + + @TargetApi(Build.VERSION_CODES.M) + private boolean checkPermissions(String name) { + PreferenceScreen screen = getPreferenceScreen(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + + // Check if permission was revoked + if ((name == null || "disable_on_call".equals(name)) && prefs.getBoolean("disable_on_call", false)) + if (!Util.hasPhoneStatePermission(this)) { + prefs.edit().putBoolean("disable_on_call", false).apply(); + ((TwoStatePreference) screen.findPreference("disable_on_call")).setChecked(false); + + requestPermissions(new String[]{Manifest.permission.READ_PHONE_STATE}, REQUEST_CALL); + + if (name != null) + return false; + } + + return true; + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + PreferenceScreen screen = getPreferenceScreen(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + + boolean granted = (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED); + + if (requestCode == REQUEST_CALL) { + prefs.edit().putBoolean("disable_on_call", granted).apply(); + ((TwoStatePreference) screen.findPreference("disable_on_call")).setChecked(granted); + } + + if (granted) + ServiceSinkhole.reload("permission granted", this, false); + } + + private void checkAddress(String address, boolean allow_local) throws IllegalArgumentException, UnknownHostException { + if (address != null) + address = address.trim(); + if (TextUtils.isEmpty(address)) + throw new IllegalArgumentException("Bad address"); + if (!Util.isNumericAddress(address)) + throw new IllegalArgumentException("Bad address"); + if (!allow_local) { + InetAddress iaddr = InetAddress.getByName(address); + if (iaddr.isLoopbackAddress() || iaddr.isAnyLocalAddress()) + throw new IllegalArgumentException("Bad address"); + } + } + + private void checkDomain(String address) throws IllegalArgumentException, UnknownHostException { + if (address != null) + address = address.trim(); + if (TextUtils.isEmpty(address)) + throw new IllegalArgumentException("Bad address"); + if (Util.isNumericAddress(address)) + throw new IllegalArgumentException("Bad address"); + if (!PatternsCompat.DOMAIN_NAME.matcher(address).matches()) + throw new IllegalArgumentException("Bad address"); + } + + private BroadcastReceiver interactiveStateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Util.logExtras(intent); + updateTechnicalInfo(); + } + }; + + private BroadcastReceiver connectivityChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Util.logExtras(intent); + updateTechnicalInfo(); + } + }; + + private void markPro(Preference pref, String sku) { + if (sku == null || !IAB.isPurchased(sku, this)) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean dark = prefs.getBoolean("dark_theme", false); + SpannableStringBuilder ssb = new SpannableStringBuilder(" " + pref.getTitle()); + ssb.setSpan(new ImageSpan(this, dark ? R.drawable.ic_shopping_cart_white_24dp : R.drawable.ic_shopping_cart_black_24dp), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + pref.setTitle(ssb); + } + } + + private void updateTechnicalInfo() { + PreferenceScreen screen = getPreferenceScreen(); + Preference pref_technical_info = screen.findPreference("technical_info"); + Preference pref_technical_network = screen.findPreference("technical_network"); + + pref_technical_info.setSummary(Util.getGeneralInfo(this)); + pref_technical_network.setSummary(Util.getNetworkInfo(this)); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, final Intent data) { + Log.i(TAG, "onActivityResult request=" + requestCode + " result=" + requestCode + " ok=" + (resultCode == RESULT_OK)); + if (requestCode == REQUEST_EXPORT) { + if (resultCode == RESULT_OK && data != null) + handleExport(data); + + } else if (requestCode == REQUEST_IMPORT) { + if (resultCode == RESULT_OK && data != null) + handleImport(data); + + } else if (requestCode == REQUEST_HOSTS) { + if (resultCode == RESULT_OK && data != null) + handleHosts(data, false); + + } else if (requestCode == REQUEST_HOSTS_APPEND) { + if (resultCode == RESULT_OK && data != null) + handleHosts(data, true); + + } else { + Log.w(TAG, "Unknown activity result request=" + requestCode); + super.onActivityResult(requestCode, resultCode, data); + } + } + + private Intent getIntentCreateExport() { + Intent intent; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { + if (Util.isPackageInstalled("org.openintents.filemanager", this)) { + intent = new Intent("org.openintents.action.PICK_DIRECTORY"); + } else { + intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("https://play.google.com/store/apps/details?id=org.openintents.filemanager")); + } + } else { + intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); // text/xml + intent.putExtra(Intent.EXTRA_TITLE, "netguard_" + new SimpleDateFormat("yyyyMMdd").format(new Date().getTime()) + ".xml"); + } + return intent; + } + + private Intent getIntentOpenExport() { + Intent intent; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) + intent = new Intent(Intent.ACTION_GET_CONTENT); + else + intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); // text/xml + return intent; + } + + private Intent getIntentOpenHosts() { + Intent intent; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) + intent = new Intent(Intent.ACTION_GET_CONTENT); + else + intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); // text/plain + return intent; + } + + private void handleExport(final Intent data) { + new AsyncTask() { + @Override + protected Throwable doInBackground(Object... objects) { + OutputStream out = null; + try { + Uri target = data.getData(); + if (data.hasExtra("org.openintents.extra.DIR_PATH")) + target = Uri.parse(target + "/netguard_" + new SimpleDateFormat("yyyyMMdd").format(new Date().getTime()) + ".xml"); + Log.i(TAG, "Writing URI=" + target); + out = getContentResolver().openOutputStream(target); + xmlExport(out); + return null; + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + return ex; + } finally { + if (out != null) + try { + out.close(); + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + } + + @Override + protected void onPostExecute(Throwable ex) { + if (running) { + if (ex == null) + Toast.makeText(ActivitySettings.this, R.string.msg_completed, Toast.LENGTH_LONG).show(); + else + Toast.makeText(ActivitySettings.this, ex.toString(), Toast.LENGTH_LONG).show(); + } + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void handleHosts(final Intent data, final boolean append) { + new AsyncTask() { + @Override + protected Throwable doInBackground(Object... objects) { + File hosts = new File(getFilesDir(), "hosts.txt"); + + FileOutputStream out = null; + InputStream in = null; + try { + Log.i(TAG, "Reading URI=" + data.getData()); + ContentResolver resolver = getContentResolver(); + String[] streamTypes = resolver.getStreamTypes(data.getData(), "*/*"); + String streamType = (streamTypes == null || streamTypes.length == 0 ? "*/*" : streamTypes[0]); + AssetFileDescriptor descriptor = resolver.openTypedAssetFileDescriptor(data.getData(), streamType, null); + in = descriptor.createInputStream(); + out = new FileOutputStream(hosts, append); + + int len; + long total = 0; + byte[] buf = new byte[4096]; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + total += len; + } + Log.i(TAG, "Copied bytes=" + total); + + return null; + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + return ex; + } finally { + if (out != null) + try { + out.close(); + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + if (in != null) + try { + in.close(); + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + } + + @Override + protected void onPostExecute(Throwable ex) { + if (running) { + if (ex == null) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ActivitySettings.this); + String last = SimpleDateFormat.getDateTimeInstance().format(new Date().getTime()); + prefs.edit().putString("hosts_last_import", last).apply(); + + if (running) { + getPreferenceScreen().findPreference("hosts_import").setSummary(getString(R.string.msg_import_last, last)); + Toast.makeText(ActivitySettings.this, R.string.msg_completed, Toast.LENGTH_LONG).show(); + } + + ServiceSinkhole.reload("hosts import", ActivitySettings.this, false); + } else + Toast.makeText(ActivitySettings.this, ex.toString(), Toast.LENGTH_LONG).show(); + } + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void handleImport(final Intent data) { + new AsyncTask() { + @Override + protected Throwable doInBackground(Object... objects) { + InputStream in = null; + try { + Log.i(TAG, "Reading URI=" + data.getData()); + ContentResolver resolver = getContentResolver(); + String[] streamTypes = resolver.getStreamTypes(data.getData(), "*/*"); + String streamType = (streamTypes == null || streamTypes.length == 0 ? "*/*" : streamTypes[0]); + AssetFileDescriptor descriptor = resolver.openTypedAssetFileDescriptor(data.getData(), streamType, null); + in = descriptor.createInputStream(); + xmlImport(in); + return null; + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + return ex; + } finally { + if (in != null) + try { + in.close(); + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + } + + @Override + protected void onPostExecute(Throwable ex) { + if (running) { + if (ex == null) { + Toast.makeText(ActivitySettings.this, R.string.msg_completed, Toast.LENGTH_LONG).show(); + ServiceSinkhole.reloadStats("import", ActivitySettings.this); + // Update theme, request permissions + recreate(); + } else + Toast.makeText(ActivitySettings.this, ex.toString(), Toast.LENGTH_LONG).show(); + } + } + }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private void xmlExport(OutputStream out) throws IOException { + XmlSerializer serializer = Xml.newSerializer(); + serializer.setOutput(out, "UTF-8"); + serializer.startDocument(null, true); + serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); + serializer.startTag(null, "netguard"); + + serializer.startTag(null, "application"); + xmlExport(PreferenceManager.getDefaultSharedPreferences(this), serializer); + serializer.endTag(null, "application"); + + serializer.startTag(null, "wifi"); + xmlExport(getSharedPreferences("wifi", Context.MODE_PRIVATE), serializer); + serializer.endTag(null, "wifi"); + + serializer.startTag(null, "mobile"); + xmlExport(getSharedPreferences("other", Context.MODE_PRIVATE), serializer); + serializer.endTag(null, "mobile"); + + serializer.startTag(null, "screen_wifi"); + xmlExport(getSharedPreferences("screen_wifi", Context.MODE_PRIVATE), serializer); + serializer.endTag(null, "screen_wifi"); + + serializer.startTag(null, "screen_other"); + xmlExport(getSharedPreferences("screen_other", Context.MODE_PRIVATE), serializer); + serializer.endTag(null, "screen_other"); + + serializer.startTag(null, "roaming"); + xmlExport(getSharedPreferences("roaming", Context.MODE_PRIVATE), serializer); + serializer.endTag(null, "roaming"); + + serializer.startTag(null, "lockdown"); + xmlExport(getSharedPreferences("lockdown", Context.MODE_PRIVATE), serializer); + serializer.endTag(null, "lockdown"); + + serializer.startTag(null, "apply"); + xmlExport(getSharedPreferences("apply", Context.MODE_PRIVATE), serializer); + serializer.endTag(null, "apply"); + + serializer.startTag(null, "notify"); + xmlExport(getSharedPreferences("notify", Context.MODE_PRIVATE), serializer); + serializer.endTag(null, "notify"); + + serializer.startTag(null, "filter"); + filterExport(serializer); + serializer.endTag(null, "filter"); + + serializer.startTag(null, "forward"); + forwardExport(serializer); + serializer.endTag(null, "forward"); + + serializer.endTag(null, "netguard"); + serializer.endDocument(); + serializer.flush(); + } + + private void xmlExport(SharedPreferences prefs, XmlSerializer serializer) throws IOException { + Map settings = prefs.getAll(); + for (String key : settings.keySet()) { + Object value = settings.get(key); + + if ("imported".equals(key)) + continue; + + if (value instanceof Boolean) { + serializer.startTag(null, "setting"); + serializer.attribute(null, "key", key); + serializer.attribute(null, "type", "boolean"); + serializer.attribute(null, "value", value.toString()); + serializer.endTag(null, "setting"); + + } else if (value instanceof Integer) { + serializer.startTag(null, "setting"); + serializer.attribute(null, "key", key); + serializer.attribute(null, "type", "integer"); + serializer.attribute(null, "value", value.toString()); + serializer.endTag(null, "setting"); + + } else if (value instanceof String) { + serializer.startTag(null, "setting"); + serializer.attribute(null, "key", key); + serializer.attribute(null, "type", "string"); + serializer.attribute(null, "value", value.toString()); + serializer.endTag(null, "setting"); + + } else if (value instanceof Set) { + Set set = (Set) value; + serializer.startTag(null, "setting"); + serializer.attribute(null, "key", key); + serializer.attribute(null, "type", "set"); + serializer.attribute(null, "value", TextUtils.join("\n", set)); + serializer.endTag(null, "setting"); + + } else + Log.e(TAG, "Unknown key=" + key); + } + } + + private void filterExport(XmlSerializer serializer) throws IOException { + try (Cursor cursor = DatabaseHelper.getInstance(this).getAccess()) { + int colUid = cursor.getColumnIndex("uid"); + int colVersion = cursor.getColumnIndex("version"); + int colProtocol = cursor.getColumnIndex("protocol"); + int colDAddr = cursor.getColumnIndex("daddr"); + int colDPort = cursor.getColumnIndex("dport"); + int colTime = cursor.getColumnIndex("time"); + int colBlock = cursor.getColumnIndex("block"); + while (cursor.moveToNext()) + for (String pkg : getPackages(cursor.getInt(colUid))) { + serializer.startTag(null, "rule"); + serializer.attribute(null, "pkg", pkg); + serializer.attribute(null, "version", Integer.toString(cursor.getInt(colVersion))); + serializer.attribute(null, "protocol", Integer.toString(cursor.getInt(colProtocol))); + serializer.attribute(null, "daddr", cursor.getString(colDAddr)); + serializer.attribute(null, "dport", Integer.toString(cursor.getInt(colDPort))); + serializer.attribute(null, "time", Long.toString(cursor.getLong(colTime))); + serializer.attribute(null, "block", Integer.toString(cursor.getInt(colBlock))); + serializer.endTag(null, "rule"); + } + } + } + + private void forwardExport(XmlSerializer serializer) throws IOException { + try (Cursor cursor = DatabaseHelper.getInstance(this).getForwarding()) { + int colProtocol = cursor.getColumnIndex("protocol"); + int colDPort = cursor.getColumnIndex("dport"); + int colRAddr = cursor.getColumnIndex("raddr"); + int colRPort = cursor.getColumnIndex("rport"); + int colRUid = cursor.getColumnIndex("ruid"); + while (cursor.moveToNext()) + for (String pkg : getPackages(cursor.getInt(colRUid))) { + serializer.startTag(null, "port"); + serializer.attribute(null, "pkg", pkg); + serializer.attribute(null, "protocol", Integer.toString(cursor.getInt(colProtocol))); + serializer.attribute(null, "dport", Integer.toString(cursor.getInt(colDPort))); + serializer.attribute(null, "raddr", cursor.getString(colRAddr)); + serializer.attribute(null, "rport", Integer.toString(cursor.getInt(colRPort))); + serializer.endTag(null, "port"); + } + } + } + + private String[] getPackages(int uid) { + if (uid == 0) + return new String[]{"root"}; + else if (uid == 1013) + return new String[]{"mediaserver"}; + else if (uid == 9999) + return new String[]{"nobody"}; + else { + String pkgs[] = getPackageManager().getPackagesForUid(uid); + if (pkgs == null) + return new String[0]; + else + return pkgs; + } + } + + private void xmlImport(InputStream in) throws IOException, SAXException, ParserConfigurationException { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.unregisterOnSharedPreferenceChangeListener(this); + prefs.edit().putBoolean("enabled", false).apply(); + ServiceSinkhole.stop("import", this, false); + + XMLReader reader = SAXParserFactory.newInstance().newSAXParser().getXMLReader(); + XmlImportHandler handler = new XmlImportHandler(this); + reader.setContentHandler(handler); + reader.parse(new InputSource(in)); + + xmlImport(handler.application, prefs); + xmlImport(handler.wifi, getSharedPreferences("wifi", Context.MODE_PRIVATE)); + xmlImport(handler.mobile, getSharedPreferences("other", Context.MODE_PRIVATE)); + xmlImport(handler.screen_wifi, getSharedPreferences("screen_wifi", Context.MODE_PRIVATE)); + xmlImport(handler.screen_other, getSharedPreferences("screen_other", Context.MODE_PRIVATE)); + xmlImport(handler.roaming, getSharedPreferences("roaming", Context.MODE_PRIVATE)); + xmlImport(handler.lockdown, getSharedPreferences("lockdown", Context.MODE_PRIVATE)); + xmlImport(handler.apply, getSharedPreferences("apply", Context.MODE_PRIVATE)); + xmlImport(handler.notify, getSharedPreferences("notify", Context.MODE_PRIVATE)); + + // Upgrade imported settings + ReceiverAutostart.upgrade(true, this); + + DatabaseHelper.clearCache(); + + // Refresh UI + prefs.edit().putBoolean("imported", true).apply(); + prefs.registerOnSharedPreferenceChangeListener(this); + } + + private void xmlImport(Map settings, SharedPreferences prefs) { + SharedPreferences.Editor editor = prefs.edit(); + + // Clear existing setting + for (String key : prefs.getAll().keySet()) + if (!"enabled".equals(key)) + editor.remove(key); + + // Apply new settings + for (String key : settings.keySet()) { + Object value = settings.get(key); + if (value instanceof Boolean) + editor.putBoolean(key, (Boolean) value); + else if (value instanceof Integer) + editor.putInt(key, (Integer) value); + else if (value instanceof String) + editor.putString(key, (String) value); + else if (value instanceof Set) + editor.putStringSet(key, (Set) value); + else + Log.e(TAG, "Unknown type=" + value.getClass()); + } + + editor.apply(); + } + + private class XmlImportHandler extends DefaultHandler { + private Context context; + public boolean enabled = false; + public Map application = new HashMap<>(); + public Map wifi = new HashMap<>(); + public Map mobile = new HashMap<>(); + public Map screen_wifi = new HashMap<>(); + public Map screen_other = new HashMap<>(); + public Map roaming = new HashMap<>(); + public Map lockdown = new HashMap<>(); + public Map apply = new HashMap<>(); + public Map notify = new HashMap<>(); + private Map current = null; + + public XmlImportHandler(Context context) { + this.context = context; + } + + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) { + if (qName.equals("netguard")) + ; // Ignore + + else if (qName.equals("application")) + current = application; + + else if (qName.equals("wifi")) + current = wifi; + + else if (qName.equals("mobile")) + current = mobile; + + else if (qName.equals("screen_wifi")) + current = screen_wifi; + + else if (qName.equals("screen_other")) + current = screen_other; + + else if (qName.equals("roaming")) + current = roaming; + + else if (qName.equals("lockdown")) + current = lockdown; + + else if (qName.equals("apply")) + current = apply; + + else if (qName.equals("notify")) + current = notify; + + else if (qName.equals("filter")) { + current = null; + Log.i(TAG, "Clearing filters"); + DatabaseHelper.getInstance(context).clearAccess(); + + } else if (qName.equals("forward")) { + current = null; + Log.i(TAG, "Clearing forwards"); + DatabaseHelper.getInstance(context).deleteForward(); + + } else if (qName.equals("setting")) { + String key = attributes.getValue("key"); + String type = attributes.getValue("type"); + String value = attributes.getValue("value"); + + if (current == null) + Log.e(TAG, "No current key=" + key); + else { + if ("enabled".equals(key)) + enabled = Boolean.parseBoolean(value); + else { + if (current == application) { + // Pro features + if ("log".equals(key)) { + if (!IAB.isPurchased(ActivityPro.SKU_LOG, context)) + return; + } else if ("theme".equals(key)) { + if (!IAB.isPurchased(ActivityPro.SKU_THEME, context)) + return; + } else if ("show_stats".equals(key)) { + if (!IAB.isPurchased(ActivityPro.SKU_SPEED, context)) + return; + } + + if ("hosts_last_import".equals(key) || "hosts_last_download".equals(key)) + return; + } + + if ("boolean".equals(type)) + current.put(key, Boolean.parseBoolean(value)); + else if ("integer".equals(type)) + current.put(key, Integer.parseInt(value)); + else if ("string".equals(type)) + current.put(key, value); + else if ("set".equals(type)) { + Set set = new HashSet<>(); + if (!TextUtils.isEmpty(value)) + for (String s : value.split("\n")) + set.add(s); + current.put(key, set); + } else + Log.e(TAG, "Unknown type key=" + key); + } + } + + } else if (qName.equals("rule")) { + String pkg = attributes.getValue("pkg"); + + String version = attributes.getValue("version"); + String protocol = attributes.getValue("protocol"); + + Packet packet = new Packet(); + packet.version = (version == null ? 4 : Integer.parseInt(version)); + packet.protocol = (protocol == null ? 6 /* TCP */ : Integer.parseInt(protocol)); + packet.daddr = attributes.getValue("daddr"); + packet.dport = Integer.parseInt(attributes.getValue("dport")); + packet.time = Long.parseLong(attributes.getValue("time")); + + int block = Integer.parseInt(attributes.getValue("block")); + + try { + packet.uid = getUid(pkg); + DatabaseHelper.getInstance(context).updateAccess(packet, null, block); + } catch (PackageManager.NameNotFoundException ex) { + Log.w(TAG, "Package not found pkg=" + pkg); + } + + } else if (qName.equals("port")) { + String pkg = attributes.getValue("pkg"); + int protocol = Integer.parseInt(attributes.getValue("protocol")); + int dport = Integer.parseInt(attributes.getValue("dport")); + String raddr = attributes.getValue("raddr"); + int rport = Integer.parseInt(attributes.getValue("rport")); + + try { + int uid = getUid(pkg); + DatabaseHelper.getInstance(context).addForward(protocol, dport, raddr, rport, uid); + } catch (PackageManager.NameNotFoundException ex) { + Log.w(TAG, "Package not found pkg=" + pkg); + } + + } else + Log.e(TAG, "Unknown element qname=" + qName); + } + + private int getUid(String pkg) throws PackageManager.NameNotFoundException { + if ("root".equals(pkg)) + return 0; + else if ("android.media".equals(pkg)) + return 1013; + else if ("android.multicast".equals(pkg)) + return 1020; + else if ("android.gps".equals(pkg)) + return 1021; + else if ("android.dns".equals(pkg)) + return 1051; + else if ("nobody".equals(pkg)) + return 9999; + else + return getPackageManager().getApplicationInfo(pkg, 0).uid; + } + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterAccess.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterAccess.java new file mode 100644 index 0000000..1ddbc11 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterAccess.java @@ -0,0 +1,186 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.Context; +import android.content.res.TypedArray; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; +import android.os.Build; +import android.text.SpannableString; +import android.text.style.UnderlineSpan; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.view.ViewCompat; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.text.SimpleDateFormat; + +public class AdapterAccess extends CursorAdapter { + private int colVersion; + private int colProtocol; + private int colDaddr; + private int colDPort; + private int colTime; + private int colAllowed; + private int colBlock; + private int colCount; + private int colSent; + private int colReceived; + private int colConnections; + + private int colorText; + private int colorOn; + private int colorOff; + + public AdapterAccess(Context context, Cursor cursor) { + super(context, cursor, 0); + colVersion = cursor.getColumnIndex("version"); + colProtocol = cursor.getColumnIndex("protocol"); + colDaddr = cursor.getColumnIndex("daddr"); + colDPort = cursor.getColumnIndex("dport"); + colTime = cursor.getColumnIndex("time"); + colAllowed = cursor.getColumnIndex("allowed"); + colBlock = cursor.getColumnIndex("block"); + colCount = cursor.getColumnIndex("count"); + colSent = cursor.getColumnIndex("sent"); + colReceived = cursor.getColumnIndex("received"); + colConnections = cursor.getColumnIndex("connections"); + + TypedArray ta = context.getTheme().obtainStyledAttributes(new int[]{android.R.attr.textColorSecondary}); + try { + colorText = ta.getColor(0, 0); + } finally { + ta.recycle(); + } + + TypedValue tv = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.colorOn, tv, true); + colorOn = tv.data; + context.getTheme().resolveAttribute(R.attr.colorOff, tv, true); + colorOff = tv.data; + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return LayoutInflater.from(context).inflate(R.layout.access, parent, false); + } + + @Override + public void bindView(final View view, final Context context, final Cursor cursor) { + // Get values + final int version = cursor.getInt(colVersion); + final int protocol = cursor.getInt(colProtocol); + final String daddr = cursor.getString(colDaddr); + final int dport = cursor.getInt(colDPort); + long time = cursor.getLong(colTime); + int allowed = cursor.getInt(colAllowed); + int block = cursor.getInt(colBlock); + int count = cursor.getInt(colCount); + long sent = cursor.isNull(colSent) ? -1 : cursor.getLong(colSent); + long received = cursor.isNull(colReceived) ? -1 : cursor.getLong(colReceived); + int connections = cursor.isNull(colConnections) ? -1 : cursor.getInt(colConnections); + + // Get views + TextView tvTime = view.findViewById(R.id.tvTime); + ImageView ivBlock = view.findViewById(R.id.ivBlock); + final TextView tvDest = view.findViewById(R.id.tvDest); + LinearLayout llTraffic = view.findViewById(R.id.llTraffic); + TextView tvConnections = view.findViewById(R.id.tvConnections); + TextView tvTraffic = view.findViewById(R.id.tvTraffic); + + // Set values + tvTime.setText(new SimpleDateFormat("dd HH:mm").format(time)); + if (block < 0) + ivBlock.setImageDrawable(null); + else { + ivBlock.setImageResource(block > 0 ? R.drawable.host_blocked : R.drawable.host_allowed); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Drawable wrap = DrawableCompat.wrap(ivBlock.getDrawable()); + DrawableCompat.setTint(wrap, block > 0 ? colorOff : colorOn); + } + } + + String dest = Util.getProtocolName(protocol, version, true) + + " " + daddr + (dport > 0 ? "/" + dport : "") + (count > 1 ? " ?" + count : ""); + SpannableString span = new SpannableString(dest); + span.setSpan(new UnderlineSpan(), 0, dest.length(), 0); + tvDest.setText(span); + + if (Util.isNumericAddress(daddr)) + new AsyncTask() { + @Override + protected void onPreExecute() { + ViewCompat.setHasTransientState(tvDest, true); + } + + @Override + protected String doInBackground(String... args) { + try { + return InetAddress.getByName(args[0]).getHostName(); + } catch (UnknownHostException ignored) { + return args[0]; + } + } + + @Override + protected void onPostExecute(String addr) { + tvDest.setText( + Util.getProtocolName(protocol, version, true) + + " >" + addr + (dport > 0 ? "/" + dport : "")); + ViewCompat.setHasTransientState(tvDest, false); + } + }.execute(daddr); + + if (allowed < 0) + tvDest.setTextColor(colorText); + else if (allowed > 0) + tvDest.setTextColor(colorOn); + else + tvDest.setTextColor(colorOff); + + llTraffic.setVisibility(connections > 0 || sent > 0 || received > 0 ? View.VISIBLE : View.GONE); + if (connections > 0) + tvConnections.setText(context.getString(R.string.msg_count, connections)); + + if (sent > 1024 * 1204 * 1024L || received > 1024 * 1024 * 1024L) + tvTraffic.setText(context.getString(R.string.msg_gb, + (sent > 0 ? sent / (1024 * 1024 * 1024f) : 0), + (received > 0 ? received / (1024 * 1024 * 1024f) : 0))); + else if (sent > 1204 * 1024L || received > 1024 * 1024L) + tvTraffic.setText(context.getString(R.string.msg_mb, + (sent > 0 ? sent / (1024 * 1024f) : 0), + (received > 0 ? received / (1024 * 1024f) : 0))); + else + tvTraffic.setText(context.getString(R.string.msg_kb, + (sent > 0 ? sent / 1024f : 0), + (received > 0 ? received / 1024f : 0))); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterDns.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterDns.java new file mode 100644 index 0000000..6ddd7e6 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterDns.java @@ -0,0 +1,95 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.TextView; + +import androidx.preference.PreferenceManager; + +import java.text.SimpleDateFormat; +import java.util.Date; + +public class AdapterDns extends CursorAdapter { + private int colorExpired; + + private int colTime; + private int colQName; + private int colAName; + private int colResource; + private int colTTL; + + public AdapterDns(Context context, Cursor cursor) { + super(context, cursor, 0); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + if (prefs.getBoolean("dark_theme", false)) + colorExpired = Color.argb(128, Color.red(Color.DKGRAY), Color.green(Color.DKGRAY), Color.blue(Color.DKGRAY)); + else + colorExpired = Color.argb(128, Color.red(Color.LTGRAY), Color.green(Color.LTGRAY), Color.blue(Color.LTGRAY)); + + colTime = cursor.getColumnIndex("time"); + colQName = cursor.getColumnIndex("qname"); + colAName = cursor.getColumnIndex("aname"); + colResource = cursor.getColumnIndex("resource"); + colTTL = cursor.getColumnIndex("ttl"); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return LayoutInflater.from(context).inflate(R.layout.dns, parent, false); + } + + @Override + public void bindView(final View view, final Context context, final Cursor cursor) { + // Get values + long time = cursor.getLong(colTime); + String qname = cursor.getString(colQName); + String aname = cursor.getString(colAName); + String resource = cursor.getString(colResource); + int ttl = cursor.getInt(colTTL); + + long now = new Date().getTime(); + boolean expired = (time + ttl < now); + view.setBackgroundColor(expired ? colorExpired : Color.TRANSPARENT); + + // Get views + TextView tvTime = view.findViewById(R.id.tvTime); + TextView tvQName = view.findViewById(R.id.tvQName); + TextView tvAName = view.findViewById(R.id.tvAName); + TextView tvResource = view.findViewById(R.id.tvResource); + TextView tvTTL = view.findViewById(R.id.tvTTL); + + // Set values + tvTime.setText(new SimpleDateFormat("dd HH:mm").format(time)); + tvQName.setText(qname); + tvAName.setText(aname); + tvResource.setText(resource); + tvTTL.setText("+" + Integer.toString(ttl / 1000)); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterForwarding.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterForwarding.java new file mode 100644 index 0000000..44ceaa0 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterForwarding.java @@ -0,0 +1,74 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.Context; +import android.database.Cursor; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.TextView; + +public class AdapterForwarding extends CursorAdapter { + private int colProtocol; + private int colDPort; + private int colRAddr; + private int colRPort; + private int colRUid; + + public AdapterForwarding(Context context, Cursor cursor) { + super(context, cursor, 0); + colProtocol = cursor.getColumnIndex("protocol"); + colDPort = cursor.getColumnIndex("dport"); + colRAddr = cursor.getColumnIndex("raddr"); + colRPort = cursor.getColumnIndex("rport"); + colRUid = cursor.getColumnIndex("ruid"); + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return LayoutInflater.from(context).inflate(R.layout.forward, parent, false); + } + + @Override + public void bindView(final View view, final Context context, final Cursor cursor) { + // Get values + int protocol = cursor.getInt(colProtocol); + int dport = cursor.getInt(colDPort); + String raddr = cursor.getString(colRAddr); + int rport = cursor.getInt(colRPort); + int ruid = cursor.getInt(colRUid); + + // Get views + TextView tvProtocol = view.findViewById(R.id.tvProtocol); + TextView tvDPort = view.findViewById(R.id.tvDPort); + TextView tvRAddr = view.findViewById(R.id.tvRAddr); + TextView tvRPort = view.findViewById(R.id.tvRPort); + TextView tvRUid = view.findViewById(R.id.tvRUid); + + tvProtocol.setText(Util.getProtocolName(protocol, 0, false)); + tvDPort.setText(Integer.toString(dport)); + tvRAddr.setText(raddr); + tvRPort.setText(Integer.toString(rport)); + tvRUid.setText(TextUtils.join(", ", Util.getApplicationNames(ruid, context))); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterLog.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterLog.java new file mode 100644 index 0000000..892bf79 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterLog.java @@ -0,0 +1,370 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CursorAdapter; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.view.ViewCompat; +import androidx.preference.PreferenceManager; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.text.SimpleDateFormat; +import java.util.List; + +public class AdapterLog extends CursorAdapter { + private static String TAG = "NetGuard.Log"; + + private boolean resolve; + private boolean organization; + private int colTime; + private int colVersion; + private int colProtocol; + private int colFlags; + private int colSAddr; + private int colSPort; + private int colDAddr; + private int colDPort; + private int colDName; + private int colUid; + private int colData; + private int colAllowed; + private int colConnection; + private int colInteractive; + private int colorOn; + private int colorOff; + private int iconSize; + private InetAddress dns1 = null; + private InetAddress dns2 = null; + private InetAddress vpn4 = null; + private InetAddress vpn6 = null; + + public AdapterLog(Context context, Cursor cursor, boolean resolve, boolean organization) { + super(context, cursor, 0); + this.resolve = resolve; + this.organization = organization; + colTime = cursor.getColumnIndex("time"); + colVersion = cursor.getColumnIndex("version"); + colProtocol = cursor.getColumnIndex("protocol"); + colFlags = cursor.getColumnIndex("flags"); + colSAddr = cursor.getColumnIndex("saddr"); + colSPort = cursor.getColumnIndex("sport"); + colDAddr = cursor.getColumnIndex("daddr"); + colDPort = cursor.getColumnIndex("dport"); + colDName = cursor.getColumnIndex("dname"); + colUid = cursor.getColumnIndex("uid"); + colData = cursor.getColumnIndex("data"); + colAllowed = cursor.getColumnIndex("allowed"); + colConnection = cursor.getColumnIndex("connection"); + colInteractive = cursor.getColumnIndex("interactive"); + + TypedValue tv = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.colorOn, tv, true); + colorOn = tv.data; + context.getTheme().resolveAttribute(R.attr.colorOff, tv, true); + colorOff = tv.data; + + iconSize = Util.dips2pixels(24, context); + + try { + List lstDns = ServiceSinkhole.getDns(context); + dns1 = (lstDns.size() > 0 ? lstDns.get(0) : null); + dns2 = (lstDns.size() > 1 ? lstDns.get(1) : null); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + vpn4 = InetAddress.getByName(prefs.getString("vpn4", "10.1.10.1")); + vpn6 = InetAddress.getByName(prefs.getString("vpn6", "fd00:1:fd00:1:fd00:1:fd00:1")); + } catch (UnknownHostException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + + public void setResolve(boolean resolve) { + this.resolve = resolve; + } + + public void setOrganization(boolean organization) { + this.organization = organization; + } + + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return LayoutInflater.from(context).inflate(R.layout.log, parent, false); + } + + @Override + public void bindView(final View view, final Context context, final Cursor cursor) { + // Get values + long time = cursor.getLong(colTime); + int version = (cursor.isNull(colVersion) ? -1 : cursor.getInt(colVersion)); + int protocol = (cursor.isNull(colProtocol) ? -1 : cursor.getInt(colProtocol)); + String flags = cursor.getString(colFlags); + String saddr = cursor.getString(colSAddr); + int sport = (cursor.isNull(colSPort) ? -1 : cursor.getInt(colSPort)); + String daddr = cursor.getString(colDAddr); + int dport = (cursor.isNull(colDPort) ? -1 : cursor.getInt(colDPort)); + String dname = (cursor.isNull(colDName) ? null : cursor.getString(colDName)); + int uid = (cursor.isNull(colUid) ? -1 : cursor.getInt(colUid)); + String data = cursor.getString(colData); + int allowed = (cursor.isNull(colAllowed) ? -1 : cursor.getInt(colAllowed)); + int connection = (cursor.isNull(colConnection) ? -1 : cursor.getInt(colConnection)); + int interactive = (cursor.isNull(colInteractive) ? -1 : cursor.getInt(colInteractive)); + + // Get views + TextView tvTime = view.findViewById(R.id.tvTime); + TextView tvProtocol = view.findViewById(R.id.tvProtocol); + TextView tvFlags = view.findViewById(R.id.tvFlags); + TextView tvSAddr = view.findViewById(R.id.tvSAddr); + TextView tvSPort = view.findViewById(R.id.tvSPort); + final TextView tvDaddr = view.findViewById(R.id.tvDAddr); + TextView tvDPort = view.findViewById(R.id.tvDPort); + final TextView tvOrganization = view.findViewById(R.id.tvOrganization); + final ImageView ivIcon = view.findViewById(R.id.ivIcon); + TextView tvUid = view.findViewById(R.id.tvUid); + TextView tvData = view.findViewById(R.id.tvData); + ImageView ivConnection = view.findViewById(R.id.ivConnection); + ImageView ivInteractive = view.findViewById(R.id.ivInteractive); + + // Show time + tvTime.setText(new SimpleDateFormat("HH:mm:ss").format(time)); + + // Show connection type + if (connection <= 0) + ivConnection.setImageResource(allowed > 0 ? R.drawable.host_allowed : R.drawable.host_blocked); + else { + if (allowed > 0) + ivConnection.setImageResource(connection == 1 ? R.drawable.wifi_on : R.drawable.other_on); + else + ivConnection.setImageResource(connection == 1 ? R.drawable.wifi_off : R.drawable.other_off); + } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Drawable wrap = DrawableCompat.wrap(ivConnection.getDrawable()); + DrawableCompat.setTint(wrap, allowed > 0 ? colorOn : colorOff); + } + + // Show if screen on + if (interactive <= 0) + ivInteractive.setImageDrawable(null); + else { + ivInteractive.setImageResource(R.drawable.screen_on); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Drawable wrap = DrawableCompat.wrap(ivInteractive.getDrawable()); + DrawableCompat.setTint(wrap, colorOn); + } + } + + // Show protocol name + tvProtocol.setText(Util.getProtocolName(protocol, version, false)); + + // SHow TCP flags + tvFlags.setText(flags); + tvFlags.setVisibility(TextUtils.isEmpty(flags) ? View.GONE : View.VISIBLE); + + // Show source and destination port + if (protocol == 6 || protocol == 17) { + tvSPort.setText(sport < 0 ? "" : getKnownPort(sport)); + tvDPort.setText(dport < 0 ? "" : getKnownPort(dport)); + } else { + tvSPort.setText(sport < 0 ? "" : Integer.toString(sport)); + tvDPort.setText(dport < 0 ? "" : Integer.toString(dport)); + } + + // Application icon + ApplicationInfo info = null; + PackageManager pm = context.getPackageManager(); + String[] pkg = pm.getPackagesForUid(uid); + if (pkg != null && pkg.length > 0) + try { + info = pm.getApplicationInfo(pkg[0], 0); + } catch (PackageManager.NameNotFoundException ignored) { + } + + if (info == null) + ivIcon.setImageDrawable(null); + else { + if (info.icon <= 0) + ivIcon.setImageResource(android.R.drawable.sym_def_app_icon); + else { + Uri uri = Uri.parse("android.resource://" + info.packageName + "/" + info.icon); + GlideApp.with(context) + .load(uri) + //.diskCacheStrategy(DiskCacheStrategy.NONE) + //.skipMemoryCache(true) + .override(iconSize, iconSize) + .into(ivIcon); + } + } + + boolean we = (android.os.Process.myUid() == uid); + + // https://android.googlesource.com/platform/system/core/+/master/include/private/android_filesystem_config.h + uid = uid % 100000; // strip off user ID + if (uid == -1) + tvUid.setText(""); + else if (uid == 0) + tvUid.setText(context.getString(R.string.title_root)); + else if (uid == 9999) + tvUid.setText("-"); // nobody + else + tvUid.setText(Integer.toString(uid)); + + // Show source address + tvSAddr.setText(getKnownAddress(saddr)); + + // Show destination address + if (!we && resolve && !isKnownAddress(daddr)) + if (dname == null) { + tvDaddr.setText(daddr); + new AsyncTask() { + @Override + protected void onPreExecute() { + ViewCompat.setHasTransientState(tvDaddr, true); + } + + @Override + protected String doInBackground(String... args) { + try { + return InetAddress.getByName(args[0]).getHostName(); + } catch (UnknownHostException ignored) { + return args[0]; + } + } + + @Override + protected void onPostExecute(String name) { + tvDaddr.setText(">" + name); + ViewCompat.setHasTransientState(tvDaddr, false); + } + }.execute(daddr); + } else + tvDaddr.setText(dname); + else + tvDaddr.setText(getKnownAddress(daddr)); + + // Show organization + tvOrganization.setVisibility(View.GONE); + if (!we && organization) { + if (!isKnownAddress(daddr)) + new AsyncTask() { + @Override + protected void onPreExecute() { + ViewCompat.setHasTransientState(tvOrganization, true); + } + + @Override + protected String doInBackground(String... args) { + try { + return Util.getOrganization(args[0]); + } catch (Throwable ex) { + Log.w(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + return null; + } + } + + @Override + protected void onPostExecute(String organization) { + if (organization != null) { + tvOrganization.setText(organization); + tvOrganization.setVisibility(View.VISIBLE); + } + ViewCompat.setHasTransientState(tvOrganization, false); + } + }.execute(daddr); + } + + // Show extra data + if (TextUtils.isEmpty(data)) { + tvData.setText(""); + tvData.setVisibility(View.GONE); + } else { + tvData.setText(data); + tvData.setVisibility(View.VISIBLE); + } + } + + public boolean isKnownAddress(String addr) { + try { + InetAddress a = InetAddress.getByName(addr); + if (a.equals(dns1) || a.equals(dns2) || a.equals(vpn4) || a.equals(vpn6)) + return true; + } catch (UnknownHostException ignored) { + } + return false; + } + + private String getKnownAddress(String addr) { + try { + InetAddress a = InetAddress.getByName(addr); + if (a.equals(dns1)) + return "dns1"; + if (a.equals(dns2)) + return "dns2"; + if (a.equals(vpn4) || a.equals(vpn6)) + return "vpn"; + } catch (UnknownHostException ignored) { + } + return addr; + } + + private String getKnownPort(int port) { + // https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers#Well-known_ports + switch (port) { + case 7: + return "echo"; + case 25: + return "smtp"; + case 53: + return "dns"; + case 80: + return "http"; + case 110: + return "pop3"; + case 143: + return "imap"; + case 443: + return "https"; + case 465: + return "smtps"; + case 993: + return "imaps"; + case 995: + return "pop3s"; + default: + return Integer.toString(port); + } + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterRule.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterRule.java new file mode 100644 index 0000000..81b1dd3 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterRule.java @@ -0,0 +1,1033 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.annotation.TargetApi; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.TypedArray; +import android.database.Cursor; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ImageSpan; +import android.util.Log; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.SubMenu; +import android.view.TouchDelegate; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.CursorAdapter; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.PopupMenu; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.DrawableCompat; +import androidx.core.widget.CompoundButtonCompat; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.load.DecodeFormat; +import com.bumptech.glide.request.RequestOptions; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class AdapterRule extends RecyclerView.Adapter implements Filterable { + private static final String TAG = "NetGuard.Adapter"; + + private View anchor; + private LayoutInflater inflater; + private RecyclerView rv; + private int colorText; + private int colorChanged; + private int colorOn; + private int colorOff; + private int colorGrayed; + private int iconSize; + private boolean wifiActive = true; + private boolean otherActive = true; + private boolean live = true; + private List listAll = new ArrayList<>(); + private List listFiltered = new ArrayList<>(); + + private List messaging = Arrays.asList( + "com.discord", + "com.facebook.mlite", + "com.facebook.orca", + "com.instagram.android", + "com.Slack", + "com.skype.raider", + "com.snapchat.android", + "com.whatsapp", + "com.whatsapp.w4b" + ); + + private List download = Arrays.asList( + "com.google.android.youtube" + ); + + public static class ViewHolder extends RecyclerView.ViewHolder { + public View view; + + public LinearLayout llApplication; + public ImageView ivIcon; + public ImageView ivExpander; + public TextView tvName; + + public TextView tvHosts; + + public RelativeLayout rlLockdown; + public ImageView ivLockdown; + + public CheckBox cbWifi; + public ImageView ivScreenWifi; + + public CheckBox cbOther; + public ImageView ivScreenOther; + public TextView tvRoaming; + + public TextView tvRemarkMessaging; + public TextView tvRemarkDownload; + + public LinearLayout llConfiguration; + public TextView tvUid; + public TextView tvPackage; + public TextView tvVersion; + public TextView tvInternet; + public TextView tvDisabled; + + public Button btnRelated; + public ImageButton ibSettings; + public ImageButton ibLaunch; + + public CheckBox cbApply; + + public LinearLayout llScreenWifi; + public ImageView ivWifiLegend; + public CheckBox cbScreenWifi; + + public LinearLayout llScreenOther; + public ImageView ivOtherLegend; + public CheckBox cbScreenOther; + + public CheckBox cbRoaming; + + public CheckBox cbLockdown; + public ImageView ivLockdownLegend; + + public ImageButton btnClear; + + public LinearLayout llFilter; + public ImageView ivLive; + public TextView tvLogging; + public Button btnLogging; + public ListView lvAccess; + public ImageButton btnClearAccess; + public CheckBox cbNotify; + + public ViewHolder(View itemView) { + super(itemView); + view = itemView; + + llApplication = itemView.findViewById(R.id.llApplication); + ivIcon = itemView.findViewById(R.id.ivIcon); + ivExpander = itemView.findViewById(R.id.ivExpander); + tvName = itemView.findViewById(R.id.tvName); + + tvHosts = itemView.findViewById(R.id.tvHosts); + + rlLockdown = itemView.findViewById(R.id.rlLockdown); + ivLockdown = itemView.findViewById(R.id.ivLockdown); + + cbWifi = itemView.findViewById(R.id.cbWifi); + ivScreenWifi = itemView.findViewById(R.id.ivScreenWifi); + + cbOther = itemView.findViewById(R.id.cbOther); + ivScreenOther = itemView.findViewById(R.id.ivScreenOther); + tvRoaming = itemView.findViewById(R.id.tvRoaming); + + tvRemarkMessaging = itemView.findViewById(R.id.tvRemarkMessaging); + tvRemarkDownload = itemView.findViewById(R.id.tvRemarkDownload); + + llConfiguration = itemView.findViewById(R.id.llConfiguration); + tvUid = itemView.findViewById(R.id.tvUid); + tvPackage = itemView.findViewById(R.id.tvPackage); + tvVersion = itemView.findViewById(R.id.tvVersion); + tvInternet = itemView.findViewById(R.id.tvInternet); + tvDisabled = itemView.findViewById(R.id.tvDisabled); + + btnRelated = itemView.findViewById(R.id.btnRelated); + ibSettings = itemView.findViewById(R.id.ibSettings); + ibLaunch = itemView.findViewById(R.id.ibLaunch); + + cbApply = itemView.findViewById(R.id.cbApply); + + llScreenWifi = itemView.findViewById(R.id.llScreenWifi); + ivWifiLegend = itemView.findViewById(R.id.ivWifiLegend); + cbScreenWifi = itemView.findViewById(R.id.cbScreenWifi); + + llScreenOther = itemView.findViewById(R.id.llScreenOther); + ivOtherLegend = itemView.findViewById(R.id.ivOtherLegend); + cbScreenOther = itemView.findViewById(R.id.cbScreenOther); + + cbRoaming = itemView.findViewById(R.id.cbRoaming); + + cbLockdown = itemView.findViewById(R.id.cbLockdown); + ivLockdownLegend = itemView.findViewById(R.id.ivLockdownLegend); + + btnClear = itemView.findViewById(R.id.btnClear); + + llFilter = itemView.findViewById(R.id.llFilter); + ivLive = itemView.findViewById(R.id.ivLive); + tvLogging = itemView.findViewById(R.id.tvLogging); + btnLogging = itemView.findViewById(R.id.btnLogging); + lvAccess = itemView.findViewById(R.id.lvAccess); + btnClearAccess = itemView.findViewById(R.id.btnClearAccess); + cbNotify = itemView.findViewById(R.id.cbNotify); + + final View wifiParent = (View) cbWifi.getParent(); + wifiParent.post(new Runnable() { + public void run() { + Rect rect = new Rect(); + cbWifi.getHitRect(rect); + rect.bottom += rect.top; + rect.right += rect.left; + rect.top = 0; + rect.left = 0; + wifiParent.setTouchDelegate(new TouchDelegate(rect, cbWifi)); + } + }); + + final View otherParent = (View) cbOther.getParent(); + otherParent.post(new Runnable() { + public void run() { + Rect rect = new Rect(); + cbOther.getHitRect(rect); + rect.bottom += rect.top; + rect.right += rect.left; + rect.top = 0; + rect.left = 0; + otherParent.setTouchDelegate(new TouchDelegate(rect, cbOther)); + } + }); + } + } + + public AdapterRule(Context context, View anchor) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + this.anchor = anchor; + this.inflater = LayoutInflater.from(context); + + if (prefs.getBoolean("dark_theme", false)) + colorChanged = Color.argb(128, Color.red(Color.DKGRAY), Color.green(Color.DKGRAY), Color.blue(Color.DKGRAY)); + else + colorChanged = Color.argb(128, Color.red(Color.LTGRAY), Color.green(Color.LTGRAY), Color.blue(Color.LTGRAY)); + + TypedArray ta = context.getTheme().obtainStyledAttributes(new int[]{android.R.attr.textColorPrimary}); + try { + colorText = ta.getColor(0, 0); + } finally { + ta.recycle(); + } + + TypedValue tv = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.colorOn, tv, true); + colorOn = tv.data; + context.getTheme().resolveAttribute(R.attr.colorOff, tv, true); + colorOff = tv.data; + + colorGrayed = ContextCompat.getColor(context, R.color.colorGrayed); + + TypedValue typedValue = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.listPreferredItemHeight, typedValue, true); + int height = TypedValue.complexToDimensionPixelSize(typedValue.data, context.getResources().getDisplayMetrics()); + this.iconSize = Math.round(height * context.getResources().getDisplayMetrics().density + 0.5f); + + setHasStableIds(true); + } + + public void set(List listRule) { + listAll = listRule; + listFiltered = new ArrayList<>(); + listFiltered.addAll(listRule); + notifyDataSetChanged(); + } + + public void setWifiActive() { + wifiActive = true; + otherActive = false; + notifyDataSetChanged(); + } + + public void setMobileActive() { + wifiActive = false; + otherActive = true; + notifyDataSetChanged(); + } + + public void setDisconnected() { + wifiActive = false; + otherActive = false; + notifyDataSetChanged(); + } + + public boolean isLive() { + return this.live; + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + rv = recyclerView; + } + + @Override + public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + super.onDetachedFromRecyclerView(recyclerView); + rv = null; + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + final Context context = holder.itemView.getContext(); + + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final boolean log_app = prefs.getBoolean("log_app", false); + final boolean filter = prefs.getBoolean("filter", false); + final boolean notify_access = prefs.getBoolean("notify_access", false); + + // Get rule + final Rule rule = listFiltered.get(position); + + // Handle expanding/collapsing + holder.llApplication.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + rule.expanded = !rule.expanded; + notifyItemChanged(holder.getAdapterPosition()); + } + }); + + // Show if non default rules + holder.itemView.setBackgroundColor(rule.changed ? colorChanged : Color.TRANSPARENT); + + // Show expand/collapse indicator + holder.ivExpander.setImageLevel(rule.expanded ? 1 : 0); + + // Show application icon + if (rule.icon <= 0) + holder.ivIcon.setImageResource(android.R.drawable.sym_def_app_icon); + else { + Uri uri = Uri.parse("android.resource://" + rule.packageName + "/" + rule.icon); + GlideApp.with(holder.itemView.getContext()) + .applyDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565)) + .load(uri) + //.diskCacheStrategy(DiskCacheStrategy.NONE) + //.skipMemoryCache(true) + .override(iconSize, iconSize) + .into(holder.ivIcon); + } + + // Show application label + holder.tvName.setText(rule.name); + + // Show application state + int color = rule.system ? colorOff : colorText; + if (!rule.internet || !rule.enabled) + color = Color.argb(128, Color.red(color), Color.green(color), Color.blue(color)); + holder.tvName.setTextColor(color); + + holder.tvHosts.setVisibility(rule.hosts > 0 ? View.VISIBLE : View.GONE); + holder.tvHosts.setText(Long.toString(rule.hosts)); + + // Lockdown settings + boolean lockdown = prefs.getBoolean("lockdown", false); + boolean lockdown_wifi = prefs.getBoolean("lockdown_wifi", true); + boolean lockdown_other = prefs.getBoolean("lockdown_other", true); + if ((otherActive && !lockdown_other) || (wifiActive && !lockdown_wifi)) + lockdown = false; + + holder.rlLockdown.setVisibility(lockdown && !rule.lockdown ? View.VISIBLE : View.GONE); + holder.ivLockdown.setEnabled(rule.apply); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Drawable wrap = DrawableCompat.wrap(holder.ivLockdown.getDrawable()); + DrawableCompat.setTint(wrap, rule.apply ? colorOff : colorGrayed); + } + + boolean screen_on = prefs.getBoolean("screen_on", true); + + // Wi-Fi settings + holder.cbWifi.setEnabled(rule.apply); + holder.cbWifi.setAlpha(wifiActive ? 1 : 0.5f); + holder.cbWifi.setOnCheckedChangeListener(null); + holder.cbWifi.setChecked(rule.wifi_blocked); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Drawable wrap = DrawableCompat.wrap(CompoundButtonCompat.getButtonDrawable(holder.cbWifi)); + DrawableCompat.setTint(wrap, rule.apply ? (rule.wifi_blocked ? colorOff : colorOn) : colorGrayed); + } + holder.cbWifi.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + rule.wifi_blocked = isChecked; + updateRule(context, rule, true, listAll); + } + }); + + holder.ivScreenWifi.setEnabled(rule.apply); + holder.ivScreenWifi.setAlpha(wifiActive ? 1 : 0.5f); + holder.ivScreenWifi.setVisibility(rule.screen_wifi && rule.wifi_blocked ? View.VISIBLE : View.INVISIBLE); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Drawable wrap = DrawableCompat.wrap(holder.ivScreenWifi.getDrawable()); + DrawableCompat.setTint(wrap, rule.apply ? colorOn : colorGrayed); + } + + // Mobile settings + holder.cbOther.setEnabled(rule.apply); + holder.cbOther.setAlpha(otherActive ? 1 : 0.5f); + holder.cbOther.setOnCheckedChangeListener(null); + holder.cbOther.setChecked(rule.other_blocked); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Drawable wrap = DrawableCompat.wrap(CompoundButtonCompat.getButtonDrawable(holder.cbOther)); + DrawableCompat.setTint(wrap, rule.apply ? (rule.other_blocked ? colorOff : colorOn) : colorGrayed); + } + holder.cbOther.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + rule.other_blocked = isChecked; + updateRule(context, rule, true, listAll); + } + }); + + holder.ivScreenOther.setEnabled(rule.apply); + holder.ivScreenOther.setAlpha(otherActive ? 1 : 0.5f); + holder.ivScreenOther.setVisibility(rule.screen_other && rule.other_blocked ? View.VISIBLE : View.INVISIBLE); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Drawable wrap = DrawableCompat.wrap(holder.ivScreenOther.getDrawable()); + DrawableCompat.setTint(wrap, rule.apply ? colorOn : colorGrayed); + } + + holder.tvRoaming.setTextColor(rule.apply ? colorOff : colorGrayed); + holder.tvRoaming.setAlpha(otherActive ? 1 : 0.5f); + holder.tvRoaming.setVisibility(rule.roaming && (!rule.other_blocked || rule.screen_other) ? View.VISIBLE : View.INVISIBLE); + + holder.tvRemarkMessaging.setVisibility(messaging.contains(rule.packageName) ? View.VISIBLE : View.GONE); + holder.tvRemarkDownload.setVisibility(download.contains(rule.packageName) ? View.VISIBLE : View.GONE); + + // Expanded configuration section + holder.llConfiguration.setVisibility(rule.expanded ? View.VISIBLE : View.GONE); + + // Show application details + holder.tvUid.setText(Integer.toString(rule.uid)); + holder.tvPackage.setText(rule.packageName); + holder.tvVersion.setText(rule.version); + + // Show application state + holder.tvInternet.setVisibility(rule.internet ? View.GONE : View.VISIBLE); + holder.tvDisabled.setVisibility(rule.enabled ? View.GONE : View.VISIBLE); + + // Show related + holder.btnRelated.setVisibility(rule.relateduids ? View.VISIBLE : View.GONE); + holder.btnRelated.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Intent main = new Intent(context, ActivityMain.class); + main.putExtra(ActivityMain.EXTRA_SEARCH, Integer.toString(rule.uid)); + main.putExtra(ActivityMain.EXTRA_RELATED, true); + context.startActivity(main); + } + }); + + // Launch application settings + if (rule.expanded) { + Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.parse("package:" + rule.packageName)); + final Intent settings = (intent.resolveActivity(context.getPackageManager()) == null ? null : intent); + + holder.ibSettings.setVisibility(settings == null ? View.GONE : View.VISIBLE); + holder.ibSettings.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + context.startActivity(settings); + } + }); + } else + holder.ibSettings.setVisibility(View.GONE); + + // Launch application + if (rule.expanded) { + Intent intent = context.getPackageManager().getLaunchIntentForPackage(rule.packageName); + final Intent launch = (intent == null || + intent.resolveActivity(context.getPackageManager()) == null ? null : intent); + + holder.ibLaunch.setVisibility(launch == null ? View.GONE : View.VISIBLE); + holder.ibLaunch.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + context.startActivity(launch); + } + }); + } else + holder.ibLaunch.setVisibility(View.GONE); + + // Apply + holder.cbApply.setEnabled(rule.pkg && filter); + holder.cbApply.setOnCheckedChangeListener(null); + holder.cbApply.setChecked(rule.apply); + holder.cbApply.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + rule.apply = isChecked; + updateRule(context, rule, true, listAll); + } + }); + + // Show Wi-Fi screen on condition + holder.llScreenWifi.setVisibility(screen_on ? View.VISIBLE : View.GONE); + holder.cbScreenWifi.setEnabled(rule.wifi_blocked && rule.apply); + holder.cbScreenWifi.setOnCheckedChangeListener(null); + holder.cbScreenWifi.setChecked(rule.screen_wifi); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Drawable wrap = DrawableCompat.wrap(holder.ivWifiLegend.getDrawable()); + DrawableCompat.setTint(wrap, colorOn); + } + + holder.cbScreenWifi.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + rule.screen_wifi = isChecked; + updateRule(context, rule, true, listAll); + } + }); + + // Show mobile screen on condition + holder.llScreenOther.setVisibility(screen_on ? View.VISIBLE : View.GONE); + holder.cbScreenOther.setEnabled(rule.other_blocked && rule.apply); + holder.cbScreenOther.setOnCheckedChangeListener(null); + holder.cbScreenOther.setChecked(rule.screen_other); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Drawable wrap = DrawableCompat.wrap(holder.ivOtherLegend.getDrawable()); + DrawableCompat.setTint(wrap, colorOn); + } + + holder.cbScreenOther.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + rule.screen_other = isChecked; + updateRule(context, rule, true, listAll); + } + }); + + // Show roaming condition + holder.cbRoaming.setEnabled((!rule.other_blocked || rule.screen_other) && rule.apply); + holder.cbRoaming.setOnCheckedChangeListener(null); + holder.cbRoaming.setChecked(rule.roaming); + holder.cbRoaming.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + @TargetApi(Build.VERSION_CODES.M) + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + rule.roaming = isChecked; + updateRule(context, rule, true, listAll); + } + }); + + // Show lockdown + holder.cbLockdown.setEnabled(rule.apply); + holder.cbLockdown.setOnCheckedChangeListener(null); + holder.cbLockdown.setChecked(rule.lockdown); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + Drawable wrap = DrawableCompat.wrap(holder.ivLockdownLegend.getDrawable()); + DrawableCompat.setTint(wrap, colorOn); + } + + holder.cbLockdown.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + @TargetApi(Build.VERSION_CODES.M) + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + rule.lockdown = isChecked; + updateRule(context, rule, true, listAll); + } + }); + + // Reset rule + holder.btnClear.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Util.areYouSure(view.getContext(), R.string.msg_clear_rules, new Util.DoubtListener() { + @Override + public void onSure() { + holder.cbApply.setChecked(true); + holder.cbWifi.setChecked(rule.wifi_default); + holder.cbOther.setChecked(rule.other_default); + holder.cbScreenWifi.setChecked(rule.screen_wifi_default); + holder.cbScreenOther.setChecked(rule.screen_other_default); + holder.cbRoaming.setChecked(rule.roaming_default); + holder.cbLockdown.setChecked(false); + } + }); + } + }); + + holder.llFilter.setVisibility(Util.canFilter(context) ? View.VISIBLE : View.GONE); + + // Live + holder.ivLive.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + live = !live; + TypedValue tv = new TypedValue(); + view.getContext().getTheme().resolveAttribute(live ? R.attr.iconPause : R.attr.iconPlay, tv, true); + holder.ivLive.setImageResource(tv.resourceId); + if (live) + AdapterRule.this.notifyDataSetChanged(); + } + }); + + // Show logging/filtering is disabled + holder.tvLogging.setText(log_app && filter ? R.string.title_logging_enabled : R.string.title_logging_disabled); + holder.btnLogging.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + LayoutInflater inflater = LayoutInflater.from(context); + View view = inflater.inflate(R.layout.enable, null, false); + + final CheckBox cbLogging = view.findViewById(R.id.cbLogging); + final CheckBox cbFiltering = view.findViewById(R.id.cbFiltering); + final CheckBox cbNotify = view.findViewById(R.id.cbNotify); + TextView tvFilter4 = view.findViewById(R.id.tvFilter4); + + cbLogging.setChecked(log_app); + cbFiltering.setChecked(filter); + cbFiltering.setEnabled(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP); + tvFilter4.setVisibility(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ? View.GONE : View.VISIBLE); + cbNotify.setChecked(notify_access); + cbNotify.setEnabled(log_app); + + cbLogging.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { + prefs.edit().putBoolean("log_app", checked).apply(); + cbNotify.setEnabled(checked); + if (!checked) { + cbNotify.setChecked(false); + prefs.edit().putBoolean("notify_access", false).apply(); + } + ServiceSinkhole.reload("changed notify", context, false); + AdapterRule.this.notifyDataSetChanged(); + } + }); + + cbFiltering.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { + if (checked) + cbLogging.setChecked(true); + prefs.edit().putBoolean("filter", checked).apply(); + ServiceSinkhole.reload("changed filter", context, false); + AdapterRule.this.notifyDataSetChanged(); + } + }); + + cbNotify.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { + prefs.edit().putBoolean("notify_access", checked).apply(); + ServiceSinkhole.reload("changed notify", context, false); + AdapterRule.this.notifyDataSetChanged(); + } + }); + + AlertDialog dialog = new AlertDialog.Builder(context) + .setView(view) + .setCancelable(true) + .create(); + dialog.show(); + } + }); + + // Show access rules + if (rule.expanded) { + // Access the database when expanded only + final AdapterAccess badapter = new AdapterAccess(context, + DatabaseHelper.getInstance(context).getAccess(rule.uid)); + holder.lvAccess.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, final int bposition, long bid) { + PackageManager pm = context.getPackageManager(); + Cursor cursor = (Cursor) badapter.getItem(bposition); + final long id = cursor.getLong(cursor.getColumnIndex("ID")); + final int version = cursor.getInt(cursor.getColumnIndex("version")); + final int protocol = cursor.getInt(cursor.getColumnIndex("protocol")); + final String daddr = cursor.getString(cursor.getColumnIndex("daddr")); + final int dport = cursor.getInt(cursor.getColumnIndex("dport")); + long time = cursor.getLong(cursor.getColumnIndex("time")); + int block = cursor.getInt(cursor.getColumnIndex("block")); + + PopupMenu popup = new PopupMenu(context, anchor); + popup.inflate(R.menu.access); + + popup.getMenu().findItem(R.id.menu_host).setTitle( + Util.getProtocolName(protocol, version, false) + " " + + daddr + (dport > 0 ? "/" + dport : "")); + + SubMenu sub = popup.getMenu().findItem(R.id.menu_host).getSubMenu(); + boolean multiple = false; + Cursor alt = null; + try { + alt = DatabaseHelper.getInstance(context).getAlternateQNames(daddr); + while (alt.moveToNext()) { + multiple = true; + sub.add(Menu.NONE, Menu.NONE, 0, alt.getString(0)).setEnabled(false); + } + } finally { + if (alt != null) + alt.close(); + } + popup.getMenu().findItem(R.id.menu_host).setEnabled(multiple); + + markPro(context, popup.getMenu().findItem(R.id.menu_allow), ActivityPro.SKU_FILTER); + markPro(context, popup.getMenu().findItem(R.id.menu_block), ActivityPro.SKU_FILTER); + + // Whois + final Intent lookupIP = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.dnslytics.com/whois-lookup/" + daddr)); + if (pm.resolveActivity(lookupIP, 0) == null) + popup.getMenu().removeItem(R.id.menu_whois); + else + popup.getMenu().findItem(R.id.menu_whois).setTitle(context.getString(R.string.title_log_whois, daddr)); + + // Lookup port + final Intent lookupPort = new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.speedguide.net/port.php?port=" + dport)); + if (dport <= 0 || pm.resolveActivity(lookupPort, 0) == null) + popup.getMenu().removeItem(R.id.menu_port); + else + popup.getMenu().findItem(R.id.menu_port).setTitle(context.getString(R.string.title_log_port, dport)); + + popup.getMenu().findItem(R.id.menu_time).setTitle( + SimpleDateFormat.getDateTimeInstance().format(time)); + + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + int menu = menuItem.getItemId(); + boolean result = false; + switch (menu) { + case R.id.menu_whois: + context.startActivity(lookupIP); + result = true; + break; + + case R.id.menu_port: + context.startActivity(lookupPort); + result = true; + break; + + case R.id.menu_allow: + if (IAB.isPurchased(ActivityPro.SKU_FILTER, context)) { + DatabaseHelper.getInstance(context).setAccess(id, 0); + ServiceSinkhole.reload("allow host", context, false); + } else + context.startActivity(new Intent(context, ActivityPro.class)); + result = true; + break; + + case R.id.menu_block: + if (IAB.isPurchased(ActivityPro.SKU_FILTER, context)) { + DatabaseHelper.getInstance(context).setAccess(id, 1); + ServiceSinkhole.reload("block host", context, false); + } else + context.startActivity(new Intent(context, ActivityPro.class)); + result = true; + break; + + case R.id.menu_reset: + DatabaseHelper.getInstance(context).setAccess(id, -1); + ServiceSinkhole.reload("reset host", context, false); + result = true; + break; + + case R.id.menu_copy: + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText("netguard", daddr); + clipboard.setPrimaryClip(clip); + return true; + } + + if (menu == R.id.menu_allow || menu == R.id.menu_block || menu == R.id.menu_reset) + new AsyncTask() { + @Override + protected Long doInBackground(Object... objects) { + return DatabaseHelper.getInstance(context).getHostCount(rule.uid, false); + } + + @Override + protected void onPostExecute(Long hosts) { + rule.hosts = hosts; + notifyDataSetChanged(); + } + }.execute(); + + return result; + } + }); + + if (block == 0) + popup.getMenu().removeItem(R.id.menu_allow); + else if (block == 1) + popup.getMenu().removeItem(R.id.menu_block); + + popup.show(); + } + }); + + holder.lvAccess.setAdapter(badapter); + } else { + holder.lvAccess.setAdapter(null); + holder.lvAccess.setOnItemClickListener(null); + } + + // Clear access log + holder.btnClearAccess.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + Util.areYouSure(view.getContext(), R.string.msg_reset_access, new Util.DoubtListener() { + @Override + public void onSure() { + DatabaseHelper.getInstance(context).clearAccess(rule.uid, true); + if (!live) + notifyDataSetChanged(); + if (rv != null) + rv.scrollToPosition(holder.getAdapterPosition()); + } + }); + } + }); + + // Notify on access + holder.cbNotify.setEnabled(prefs.getBoolean("notify_access", false) && rule.apply); + holder.cbNotify.setOnCheckedChangeListener(null); + holder.cbNotify.setChecked(rule.notify); + holder.cbNotify.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { + rule.notify = isChecked; + updateRule(context, rule, true, listAll); + } + }); + } + + @Override + public void onViewRecycled(ViewHolder holder) { + super.onViewRecycled(holder); + + //Context context = holder.itemView.getContext(); + //GlideApp.with(context).clear(holder.ivIcon); + + CursorAdapter adapter = (CursorAdapter) holder.lvAccess.getAdapter(); + if (adapter != null) { + Log.i(TAG, "Closing access cursor"); + adapter.changeCursor(null); + holder.lvAccess.setAdapter(null); + } + } + + private void markPro(Context context, MenuItem menu, String sku) { + if (sku == null || !IAB.isPurchased(sku, context)) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean dark = prefs.getBoolean("dark_theme", false); + SpannableStringBuilder ssb = new SpannableStringBuilder(" " + menu.getTitle()); + ssb.setSpan(new ImageSpan(context, dark ? R.drawable.ic_shopping_cart_white_24dp : R.drawable.ic_shopping_cart_black_24dp), 0, 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + menu.setTitle(ssb); + } + } + + private void updateRule(Context context, Rule rule, boolean root, List listAll) { + SharedPreferences wifi = context.getSharedPreferences("wifi", Context.MODE_PRIVATE); + SharedPreferences other = context.getSharedPreferences("other", Context.MODE_PRIVATE); + SharedPreferences apply = context.getSharedPreferences("apply", Context.MODE_PRIVATE); + SharedPreferences screen_wifi = context.getSharedPreferences("screen_wifi", Context.MODE_PRIVATE); + SharedPreferences screen_other = context.getSharedPreferences("screen_other", Context.MODE_PRIVATE); + SharedPreferences roaming = context.getSharedPreferences("roaming", Context.MODE_PRIVATE); + SharedPreferences lockdown = context.getSharedPreferences("lockdown", Context.MODE_PRIVATE); + SharedPreferences notify = context.getSharedPreferences("notify", Context.MODE_PRIVATE); + + if (rule.wifi_blocked == rule.wifi_default) + wifi.edit().remove(rule.packageName).apply(); + else + wifi.edit().putBoolean(rule.packageName, rule.wifi_blocked).apply(); + + if (rule.other_blocked == rule.other_default) + other.edit().remove(rule.packageName).apply(); + else + other.edit().putBoolean(rule.packageName, rule.other_blocked).apply(); + + if (rule.apply) + apply.edit().remove(rule.packageName).apply(); + else + apply.edit().putBoolean(rule.packageName, rule.apply).apply(); + + if (rule.screen_wifi == rule.screen_wifi_default) + screen_wifi.edit().remove(rule.packageName).apply(); + else + screen_wifi.edit().putBoolean(rule.packageName, rule.screen_wifi).apply(); + + if (rule.screen_other == rule.screen_other_default) + screen_other.edit().remove(rule.packageName).apply(); + else + screen_other.edit().putBoolean(rule.packageName, rule.screen_other).apply(); + + if (rule.roaming == rule.roaming_default) + roaming.edit().remove(rule.packageName).apply(); + else + roaming.edit().putBoolean(rule.packageName, rule.roaming).apply(); + + if (rule.lockdown) + lockdown.edit().putBoolean(rule.packageName, rule.lockdown).apply(); + else + lockdown.edit().remove(rule.packageName).apply(); + + if (rule.notify) + notify.edit().remove(rule.packageName).apply(); + else + notify.edit().putBoolean(rule.packageName, rule.notify).apply(); + + rule.updateChanged(context); + Log.i(TAG, "Updated " + rule); + + List listModified = new ArrayList<>(); + for (String pkg : rule.related) { + for (Rule related : listAll) + if (related.packageName.equals(pkg)) { + related.wifi_blocked = rule.wifi_blocked; + related.other_blocked = rule.other_blocked; + related.apply = rule.apply; + related.screen_wifi = rule.screen_wifi; + related.screen_other = rule.screen_other; + related.roaming = rule.roaming; + related.lockdown = rule.lockdown; + related.notify = rule.notify; + listModified.add(related); + } + } + + List listSearch = (root ? new ArrayList<>(listAll) : listAll); + listSearch.remove(rule); + for (Rule modified : listModified) + listSearch.remove(modified); + for (Rule modified : listModified) + updateRule(context, modified, false, listSearch); + + if (root) { + notifyDataSetChanged(); + NotificationManagerCompat.from(context).cancel(rule.uid); + ServiceSinkhole.reload("rule changed", context, false); + } + } + + @Override + public Filter getFilter() { + return new Filter() { + @Override + protected FilterResults performFiltering(CharSequence query) { + List listResult = new ArrayList<>(); + if (query == null) + listResult.addAll(listAll); + else { + query = query.toString().toLowerCase().trim(); + int uid; + try { + uid = Integer.parseInt(query.toString()); + } catch (NumberFormatException ignore) { + uid = -1; + } + for (Rule rule : listAll) + if (rule.uid == uid || + rule.packageName.toLowerCase().contains(query) || + (rule.name != null && rule.name.toLowerCase().contains(query))) + listResult.add(rule); + } + + FilterResults result = new FilterResults(); + result.values = listResult; + result.count = listResult.size(); + return result; + } + + @Override + protected void publishResults(CharSequence query, FilterResults result) { + listFiltered.clear(); + if (result == null) + listFiltered.addAll(listAll); + else { + listFiltered.addAll((List) result.values); + if (listFiltered.size() == 1) + listFiltered.get(0).expanded = true; + } + notifyDataSetChanged(); + } + }; + } + + @Override + public AdapterRule.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ViewHolder(inflater.inflate(R.layout.rule, parent, false)); + } + + @Override + public long getItemId(int position) { + Rule rule = listFiltered.get(position); + return rule.packageName.hashCode() * 100000L + rule.uid; + } + + @Override + public int getItemCount() { + return listFiltered.size(); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/Allowed.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/Allowed.java new file mode 100644 index 0000000..c854d38 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/Allowed.java @@ -0,0 +1,35 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +public class Allowed { + public String raddr; + public int rport; + + public Allowed() { + this.raddr = null; + this.rport = 0; + } + + public Allowed(String raddr, int rport) { + this.raddr = raddr; + this.rport = rport; + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ApplicationEx.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ApplicationEx.java new file mode 100644 index 0000000..3b7e0da --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ApplicationEx.java @@ -0,0 +1,78 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.annotation.TargetApi; +import android.app.Application; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.os.Build; +import android.util.Log; + +public class ApplicationEx extends Application { + private static final String TAG = "NetGuard.App"; + + private Thread.UncaughtExceptionHandler mPrevHandler; + + @Override + public void onCreate() { + super.onCreate(); + Log.i(TAG, "Create version=" + Util.getSelfVersionName(this) + "/" + Util.getSelfVersionCode(this)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + createNotificationChannels(); + + mPrevHandler = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread thread, Throwable ex) { + if (Util.ownFault(ApplicationEx.this, ex) + && Util.isPlayStoreInstall(ApplicationEx.this)) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + mPrevHandler.uncaughtException(thread, ex); + } else { + Log.w(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + System.exit(1); + } + } + }); + } + + @TargetApi(Build.VERSION_CODES.O) + private void createNotificationChannels() { + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + NotificationChannel foreground = new NotificationChannel("foreground", getString(R.string.channel_foreground), NotificationManager.IMPORTANCE_MIN); + foreground.setSound(null, Notification.AUDIO_ATTRIBUTES_DEFAULT); + nm.createNotificationChannel(foreground); + + NotificationChannel notify = new NotificationChannel("notify", getString(R.string.channel_notify), NotificationManager.IMPORTANCE_DEFAULT); + notify.setSound(null, Notification.AUDIO_ATTRIBUTES_DEFAULT); + notify.setBypassDnd(true); + nm.createNotificationChannel(notify); + + NotificationChannel access = new NotificationChannel("access", getString(R.string.channel_access), NotificationManager.IMPORTANCE_DEFAULT); + access.setSound(null, Notification.AUDIO_ATTRIBUTES_DEFAULT); + access.setBypassDnd(true); + nm.createNotificationChannel(access); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/DatabaseHelper.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/DatabaseHelper.java new file mode 100644 index 0000000..47c0b9a --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/DatabaseHelper.java @@ -0,0 +1,1164 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDoneException; +import android.database.sqlite.SQLiteOpenHelper; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.util.Log; + +import androidx.preference.PreferenceManager; + +import java.io.File; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class DatabaseHelper extends SQLiteOpenHelper { + private static final String TAG = "NetGuard.Database"; + + private static final String DB_NAME = "Netguard"; + private static final int DB_VERSION = 21; + + private static boolean once = true; + private static List logChangedListeners = new ArrayList<>(); + private static List accessChangedListeners = new ArrayList<>(); + private static List forwardChangedListeners = new ArrayList<>(); + + private static HandlerThread hthread = null; + private static Handler handler = null; + + private static final Map mapUidHosts = new HashMap<>(); + + private final static int MSG_LOG = 1; + private final static int MSG_ACCESS = 2; + private final static int MSG_FORWARD = 3; + + private SharedPreferences prefs; + private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); + + static { + hthread = new HandlerThread("DatabaseHelper"); + hthread.start(); + handler = new Handler(hthread.getLooper()) { + @Override + public void handleMessage(Message msg) { + handleChangedNotification(msg); + } + }; + } + + private static DatabaseHelper dh = null; + + public static DatabaseHelper getInstance(Context context) { + if (dh == null) + dh = new DatabaseHelper(context.getApplicationContext()); + return dh; + } + + public static void clearCache() { + synchronized (mapUidHosts) { + mapUidHosts.clear(); + } + } + + @Override + public void close() { + Log.w(TAG, "Database is being closed"); + } + + private DatabaseHelper(Context context) { + super(context, DB_NAME, null, DB_VERSION); + prefs = PreferenceManager.getDefaultSharedPreferences(context); + + if (!once) { + once = true; + + File dbfile = context.getDatabasePath(DB_NAME); + if (dbfile.exists()) { + Log.w(TAG, "Deleting " + dbfile); + dbfile.delete(); + } + + File dbjournal = context.getDatabasePath(DB_NAME + "-journal"); + if (dbjournal.exists()) { + Log.w(TAG, "Deleting " + dbjournal); + dbjournal.delete(); + } + } + } + + @Override + public void onCreate(SQLiteDatabase db) { + Log.i(TAG, "Creating database " + DB_NAME + " version " + DB_VERSION); + createTableLog(db); + createTableAccess(db); + createTableDns(db); + createTableForward(db); + createTableApp(db); + } + + @Override + public void onConfigure(SQLiteDatabase db) { + db.enableWriteAheadLogging(); + super.onConfigure(db); + } + + private void createTableLog(SQLiteDatabase db) { + Log.i(TAG, "Creating log table"); + db.execSQL("CREATE TABLE log (" + + " ID INTEGER PRIMARY KEY AUTOINCREMENT" + + ", time INTEGER NOT NULL" + + ", version INTEGER" + + ", protocol INTEGER" + + ", flags TEXT" + + ", saddr TEXT" + + ", sport INTEGER" + + ", daddr TEXT" + + ", dport INTEGER" + + ", dname TEXT" + + ", uid INTEGER" + + ", data TEXT" + + ", allowed INTEGER" + + ", connection INTEGER" + + ", interactive INTEGER" + + ");"); + db.execSQL("CREATE INDEX idx_log_time ON log(time)"); + db.execSQL("CREATE INDEX idx_log_dest ON log(daddr)"); + db.execSQL("CREATE INDEX idx_log_dname ON log(dname)"); + db.execSQL("CREATE INDEX idx_log_dport ON log(dport)"); + db.execSQL("CREATE INDEX idx_log_uid ON log(uid)"); + } + + private void createTableAccess(SQLiteDatabase db) { + Log.i(TAG, "Creating access table"); + db.execSQL("CREATE TABLE access (" + + " ID INTEGER PRIMARY KEY AUTOINCREMENT" + + ", uid INTEGER NOT NULL" + + ", version INTEGER NOT NULL" + + ", protocol INTEGER NOT NULL" + + ", daddr TEXT NOT NULL" + + ", dport INTEGER NOT NULL" + + ", time INTEGER NOT NULL" + + ", allowed INTEGER" + + ", block INTEGER NOT NULL" + + ", sent INTEGER" + + ", received INTEGER" + + ", connections INTEGER" + + ");"); + db.execSQL("CREATE UNIQUE INDEX idx_access ON access(uid, version, protocol, daddr, dport)"); + db.execSQL("CREATE INDEX idx_access_daddr ON access(daddr)"); + db.execSQL("CREATE INDEX idx_access_block ON access(block)"); + } + + private void createTableDns(SQLiteDatabase db) { + Log.i(TAG, "Creating dns table"); + db.execSQL("CREATE TABLE dns (" + + " ID INTEGER PRIMARY KEY AUTOINCREMENT" + + ", time INTEGER NOT NULL" + + ", qname TEXT NOT NULL" + + ", aname TEXT NOT NULL" + + ", resource TEXT NOT NULL" + + ", ttl INTEGER" + + ");"); + db.execSQL("CREATE UNIQUE INDEX idx_dns ON dns(qname, aname, resource)"); + db.execSQL("CREATE INDEX idx_dns_resource ON dns(resource)"); + } + + private void createTableForward(SQLiteDatabase db) { + Log.i(TAG, "Creating forward table"); + db.execSQL("CREATE TABLE forward (" + + " ID INTEGER PRIMARY KEY AUTOINCREMENT" + + ", protocol INTEGER NOT NULL" + + ", dport INTEGER NOT NULL" + + ", raddr TEXT NOT NULL" + + ", rport INTEGER NOT NULL" + + ", ruid INTEGER NOT NULL" + + ");"); + db.execSQL("CREATE UNIQUE INDEX idx_forward ON forward(protocol, dport)"); + } + + private void createTableApp(SQLiteDatabase db) { + Log.i(TAG, "Creating app table"); + db.execSQL("CREATE TABLE app (" + + " ID INTEGER PRIMARY KEY AUTOINCREMENT" + + ", package TEXT" + + ", label TEXT" + + ", system INTEGER NOT NULL" + + ", internet INTEGER NOT NULL" + + ", enabled INTEGER NOT NULL" + + ");"); + db.execSQL("CREATE UNIQUE INDEX idx_package ON app(package)"); + } + + private boolean columnExists(SQLiteDatabase db, String table, String column) { + Cursor cursor = null; + try { + cursor = db.rawQuery("SELECT * FROM " + table + " LIMIT 0", null); + return (cursor.getColumnIndex(column) >= 0); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + return false; + } finally { + if (cursor != null) + cursor.close(); + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.i(TAG, DB_NAME + " upgrading from version " + oldVersion + " to " + newVersion); + + db.beginTransaction(); + try { + if (oldVersion < 2) { + if (!columnExists(db, "log", "version")) + db.execSQL("ALTER TABLE log ADD COLUMN version INTEGER"); + if (!columnExists(db, "log", "protocol")) + db.execSQL("ALTER TABLE log ADD COLUMN protocol INTEGER"); + if (!columnExists(db, "log", "uid")) + db.execSQL("ALTER TABLE log ADD COLUMN uid INTEGER"); + oldVersion = 2; + } + if (oldVersion < 3) { + if (!columnExists(db, "log", "port")) + db.execSQL("ALTER TABLE log ADD COLUMN port INTEGER"); + if (!columnExists(db, "log", "flags")) + db.execSQL("ALTER TABLE log ADD COLUMN flags TEXT"); + oldVersion = 3; + } + if (oldVersion < 4) { + if (!columnExists(db, "log", "connection")) + db.execSQL("ALTER TABLE log ADD COLUMN connection INTEGER"); + oldVersion = 4; + } + if (oldVersion < 5) { + if (!columnExists(db, "log", "interactive")) + db.execSQL("ALTER TABLE log ADD COLUMN interactive INTEGER"); + oldVersion = 5; + } + if (oldVersion < 6) { + if (!columnExists(db, "log", "allowed")) + db.execSQL("ALTER TABLE log ADD COLUMN allowed INTEGER"); + oldVersion = 6; + } + if (oldVersion < 7) { + db.execSQL("DROP TABLE log"); + createTableLog(db); + oldVersion = 8; + } + if (oldVersion < 8) { + if (!columnExists(db, "log", "data")) + db.execSQL("ALTER TABLE log ADD COLUMN data TEXT"); + db.execSQL("DROP INDEX idx_log_source"); + db.execSQL("DROP INDEX idx_log_dest"); + db.execSQL("CREATE INDEX idx_log_source ON log(saddr)"); + db.execSQL("CREATE INDEX idx_log_dest ON log(daddr)"); + db.execSQL("CREATE INDEX IF NOT EXISTS idx_log_uid ON log(uid)"); + oldVersion = 8; + } + if (oldVersion < 9) { + createTableAccess(db); + oldVersion = 9; + } + if (oldVersion < 10) { + db.execSQL("DROP TABLE log"); + db.execSQL("DROP TABLE access"); + createTableLog(db); + createTableAccess(db); + oldVersion = 10; + } + if (oldVersion < 12) { + db.execSQL("DROP TABLE access"); + createTableAccess(db); + oldVersion = 12; + } + if (oldVersion < 13) { + db.execSQL("CREATE INDEX IF NOT EXISTS idx_log_dport ON log(dport)"); + db.execSQL("CREATE INDEX IF NOT EXISTS idx_log_dname ON log(dname)"); + oldVersion = 13; + } + if (oldVersion < 14) { + createTableDns(db); + oldVersion = 14; + } + if (oldVersion < 15) { + db.execSQL("DROP TABLE access"); + createTableAccess(db); + oldVersion = 15; + } + if (oldVersion < 16) { + createTableForward(db); + oldVersion = 16; + } + if (oldVersion < 17) { + if (!columnExists(db, "access", "sent")) + db.execSQL("ALTER TABLE access ADD COLUMN sent INTEGER"); + if (!columnExists(db, "access", "received")) + db.execSQL("ALTER TABLE access ADD COLUMN received INTEGER"); + oldVersion = 17; + } + if (oldVersion < 18) { + db.execSQL("CREATE INDEX IF NOT EXISTS idx_access_block ON access(block)"); + db.execSQL("DROP INDEX idx_dns"); + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS idx_dns ON dns(qname, aname, resource)"); + db.execSQL("CREATE INDEX IF NOT EXISTS idx_dns_resource ON dns(resource)"); + oldVersion = 18; + } + if (oldVersion < 19) { + if (!columnExists(db, "access", "connections")) + db.execSQL("ALTER TABLE access ADD COLUMN connections INTEGER"); + oldVersion = 19; + } + if (oldVersion < 20) { + db.execSQL("CREATE INDEX IF NOT EXISTS idx_access_daddr ON access(daddr)"); + oldVersion = 20; + } + if (oldVersion < 21) { + createTableApp(db); + oldVersion = 21; + } + + if (oldVersion == DB_VERSION) { + db.setVersion(oldVersion); + db.setTransactionSuccessful(); + Log.i(TAG, DB_NAME + " upgraded to " + DB_VERSION); + } else + throw new IllegalArgumentException(DB_NAME + " upgraded to " + oldVersion + " but required " + DB_VERSION); + + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } finally { + db.endTransaction(); + } + } + + // Log + + public void insertLog(Packet packet, String dname, int connection, boolean interactive) { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + ContentValues cv = new ContentValues(); + cv.put("time", packet.time); + cv.put("version", packet.version); + + if (packet.protocol < 0) + cv.putNull("protocol"); + else + cv.put("protocol", packet.protocol); + + cv.put("flags", packet.flags); + + cv.put("saddr", packet.saddr); + if (packet.sport < 0) + cv.putNull("sport"); + else + cv.put("sport", packet.sport); + + cv.put("daddr", packet.daddr); + if (packet.dport < 0) + cv.putNull("dport"); + else + cv.put("dport", packet.dport); + + if (dname == null) + cv.putNull("dname"); + else + cv.put("dname", dname); + + cv.put("data", packet.data); + + if (packet.uid < 0) + cv.putNull("uid"); + else + cv.put("uid", packet.uid); + + cv.put("allowed", packet.allowed ? 1 : 0); + + cv.put("connection", connection); + cv.put("interactive", interactive ? 1 : 0); + + if (db.insert("log", null, cv) == -1) + Log.e(TAG, "Insert log failed"); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + + notifyLogChanged(); + } + + public void clearLog(int uid) { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + if (uid < 0) + db.delete("log", null, new String[]{}); + else + db.delete("log", "uid = ?", new String[]{Integer.toString(uid)}); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + db.execSQL("VACUUM"); + } finally { + lock.writeLock().unlock(); + } + + notifyLogChanged(); + } + + public void cleanupLog(long time) { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + // There an index on time + int rows = db.delete("log", "time < ?", new String[]{Long.toString(time)}); + Log.i(TAG, "Cleanup log" + + " before=" + SimpleDateFormat.getDateTimeInstance().format(new Date(time)) + + " rows=" + rows); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + } + + public Cursor getLog(boolean udp, boolean tcp, boolean other, boolean allowed, boolean blocked) { + lock.readLock().lock(); + try { + SQLiteDatabase db = this.getReadableDatabase(); + // There is an index on time + // There is no index on protocol/allowed for write performance + String query = "SELECT ID AS _id, *"; + query += " FROM log"; + query += " WHERE (0 = 1"; + if (udp) + query += " OR protocol = 17"; + if (tcp) + query += " OR protocol = 6"; + if (other) + query += " OR (protocol <> 6 AND protocol <> 17)"; + query += ") AND (0 = 1"; + if (allowed) + query += " OR allowed = 1"; + if (blocked) + query += " OR allowed = 0"; + query += ")"; + query += " ORDER BY time DESC"; + return db.rawQuery(query, new String[]{}); + } finally { + lock.readLock().unlock(); + } + } + + public Cursor searchLog(String find) { + lock.readLock().lock(); + try { + SQLiteDatabase db = this.getReadableDatabase(); + // There is an index on daddr, dname, dport and uid + String query = "SELECT ID AS _id, *"; + query += " FROM log"; + query += " WHERE daddr LIKE ? OR dname LIKE ? OR dport = ? OR uid = ?"; + query += " ORDER BY time DESC"; + return db.rawQuery(query, new String[]{"%" + find + "%", "%" + find + "%", find, find}); + } finally { + lock.readLock().unlock(); + } + } + + // Access + + public boolean updateAccess(Packet packet, String dname, int block) { + int rows; + + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + ContentValues cv = new ContentValues(); + cv.put("time", packet.time); + cv.put("allowed", packet.allowed ? 1 : 0); + if (block >= 0) + cv.put("block", block); + + // There is a segmented index on uid, version, protocol, daddr and dport + rows = db.update("access", cv, "uid = ? AND version = ? AND protocol = ? AND daddr = ? AND dport = ?", + new String[]{ + Integer.toString(packet.uid), + Integer.toString(packet.version), + Integer.toString(packet.protocol), + dname == null ? packet.daddr : dname, + Integer.toString(packet.dport)}); + + if (rows == 0) { + cv.put("uid", packet.uid); + cv.put("version", packet.version); + cv.put("protocol", packet.protocol); + cv.put("daddr", dname == null ? packet.daddr : dname); + cv.put("dport", packet.dport); + if (block < 0) + cv.put("block", block); + + if (db.insert("access", null, cv) == -1) + Log.e(TAG, "Insert access failed"); + } else if (rows != 1) + Log.e(TAG, "Update access failed rows=" + rows); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + + notifyAccessChanged(); + return (rows == 0); + } + + public void updateUsage(Usage usage, String dname) { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + // There is a segmented index on uid, version, protocol, daddr and dport + String selection = "uid = ? AND version = ? AND protocol = ? AND daddr = ? AND dport = ?"; + String[] selectionArgs = new String[]{ + Integer.toString(usage.Uid), + Integer.toString(usage.Version), + Integer.toString(usage.Protocol), + dname == null ? usage.DAddr : dname, + Integer.toString(usage.DPort) + }; + + try (Cursor cursor = db.query("access", new String[]{"sent", "received", "connections"}, selection, selectionArgs, null, null, null)) { + long sent = 0; + long received = 0; + int connections = 0; + int colSent = cursor.getColumnIndex("sent"); + int colReceived = cursor.getColumnIndex("received"); + int colConnections = cursor.getColumnIndex("connections"); + if (cursor.moveToNext()) { + sent = cursor.isNull(colSent) ? 0 : cursor.getLong(colSent); + received = cursor.isNull(colReceived) ? 0 : cursor.getLong(colReceived); + connections = cursor.isNull(colConnections) ? 0 : cursor.getInt(colConnections); + } + + ContentValues cv = new ContentValues(); + cv.put("sent", sent + usage.Sent); + cv.put("received", received + usage.Received); + cv.put("connections", connections + 1); + + int rows = db.update("access", cv, selection, selectionArgs); + if (rows != 1) + Log.e(TAG, "Update usage failed rows=" + rows); + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + + notifyAccessChanged(); + } + + public void setAccess(long id, int block) { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + ContentValues cv = new ContentValues(); + cv.put("block", block); + cv.put("allowed", -1); + + if (db.update("access", cv, "ID = ?", new String[]{Long.toString(id)}) != 1) + Log.e(TAG, "Set access failed"); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + + notifyAccessChanged(); + } + + public void clearAccess() { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + db.delete("access", null, null); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + + notifyAccessChanged(); + } + + public void clearAccess(int uid, boolean keeprules) { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + // There is a segmented index on uid + // There is an index on block + if (keeprules) + db.delete("access", "uid = ? AND block < 0", new String[]{Integer.toString(uid)}); + else + db.delete("access", "uid = ?", new String[]{Integer.toString(uid)}); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + + notifyAccessChanged(); + } + + public void resetUsage(int uid) { + lock.writeLock().lock(); + try { + // There is a segmented index on uid + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + ContentValues cv = new ContentValues(); + cv.putNull("sent"); + cv.putNull("received"); + cv.putNull("connections"); + db.update("access", cv, + (uid < 0 ? null : "uid = ?"), + (uid < 0 ? null : new String[]{Integer.toString(uid)})); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + + notifyAccessChanged(); + } + + public Cursor getAccess(int uid) { + lock.readLock().lock(); + try { + SQLiteDatabase db = this.getReadableDatabase(); + // There is a segmented index on uid + // There is no index on time for write performance + String query = "SELECT a.ID AS _id, a.*"; + query += ", (SELECT COUNT(DISTINCT d.qname) FROM dns d WHERE d.resource IN (SELECT d1.resource FROM dns d1 WHERE d1.qname = a.daddr)) count"; + query += " FROM access a"; + query += " WHERE a.uid = ?"; + query += " ORDER BY a.time DESC"; + query += " LIMIT 250"; + return db.rawQuery(query, new String[]{Integer.toString(uid)}); + } finally { + lock.readLock().unlock(); + } + } + + public Cursor getAccess() { + lock.readLock().lock(); + try { + SQLiteDatabase db = this.getReadableDatabase(); + // There is a segmented index on uid + // There is an index on block + return db.query("access", null, "block >= 0", null, null, null, "uid"); + } finally { + lock.readLock().unlock(); + } + } + + public Cursor getAccessUnset(int uid, int limit, long since) { + lock.readLock().lock(); + try { + SQLiteDatabase db = this.getReadableDatabase(); + // There is a segmented index on uid, block and daddr + // There is no index on allowed and time for write performance + String query = "SELECT MAX(time) AS time, daddr, allowed"; + query += " FROM access"; + query += " WHERE uid = ?"; + query += " AND block < 0"; + query += " AND time >= ?"; + query += " GROUP BY daddr, allowed"; + query += " ORDER BY time DESC"; + if (limit > 0) + query += " LIMIT " + limit; + return db.rawQuery(query, new String[]{Integer.toString(uid), Long.toString(since)}); + } finally { + lock.readLock().unlock(); + } + } + + public long getHostCount(int uid, boolean usecache) { + if (usecache) + synchronized (mapUidHosts) { + if (mapUidHosts.containsKey(uid)) + return mapUidHosts.get(uid); + } + + lock.readLock().lock(); + try { + SQLiteDatabase db = this.getReadableDatabase(); + // There is a segmented index on uid + // There is an index on block + long hosts = db.compileStatement("SELECT COUNT(*) FROM access WHERE block >= 0 AND uid =" + uid).simpleQueryForLong(); + synchronized (mapUidHosts) { + mapUidHosts.put(uid, hosts); + } + return hosts; + } finally { + lock.readLock().unlock(); + } + } + + // DNS + + public boolean insertDns(ResourceRecord rr) { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + int ttl = rr.TTL; + + int min = Integer.parseInt(prefs.getString("ttl", "259200")); + if (ttl < min) + ttl = min; + + ContentValues cv = new ContentValues(); + cv.put("time", rr.Time); + cv.put("ttl", ttl * 1000L); + + int rows = db.update("dns", cv, "qname = ? AND aname = ? AND resource = ?", + new String[]{rr.QName, rr.AName, rr.Resource}); + + if (rows == 0) { + cv.put("qname", rr.QName); + cv.put("aname", rr.AName); + cv.put("resource", rr.Resource); + + if (db.insert("dns", null, cv) == -1) + Log.e(TAG, "Insert dns failed"); + else + rows = 1; + } else if (rows != 1) + Log.e(TAG, "Update dns failed rows=" + rows); + + db.setTransactionSuccessful(); + + return (rows > 0); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + } + + public void cleanupDns() { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + // There is no index on time for write performance + long now = new Date().getTime(); + db.execSQL("DELETE FROM dns WHERE time + ttl < " + now); + Log.i(TAG, "Cleanup DNS"); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + } + + public void clearDns() { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + db.delete("dns", null, new String[]{}); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + } + + public String getQName(int uid, String ip) { + lock.readLock().lock(); + try { + SQLiteDatabase db = this.getReadableDatabase(); + // There is a segmented index on resource + String query = "SELECT d.qname"; + query += " FROM dns AS d"; + query += " WHERE d.resource = '" + ip.replace("'", "''") + "'"; + query += " ORDER BY d.qname"; + query += " LIMIT 1"; + // There is no way to known for sure which domain name an app used, so just pick the first one + return db.compileStatement(query).simpleQueryForString(); + } catch (SQLiteDoneException ignored) { + // Not found + return null; + } finally { + lock.readLock().unlock(); + } + } + + public Cursor getAlternateQNames(String qname) { + lock.readLock().lock(); + try { + SQLiteDatabase db = this.getReadableDatabase(); + String query = "SELECT DISTINCT d2.qname"; + query += " FROM dns d1"; + query += " JOIN dns d2"; + query += " ON d2.resource = d1.resource AND d2.id <> d1.id"; + query += " WHERE d1.qname = ?"; + query += " ORDER BY d2.qname"; + return db.rawQuery(query, new String[]{qname}); + } finally { + lock.readLock().unlock(); + } + } + + public Cursor getDns() { + lock.readLock().lock(); + try { + SQLiteDatabase db = this.getReadableDatabase(); + // There is an index on resource + // There is a segmented index on qname + String query = "SELECT ID AS _id, *"; + query += " FROM dns"; + query += " ORDER BY resource, qname"; + return db.rawQuery(query, new String[]{}); + } finally { + lock.readLock().unlock(); + } + } + + public Cursor getAccessDns(String dname) { + long now = new Date().getTime(); + lock.readLock().lock(); + try { + SQLiteDatabase db = this.getReadableDatabase(); + + // There is a segmented index on dns.qname + // There is an index on access.daddr and access.block + String query = "SELECT a.uid, a.version, a.protocol, a.daddr, d.resource, a.dport, a.block, d.time, d.ttl"; + query += " FROM access AS a"; + query += " LEFT JOIN dns AS d"; + query += " ON d.qname = a.daddr"; + query += " WHERE a.block >= 0"; + query += " AND (d.time IS NULL OR d.time + d.ttl >= " + now + ")"; + if (dname != null) + query += " AND a.daddr = ?"; + + return db.rawQuery(query, dname == null ? new String[]{} : new String[]{dname}); + } finally { + lock.readLock().unlock(); + } + } + + // Forward + + public void addForward(int protocol, int dport, String raddr, int rport, int ruid) { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + ContentValues cv = new ContentValues(); + cv.put("protocol", protocol); + cv.put("dport", dport); + cv.put("raddr", raddr); + cv.put("rport", rport); + cv.put("ruid", ruid); + + if (db.insert("forward", null, cv) < 0) + Log.e(TAG, "Insert forward failed"); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + + notifyForwardChanged(); + } + + public void deleteForward() { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + db.delete("forward", null, null); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + + notifyForwardChanged(); + } + + public void deleteForward(int protocol, int dport) { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + db.delete("forward", "protocol = ? AND dport = ?", + new String[]{Integer.toString(protocol), Integer.toString(dport)}); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + + notifyForwardChanged(); + } + + public Cursor getForwarding() { + lock.readLock().lock(); + try { + SQLiteDatabase db = this.getReadableDatabase(); + String query = "SELECT ID AS _id, *"; + query += " FROM forward"; + query += " ORDER BY dport"; + return db.rawQuery(query, new String[]{}); + } finally { + lock.readLock().unlock(); + } + } + + public void addApp(String packageName, String label, boolean system, boolean internet, boolean enabled) { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + ContentValues cv = new ContentValues(); + cv.put("package", packageName); + if (label == null) + cv.putNull("label"); + else + cv.put("label", label); + cv.put("system", system ? 1 : 0); + cv.put("internet", internet ? 1 : 0); + cv.put("enabled", enabled ? 1 : 0); + + if (db.insert("app", null, cv) < 0) + Log.e(TAG, "Insert app failed"); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + } + + public Cursor getApp(String packageName) { + lock.readLock().lock(); + try { + SQLiteDatabase db = this.getReadableDatabase(); + + // There is an index on package + String query = "SELECT * FROM app WHERE package = ?"; + + return db.rawQuery(query, new String[]{packageName}); + } finally { + lock.readLock().unlock(); + } + } + + public void clearApps() { + lock.writeLock().lock(); + try { + SQLiteDatabase db = this.getWritableDatabase(); + db.beginTransactionNonExclusive(); + try { + db.delete("app", null, null); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } finally { + lock.writeLock().unlock(); + } + } + + public void addLogChangedListener(LogChangedListener listener) { + logChangedListeners.add(listener); + } + + public void removeLogChangedListener(LogChangedListener listener) { + logChangedListeners.remove(listener); + } + + public void addAccessChangedListener(AccessChangedListener listener) { + accessChangedListeners.add(listener); + } + + public void removeAccessChangedListener(AccessChangedListener listener) { + accessChangedListeners.remove(listener); + } + + public void addForwardChangedListener(ForwardChangedListener listener) { + forwardChangedListeners.add(listener); + } + + public void removeForwardChangedListener(ForwardChangedListener listener) { + forwardChangedListeners.remove(listener); + } + + private void notifyLogChanged() { + Message msg = handler.obtainMessage(); + msg.what = MSG_LOG; + handler.sendMessage(msg); + } + + private void notifyAccessChanged() { + Message msg = handler.obtainMessage(); + msg.what = MSG_ACCESS; + handler.sendMessage(msg); + } + + private void notifyForwardChanged() { + Message msg = handler.obtainMessage(); + msg.what = MSG_FORWARD; + handler.sendMessage(msg); + } + + private static void handleChangedNotification(Message msg) { + // Batch notifications + try { + Thread.sleep(1000); + if (handler.hasMessages(msg.what)) + handler.removeMessages(msg.what); + } catch (InterruptedException ignored) { + } + + // Notify listeners + if (msg.what == MSG_LOG) { + for (LogChangedListener listener : logChangedListeners) + try { + listener.onChanged(); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + } else if (msg.what == MSG_ACCESS) { + for (AccessChangedListener listener : accessChangedListeners) + try { + listener.onChanged(); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + } else if (msg.what == MSG_FORWARD) { + for (ForwardChangedListener listener : forwardChangedListeners) + try { + listener.onChanged(); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + } + + public interface LogChangedListener { + void onChanged(); + } + + public interface AccessChangedListener { + void onChanged(); + } + + public interface ForwardChangedListener { + void onChanged(); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/DownloadTask.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/DownloadTask.java new file mode 100644 index 0000000..ca74ade --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/DownloadTask.java @@ -0,0 +1,181 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Build; +import android.os.PowerManager; +import android.util.Log; +import android.util.TypedValue; +import android.widget.Toast; + +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; + +public class DownloadTask extends AsyncTask { + private static final String TAG = "NetGuard.Download"; + + private Context context; + private URL url; + private File file; + private Listener listener; + private PowerManager.WakeLock wakeLock; + + public interface Listener { + void onCompleted(); + + void onCancelled(); + + void onException(Throwable ex); + } + + public DownloadTask(Activity context, URL url, File file, Listener listener) { + this.context = context; + this.url = url; + this.file = file; + this.listener = listener; + } + + @Override + protected void onPreExecute() { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, getClass().getName()); + wakeLock.acquire(); + showNotification(0); + Toast.makeText(context, context.getString(R.string.msg_downloading, url.toString()), Toast.LENGTH_SHORT).show(); + } + + @Override + protected Object doInBackground(Object... args) { + Log.i(TAG, "Downloading " + url + " into " + file); + + InputStream in = null; + OutputStream out = null; + URLConnection connection = null; + try { + connection = url.openConnection(); + connection.connect(); + + if (connection instanceof HttpURLConnection) { + HttpURLConnection httpConnection = (HttpURLConnection) connection; + if (httpConnection.getResponseCode() != HttpURLConnection.HTTP_OK) + throw new IOException(httpConnection.getResponseCode() + " " + httpConnection.getResponseMessage()); + } + + int contentLength = connection.getContentLength(); + Log.i(TAG, "Content length=" + contentLength); + in = connection.getInputStream(); + out = new FileOutputStream(file); + + long size = 0; + byte buffer[] = new byte[4096]; + int bytes; + while (!isCancelled() && (bytes = in.read(buffer)) != -1) { + out.write(buffer, 0, bytes); + + size += bytes; + if (contentLength > 0) + publishProgress((int) (size * 100 / contentLength)); + } + + Log.i(TAG, "Downloaded size=" + size); + return null; + } catch (Throwable ex) { + return ex; + } finally { + try { + if (out != null) + out.close(); + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + try { + if (in != null) + in.close(); + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + if (connection instanceof HttpURLConnection) + ((HttpURLConnection) connection).disconnect(); + } + } + + @Override + protected void onProgressUpdate(Integer... progress) { + super.onProgressUpdate(progress); + showNotification(progress[0]); + } + + @Override + protected void onCancelled() { + super.onCancelled(); + Log.i(TAG, "Cancelled"); + listener.onCancelled(); + } + + @Override + protected void onPostExecute(Object result) { + wakeLock.release(); + NotificationManagerCompat.from(context).cancel(ServiceSinkhole.NOTIFY_DOWNLOAD); + if (result instanceof Throwable) { + Log.e(TAG, result.toString() + "\n" + Log.getStackTraceString((Throwable) result)); + listener.onException((Throwable) result); + } else + listener.onCompleted(); + } + + private void showNotification(int progress) { + Intent main = new Intent(context, ActivitySettings.class); + PendingIntent pi = PendingIntent.getActivity(context, ServiceSinkhole.NOTIFY_DOWNLOAD, main, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + + TypedValue tv = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.colorOff, tv, true); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "notify"); + builder.setSmallIcon(R.drawable.ic_file_download_white_24dp) + .setContentTitle(context.getString(R.string.app_name)) + .setContentText(context.getString(R.string.msg_downloading, url.toString())) + .setContentIntent(pi) + .setProgress(100, progress, false) + .setColor(tv.data) + .setOngoing(true) + .setAutoCancel(false); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + builder.setCategory(NotificationCompat.CATEGORY_STATUS) + .setVisibility(NotificationCompat.VISIBILITY_SECRET); + + NotificationManagerCompat.from(context).notify(ServiceSinkhole.NOTIFY_DOWNLOAD, builder.build()); + } + +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ExpandedListView.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ExpandedListView.java new file mode 100644 index 0000000..ba3a684 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ExpandedListView.java @@ -0,0 +1,45 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ListView; + +// This requires list view items with equal heights + +public class ExpandedListView extends ListView { + public ExpandedListView(Context context) { + super(context); + } + + public ExpandedListView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ExpandedListView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 4, MeasureSpec.AT_MOST)); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/Forward.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/Forward.java new file mode 100644 index 0000000..675d782 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/Forward.java @@ -0,0 +1,33 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +public class Forward { + public int protocol; + public int dport; + public String raddr; + public int rport; + public int ruid; + + @Override + public String toString() { + return "protocol=" + protocol + " port " + dport + " to " + raddr + "/" + rport + " uid " + ruid; + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/FragmentSettings.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/FragmentSettings.java new file mode 100644 index 0000000..df8b3d3 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/FragmentSettings.java @@ -0,0 +1,32 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.os.Bundle; +import android.preference.PreferenceFragment; + +public class FragmentSettings extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.preferences); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/GlideHelper.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/GlideHelper.java new file mode 100644 index 0000000..a10561e --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/GlideHelper.java @@ -0,0 +1,8 @@ +package eu.faircode.netguard; + +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.module.AppGlideModule; + +@GlideModule +public final class GlideHelper extends AppGlideModule { +} \ No newline at end of file diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/IAB.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/IAB.java new file mode 100644 index 0000000..86b0dd1 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/IAB.java @@ -0,0 +1,240 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import androidx.preference.PreferenceManager; + +import com.android.vending.billing.IInAppBillingService; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +public class IAB implements ServiceConnection { + private static final String TAG = "NetGuard.IAB"; + + private Context context; + private Delegate delegate; + private IInAppBillingService service = null; + + private static final int IAB_VERSION = 3; + + public interface Delegate { + void onReady(IAB iab); + } + + public IAB(Delegate delegate, Context context) { + this.context = context.getApplicationContext(); + this.delegate = delegate; + } + + public void bind() { + Log.i(TAG, "Bind"); + Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); + serviceIntent.setPackage("com.android.vending"); + context.bindService(serviceIntent, this, Context.BIND_AUTO_CREATE); + } + + public void unbind() { + if (service != null) { + Log.i(TAG, "Unbind"); + context.unbindService(this); + service = null; + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + Log.i(TAG, "Connected"); + service = IInAppBillingService.Stub.asInterface(binder); + delegate.onReady(this); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.i(TAG, "Disconnected"); + service = null; + } + + public boolean isAvailable(String sku) throws RemoteException, JSONException { + // Get available SKUs + ArrayList skuList = new ArrayList<>(); + skuList.add(sku); + Bundle query = new Bundle(); + query.putStringArrayList("ITEM_ID_LIST", skuList); + Bundle bundle = service.getSkuDetails(IAB_VERSION, context.getPackageName(), "inapp", query); + Log.i(TAG, "getSkuDetails"); + Util.logBundle(bundle); + int response = (bundle == null ? -1 : bundle.getInt("RESPONSE_CODE", -1)); + Log.i(TAG, "Response=" + getResult(response)); + if (response != 0) + throw new IllegalArgumentException(getResult(response)); + + // Check available SKUs + boolean found = false; + ArrayList details = bundle.getStringArrayList("DETAILS_LIST"); + if (details != null) + for (String item : details) { + JSONObject object = new JSONObject(item); + if (sku.equals(object.getString("productId"))) { + found = true; + break; + } + } + Log.i(TAG, sku + "=" + found); + + return found; + } + + public void updatePurchases() throws RemoteException { + // Get purchases + List skus = new ArrayList<>(); + skus.addAll(getPurchases("inapp")); + skus.addAll(getPurchases("subs")); + + SharedPreferences prefs = context.getSharedPreferences("IAB", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + for (String product : prefs.getAll().keySet()) + if (!ActivityPro.SKU_DONATION.equals(product)) { + Log.i(TAG, "removing SKU=" + product); + editor.remove(product); + } + for (String sku : skus) { + Log.i(TAG, "adding SKU=" + sku); + editor.putBoolean(sku, true); + } + editor.apply(); + } + + public boolean isPurchased(String sku, String type) throws RemoteException { + return getPurchases(type).contains(sku); + } + + public List getPurchases(String type) throws RemoteException { + // Get purchases + Bundle bundle = service.getPurchases(IAB_VERSION, context.getPackageName(), type, null); + Log.i(TAG, "getPurchases"); + Util.logBundle(bundle); + int response = (bundle == null ? -1 : bundle.getInt("RESPONSE_CODE", -1)); + Log.i(TAG, "Response=" + getResult(response)); + if (response != 0) + throw new IllegalArgumentException(getResult(response)); + + ArrayList details = bundle.getStringArrayList("INAPP_PURCHASE_ITEM_LIST"); + return (details == null ? new ArrayList() : details); + } + + public PendingIntent getBuyIntent(String sku, boolean subscription) throws RemoteException { + if (service == null) + return null; + Bundle bundle = service.getBuyIntent(IAB_VERSION, context.getPackageName(), sku, subscription ? "subs" : "inapp", "netguard"); + Log.i(TAG, "getBuyIntent sku=" + sku + " subscription=" + subscription); + Util.logBundle(bundle); + int response = (bundle == null ? -1 : bundle.getInt("RESPONSE_CODE", -1)); + Log.i(TAG, "Response=" + getResult(response)); + if (response != 0) + throw new IllegalArgumentException(getResult(response)); + if (!bundle.containsKey("BUY_INTENT")) + throw new IllegalArgumentException("BUY_INTENT missing"); + return bundle.getParcelable("BUY_INTENT"); + } + + public static void setBought(String sku, Context context) { + Log.i(TAG, "Bought " + sku); + SharedPreferences prefs = context.getSharedPreferences("IAB", Context.MODE_PRIVATE); + prefs.edit().putBoolean(sku, true).apply(); + } + + public static boolean isPurchased(String sku, Context context) { + try { + if (Util.isDebuggable(context)) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return !prefs.getBoolean("debug_iab", false); + } + + SharedPreferences prefs = context.getSharedPreferences("IAB", Context.MODE_PRIVATE); + if (ActivityPro.SKU_SUPPORT1.equals(sku) || ActivityPro.SKU_SUPPORT2.equals(sku)) + return prefs.getBoolean(sku, false); + + return (prefs.getBoolean(sku, false) || + prefs.getBoolean(ActivityPro.SKU_PRO1, false) || + prefs.getBoolean(ActivityPro.SKU_SUPPORT1, false) || + prefs.getBoolean(ActivityPro.SKU_SUPPORT2, false) || + prefs.getBoolean(ActivityPro.SKU_DONATION, false)); + } catch (SecurityException ignored) { + return false; + } + } + + public static boolean isPurchasedAny(Context context) { + try { + if (Util.isDebuggable(context)) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return !(prefs.getBoolean("debug_iab", false)); + } + + SharedPreferences prefs = context.getSharedPreferences("IAB", Context.MODE_PRIVATE); + for (String key : prefs.getAll().keySet()) + if (prefs.getBoolean(key, false)) + return true; + return false; + } catch (SecurityException ignored) { + return false; + } + } + + public static String getResult(int responseCode) { + switch (responseCode) { + case 0: + return "OK"; + case 1: + return "USER_CANCELED"; + case 2: + return "SERVICE_UNAVAILABLE"; + case 3: + return "BILLING_UNAVAILABLE"; + case 4: + return "ITEM_UNAVAILABLE"; + case 5: + return "DEVELOPER_ERROR"; + case 6: + return "ERROR"; + case 7: + return "ITEM_ALREADY_OWNED"; + case 8: + return "ITEM_NOT_OWNED"; + default: + return Integer.toString(responseCode); + } + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/IPUtil.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/IPUtil.java new file mode 100644 index 0000000..e18170d --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/IPUtil.java @@ -0,0 +1,140 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.util.Log; + +import androidx.annotation.NonNull; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; + +public class IPUtil { + private static final String TAG = "NetGuard.IPUtil"; + + public static List toCIDR(String start, String end) throws UnknownHostException { + return toCIDR(InetAddress.getByName(start), InetAddress.getByName(end)); + } + + public static List toCIDR(InetAddress start, InetAddress end) throws UnknownHostException { + List listResult = new ArrayList<>(); + + Log.i(TAG, "toCIDR(" + start.getHostAddress() + "," + end.getHostAddress() + ")"); + + long from = inet2long(start); + long to = inet2long(end); + while (to >= from) { + byte prefix = 32; + while (prefix > 0) { + long mask = prefix2mask(prefix - 1); + if ((from & mask) != from) + break; + prefix--; + } + + byte max = (byte) (32 - Math.floor(Math.log(to - from + 1) / Math.log(2))); + if (prefix < max) + prefix = max; + + listResult.add(new CIDR(long2inet(from), prefix)); + + from += Math.pow(2, (32 - prefix)); + } + + for (CIDR cidr : listResult) + Log.i(TAG, cidr.toString()); + + return listResult; + } + + private static long prefix2mask(int bits) { + return (0xFFFFFFFF00000000L >> bits) & 0xFFFFFFFFL; + } + + private static long inet2long(InetAddress addr) { + long result = 0; + if (addr != null) + for (byte b : addr.getAddress()) + result = result << 8 | (b & 0xFF); + return result; + } + + private static InetAddress long2inet(long addr) { + try { + byte[] b = new byte[4]; + for (int i = b.length - 1; i >= 0; i--) { + b[i] = (byte) (addr & 0xFF); + addr = addr >> 8; + } + return InetAddress.getByAddress(b); + } catch (UnknownHostException ignore) { + return null; + } + } + + public static InetAddress minus1(InetAddress addr) { + return long2inet(inet2long(addr) - 1); + } + + public static InetAddress plus1(InetAddress addr) { + return long2inet(inet2long(addr) + 1); + } + + public static class CIDR implements Comparable { + public InetAddress address; + public int prefix; + + public CIDR(InetAddress address, int prefix) { + this.address = address; + this.prefix = prefix; + } + + public CIDR(String ip, int prefix) { + try { + this.address = InetAddress.getByName(ip); + this.prefix = prefix; + } catch (UnknownHostException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + + public InetAddress getStart() { + return long2inet(inet2long(this.address) & prefix2mask(this.prefix)); + } + + public InetAddress getEnd() { + return long2inet((inet2long(this.address) & prefix2mask(this.prefix)) + (1L << (32 - this.prefix)) - 1); + } + + @Override + public String toString() { + return address.getHostAddress() + "/" + prefix + "=" + getStart().getHostAddress() + "..." + getEnd().getHostAddress(); + } + + @Override + public int compareTo(@NonNull CIDR other) { + Long lcidr = IPUtil.inet2long(this.address); + Long lother = IPUtil.inet2long(other.address); + return lcidr.compareTo(lother); + } + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/Packet.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/Packet.java new file mode 100644 index 0000000..5460add --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/Packet.java @@ -0,0 +1,42 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +public class Packet { + public long time; + public int version; + public int protocol; + public String flags; + public String saddr; + public int sport; + public String daddr; + public int dport; + public String data; + public int uid; + public boolean allowed; + + public Packet() { + } + + @Override + public String toString() { + return "uid=" + uid + " v" + version + " p" + protocol + " " + daddr + "/" + dport; + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ReceiverAutostart.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ReceiverAutostart.java new file mode 100644 index 0000000..75aad2a --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ReceiverAutostart.java @@ -0,0 +1,132 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.util.Log; + +import androidx.preference.PreferenceManager; + +import java.util.Map; + +public class ReceiverAutostart extends BroadcastReceiver { + private static final String TAG = "NetGuard.Receiver"; + + @Override + public void onReceive(final Context context, Intent intent) { + Log.i(TAG, "Received " + intent); + Util.logExtras(intent); + + String action = (intent == null ? null : intent.getAction()); + if (Intent.ACTION_BOOT_COMPLETED.equals(action) || Intent.ACTION_MY_PACKAGE_REPLACED.equals(action)) + try { + // Upgrade settings + upgrade(true, context); + + // Start service + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + if (prefs.getBoolean("enabled", false)) + ServiceSinkhole.start("receiver", context); + else if (prefs.getBoolean("show_stats", false)) + ServiceSinkhole.run("receiver", context); + + if (Util.isInteractive(context)) + ServiceSinkhole.reloadStats("receiver", context); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + + public static void upgrade(boolean initialized, Context context) { + synchronized (context.getApplicationContext()) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + int oldVersion = prefs.getInt("version", -1); + int newVersion = Util.getSelfVersionCode(context); + if (oldVersion == newVersion) + return; + Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion); + + SharedPreferences.Editor editor = prefs.edit(); + + if (initialized) { + if (oldVersion < 38) { + Log.i(TAG, "Converting screen wifi/mobile"); + editor.putBoolean("screen_wifi", prefs.getBoolean("unused", false)); + editor.putBoolean("screen_other", prefs.getBoolean("unused", false)); + editor.remove("unused"); + + SharedPreferences unused = context.getSharedPreferences("unused", Context.MODE_PRIVATE); + SharedPreferences screen_wifi = context.getSharedPreferences("screen_wifi", Context.MODE_PRIVATE); + SharedPreferences screen_other = context.getSharedPreferences("screen_other", Context.MODE_PRIVATE); + + Map punused = unused.getAll(); + SharedPreferences.Editor edit_screen_wifi = screen_wifi.edit(); + SharedPreferences.Editor edit_screen_other = screen_other.edit(); + for (String key : punused.keySet()) { + edit_screen_wifi.putBoolean(key, (Boolean) punused.get(key)); + edit_screen_other.putBoolean(key, (Boolean) punused.get(key)); + } + edit_screen_wifi.apply(); + edit_screen_other.apply(); + + } else if (oldVersion <= 2017032112) + editor.remove("ip6"); + + } else { + Log.i(TAG, "Initializing sdk=" + Build.VERSION.SDK_INT); + editor.putBoolean("filter_udp", true); + editor.putBoolean("whitelist_wifi", false); + editor.putBoolean("whitelist_other", false); + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.LOLLIPOP) + editor.putBoolean("filter", true); // Optional + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + editor.putBoolean("filter", true); // Mandatory + + if (!Util.canFilter(context)) { + editor.putBoolean("log_app", false); + editor.putBoolean("filter", false); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + editor.remove("show_top"); + if ("data".equals(prefs.getString("sort", "name"))) + editor.remove("sort"); + } + + if (Util.isPlayStoreInstall(context)) { + editor.remove("update_check"); + editor.remove("use_hosts"); + editor.remove("hosts_url"); + } + + if (!Util.isDebuggable(context)) + editor.remove("loglevel"); + + editor.putInt("version", newVersion); + editor.apply(); + } + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ReceiverPackageRemoved.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ReceiverPackageRemoved.java new file mode 100644 index 0000000..d8ebf9a --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ReceiverPackageRemoved.java @@ -0,0 +1,50 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import androidx.core.app.NotificationManagerCompat; + +public class ReceiverPackageRemoved extends BroadcastReceiver { + private static final String TAG = "NetGuard.Receiver"; + + @Override + public void onReceive(final Context context, Intent intent) { + Log.i(TAG, "Received " + intent); + Util.logExtras(intent); + + String action = (intent == null ? null : intent.getAction()); + if (Intent.ACTION_PACKAGE_FULLY_REMOVED.equals(action)) { + int uid = intent.getIntExtra(Intent.EXTRA_UID, 0); + if (uid > 0) { + DatabaseHelper dh = DatabaseHelper.getInstance(context); + dh.clearLog(uid); + dh.clearAccess(uid, false); + + NotificationManagerCompat.from(context).cancel(uid); // installed notification + NotificationManagerCompat.from(context).cancel(uid + 10000); // access notification + } + } + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ResourceRecord.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ResourceRecord.java new file mode 100644 index 0000000..0d689d1 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ResourceRecord.java @@ -0,0 +1,47 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class ResourceRecord { + public long Time; + public String QName; + public String AName; + public String Resource; + public int TTL; + + private static DateFormat formatter = SimpleDateFormat.getDateTimeInstance(); + + public ResourceRecord() { + } + + @Override + public String toString() { + return formatter.format(new Date(Time).getTime()) + + " Q " + QName + + " A " + AName + + " R " + Resource + + " TTL " + TTL + + " " + formatter.format(new Date(Time + TTL * 1000L).getTime()); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/Rule.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/Rule.java new file mode 100644 index 0000000..e2e2b6e --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/Rule.java @@ -0,0 +1,453 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.os.Build; +import android.os.Process; +import android.util.Log; + +import androidx.preference.PreferenceManager; + +import org.xmlpull.v1.XmlPullParser; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class Rule { + private static final String TAG = "NetGuard.Rule"; + + public int uid; + public String packageName; + public int icon; + public String name; + public String version; + public boolean system; + public boolean internet; + public boolean enabled; + public boolean pkg = true; + + public boolean wifi_default = false; + public boolean other_default = false; + public boolean screen_wifi_default = false; + public boolean screen_other_default = false; + public boolean roaming_default = false; + + public boolean wifi_blocked = false; + public boolean other_blocked = false; + public boolean screen_wifi = false; + public boolean screen_other = false; + public boolean roaming = false; + public boolean lockdown = false; + + public boolean apply = true; + public boolean notify = true; + + public boolean relateduids = false; + public String[] related = null; + + public long hosts; + public boolean changed; + + public boolean expanded = false; + + private static List cachePackageInfo = null; + private static Map cacheLabel = new HashMap<>(); + private static Map cacheSystem = new HashMap<>(); + private static Map cacheInternet = new HashMap<>(); + private static Map cacheEnabled = new HashMap<>(); + + private static List getPackages(Context context) { + if (cachePackageInfo == null) { + PackageManager pm = context.getPackageManager(); + cachePackageInfo = pm.getInstalledPackages(0); + } + return new ArrayList<>(cachePackageInfo); + } + + private static String getLabel(PackageInfo info, Context context) { + if (!cacheLabel.containsKey(info)) { + PackageManager pm = context.getPackageManager(); + cacheLabel.put(info, info.applicationInfo.loadLabel(pm).toString()); + } + return cacheLabel.get(info); + } + + private static boolean isSystem(String packageName, Context context) { + if (!cacheSystem.containsKey(packageName)) + cacheSystem.put(packageName, Util.isSystem(packageName, context)); + return cacheSystem.get(packageName); + } + + private static boolean hasInternet(String packageName, Context context) { + if (!cacheInternet.containsKey(packageName)) + cacheInternet.put(packageName, Util.hasInternet(packageName, context)); + return cacheInternet.get(packageName); + } + + private static boolean isEnabled(PackageInfo info, Context context) { + if (!cacheEnabled.containsKey(info)) + cacheEnabled.put(info, Util.isEnabled(info, context)); + return cacheEnabled.get(info); + } + + public static void clearCache(Context context) { + Log.i(TAG, "Clearing cache"); + synchronized (context.getApplicationContext()) { + cachePackageInfo = null; + cacheLabel.clear(); + cacheSystem.clear(); + cacheInternet.clear(); + cacheEnabled.clear(); + } + + DatabaseHelper dh = DatabaseHelper.getInstance(context); + dh.clearApps(); + } + + private Rule(DatabaseHelper dh, PackageInfo info, Context context) { + this.uid = info.applicationInfo.uid; + this.packageName = info.packageName; + this.icon = info.applicationInfo.icon; + this.version = info.versionName; + if (info.applicationInfo.uid == 0) { + this.name = context.getString(R.string.title_root); + this.system = true; + this.internet = true; + this.enabled = true; + this.pkg = false; + } else if (info.applicationInfo.uid == 1013) { + this.name = context.getString(R.string.title_mediaserver); + this.system = true; + this.internet = true; + this.enabled = true; + this.pkg = false; + } else if (info.applicationInfo.uid == 1020) { + this.name = "MulticastDNSResponder"; + this.system = true; + this.internet = true; + this.enabled = true; + this.pkg = false; + } else if (info.applicationInfo.uid == 1021) { + this.name = context.getString(R.string.title_gpsdaemon); + this.system = true; + this.internet = true; + this.enabled = true; + this.pkg = false; + } else if (info.applicationInfo.uid == 1051) { + this.name = context.getString(R.string.title_dnsdaemon); + this.system = true; + this.internet = true; + this.enabled = true; + this.pkg = false; + } else if (info.applicationInfo.uid == 9999) { + this.name = context.getString(R.string.title_nobody); + this.system = true; + this.internet = true; + this.enabled = true; + this.pkg = false; + } else { + Cursor cursor = null; + try { + cursor = dh.getApp(this.packageName); + if (cursor.moveToNext()) { + this.name = cursor.getString(cursor.getColumnIndex("label")); + this.system = cursor.getInt(cursor.getColumnIndex("system")) > 0; + this.internet = cursor.getInt(cursor.getColumnIndex("internet")) > 0; + this.enabled = cursor.getInt(cursor.getColumnIndex("enabled")) > 0; + } else { + this.name = getLabel(info, context); + this.system = isSystem(info.packageName, context); + this.internet = hasInternet(info.packageName, context); + this.enabled = isEnabled(info, context); + + dh.addApp(this.packageName, this.name, this.system, this.internet, this.enabled); + } + } finally { + if (cursor != null) + cursor.close(); + } + } + } + + public static List getRules(final boolean all, Context context) { + synchronized (context.getApplicationContext()) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences wifi = context.getSharedPreferences("wifi", Context.MODE_PRIVATE); + SharedPreferences other = context.getSharedPreferences("other", Context.MODE_PRIVATE); + SharedPreferences screen_wifi = context.getSharedPreferences("screen_wifi", Context.MODE_PRIVATE); + SharedPreferences screen_other = context.getSharedPreferences("screen_other", Context.MODE_PRIVATE); + SharedPreferences roaming = context.getSharedPreferences("roaming", Context.MODE_PRIVATE); + SharedPreferences lockdown = context.getSharedPreferences("lockdown", Context.MODE_PRIVATE); + SharedPreferences apply = context.getSharedPreferences("apply", Context.MODE_PRIVATE); + SharedPreferences notify = context.getSharedPreferences("notify", Context.MODE_PRIVATE); + + // Get settings + boolean default_wifi = prefs.getBoolean("whitelist_wifi", true); + boolean default_other = prefs.getBoolean("whitelist_other", true); + boolean default_screen_wifi = prefs.getBoolean("screen_wifi", false); + boolean default_screen_other = prefs.getBoolean("screen_other", false); + boolean default_roaming = prefs.getBoolean("whitelist_roaming", true); + + boolean manage_system = prefs.getBoolean("manage_system", false); + boolean screen_on = prefs.getBoolean("screen_on", true); + boolean show_user = prefs.getBoolean("show_user", true); + boolean show_system = prefs.getBoolean("show_system", false); + boolean show_nointernet = prefs.getBoolean("show_nointernet", true); + boolean show_disabled = prefs.getBoolean("show_disabled", true); + + default_screen_wifi = default_screen_wifi && screen_on; + default_screen_other = default_screen_other && screen_on; + + // Get predefined rules + Map pre_wifi_blocked = new HashMap<>(); + Map pre_other_blocked = new HashMap<>(); + Map pre_roaming = new HashMap<>(); + Map pre_related = new HashMap<>(); + Map pre_system = new HashMap<>(); + try { + XmlResourceParser xml = context.getResources().getXml(R.xml.predefined); + int eventType = xml.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) + if ("wifi".equals(xml.getName())) { + String pkg = xml.getAttributeValue(null, "package"); + boolean pblocked = xml.getAttributeBooleanValue(null, "blocked", false); + pre_wifi_blocked.put(pkg, pblocked); + + } else if ("other".equals(xml.getName())) { + String pkg = xml.getAttributeValue(null, "package"); + boolean pblocked = xml.getAttributeBooleanValue(null, "blocked", false); + boolean proaming = xml.getAttributeBooleanValue(null, "roaming", default_roaming); + pre_other_blocked.put(pkg, pblocked); + pre_roaming.put(pkg, proaming); + + } else if ("relation".equals(xml.getName())) { + String pkg = xml.getAttributeValue(null, "package"); + String[] rel = xml.getAttributeValue(null, "related").split(","); + pre_related.put(pkg, rel); + + } else if ("type".equals(xml.getName())) { + String pkg = xml.getAttributeValue(null, "package"); + boolean system = xml.getAttributeBooleanValue(null, "system", true); + pre_system.put(pkg, system); + } + + + eventType = xml.next(); + } + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + // Build rule list + List listRules = new ArrayList<>(); + List listPI = getPackages(context); + + int userId = Process.myUid() / 100000; + + // Add root + PackageInfo root = new PackageInfo(); + root.packageName = "root"; + root.versionCode = Build.VERSION.SDK_INT; + root.versionName = Build.VERSION.RELEASE; + root.applicationInfo = new ApplicationInfo(); + root.applicationInfo.uid = 0; + root.applicationInfo.icon = 0; + listPI.add(root); + + // Add mediaserver + PackageInfo media = new PackageInfo(); + media.packageName = "android.media"; + media.versionCode = Build.VERSION.SDK_INT; + media.versionName = Build.VERSION.RELEASE; + media.applicationInfo = new ApplicationInfo(); + media.applicationInfo.uid = 1013 + userId * 100000; + media.applicationInfo.icon = 0; + listPI.add(media); + + // MulticastDNSResponder + PackageInfo mdr = new PackageInfo(); + mdr.packageName = "android.multicast"; + mdr.versionCode = Build.VERSION.SDK_INT; + mdr.versionName = Build.VERSION.RELEASE; + mdr.applicationInfo = new ApplicationInfo(); + mdr.applicationInfo.uid = 1020 + userId * 100000; + mdr.applicationInfo.icon = 0; + listPI.add(mdr); + + // Add GPS daemon + PackageInfo gps = new PackageInfo(); + gps.packageName = "android.gps"; + gps.versionCode = Build.VERSION.SDK_INT; + gps.versionName = Build.VERSION.RELEASE; + gps.applicationInfo = new ApplicationInfo(); + gps.applicationInfo.uid = 1021 + userId * 100000; + gps.applicationInfo.icon = 0; + listPI.add(gps); + + // Add DNS daemon + PackageInfo dns = new PackageInfo(); + dns.packageName = "android.dns"; + dns.versionCode = Build.VERSION.SDK_INT; + dns.versionName = Build.VERSION.RELEASE; + dns.applicationInfo = new ApplicationInfo(); + dns.applicationInfo.uid = 1051 + userId * 100000; + dns.applicationInfo.icon = 0; + listPI.add(dns); + + // Add nobody + PackageInfo nobody = new PackageInfo(); + nobody.packageName = "nobody"; + nobody.versionCode = Build.VERSION.SDK_INT; + nobody.versionName = Build.VERSION.RELEASE; + nobody.applicationInfo = new ApplicationInfo(); + nobody.applicationInfo.uid = 9999; + nobody.applicationInfo.icon = 0; + listPI.add(nobody); + + DatabaseHelper dh = DatabaseHelper.getInstance(context); + for (PackageInfo info : listPI) + try { + // Skip self + if (info.applicationInfo.uid == Process.myUid()) + continue; + + Rule rule = new Rule(dh, info, context); + + if (pre_system.containsKey(info.packageName)) + rule.system = pre_system.get(info.packageName); + if (info.applicationInfo.uid == Process.myUid()) + rule.system = true; + + if (all || + ((rule.system ? show_system : show_user) && + (show_nointernet || rule.internet) && + (show_disabled || rule.enabled))) { + + rule.wifi_default = (pre_wifi_blocked.containsKey(info.packageName) ? pre_wifi_blocked.get(info.packageName) : default_wifi); + rule.other_default = (pre_other_blocked.containsKey(info.packageName) ? pre_other_blocked.get(info.packageName) : default_other); + rule.screen_wifi_default = default_screen_wifi; + rule.screen_other_default = default_screen_other; + rule.roaming_default = (pre_roaming.containsKey(info.packageName) ? pre_roaming.get(info.packageName) : default_roaming); + + rule.wifi_blocked = (!(rule.system && !manage_system) && wifi.getBoolean(info.packageName, rule.wifi_default)); + rule.other_blocked = (!(rule.system && !manage_system) && other.getBoolean(info.packageName, rule.other_default)); + rule.screen_wifi = screen_wifi.getBoolean(info.packageName, rule.screen_wifi_default) && screen_on; + rule.screen_other = screen_other.getBoolean(info.packageName, rule.screen_other_default) && screen_on; + rule.roaming = roaming.getBoolean(info.packageName, rule.roaming_default); + rule.lockdown = lockdown.getBoolean(info.packageName, false); + + rule.apply = apply.getBoolean(info.packageName, true); + rule.notify = notify.getBoolean(info.packageName, true); + + // Related packages + List listPkg = new ArrayList<>(); + if (pre_related.containsKey(info.packageName)) + listPkg.addAll(Arrays.asList(pre_related.get(info.packageName))); + for (PackageInfo pi : listPI) + if (pi.applicationInfo.uid == rule.uid && !pi.packageName.equals(rule.packageName)) { + rule.relateduids = true; + listPkg.add(pi.packageName); + } + rule.related = listPkg.toArray(new String[0]); + + rule.hosts = dh.getHostCount(rule.uid, true); + + rule.updateChanged(default_wifi, default_other, default_roaming); + + listRules.add(rule); + } + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + // Sort rule list + final Collator collator = Collator.getInstance(Locale.getDefault()); + collator.setStrength(Collator.SECONDARY); // Case insensitive, process accents etc + + String sort = prefs.getString("sort", "name"); + if ("uid".equals(sort)) + Collections.sort(listRules, new Comparator() { + @Override + public int compare(Rule rule, Rule other) { + if (rule.uid < other.uid) + return -1; + else if (rule.uid > other.uid) + return 1; + else { + int i = collator.compare(rule.name, other.name); + return (i == 0 ? rule.packageName.compareTo(other.packageName) : i); + } + } + }); + else + Collections.sort(listRules, new Comparator() { + @Override + public int compare(Rule rule, Rule other) { + if (all || rule.changed == other.changed) { + int i = collator.compare(rule.name, other.name); + return (i == 0 ? rule.packageName.compareTo(other.packageName) : i); + } + return (rule.changed ? -1 : 1); + } + }); + + return listRules; + } + } + + private void updateChanged(boolean default_wifi, boolean default_other, boolean default_roaming) { + changed = (wifi_blocked != default_wifi || + (other_blocked != default_other) || + (wifi_blocked && screen_wifi != screen_wifi_default) || + (other_blocked && screen_other != screen_other_default) || + ((!other_blocked || screen_other) && roaming != default_roaming) || + hosts > 0 || lockdown || !apply); + } + + public void updateChanged(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean screen_on = prefs.getBoolean("screen_on", false); + boolean default_wifi = prefs.getBoolean("whitelist_wifi", true) && screen_on; + boolean default_other = prefs.getBoolean("whitelist_other", true) && screen_on; + boolean default_roaming = prefs.getBoolean("whitelist_roaming", true); + updateChanged(default_wifi, default_other, default_roaming); + } + + @Override + public String toString() { + // This is used in the port forwarding dialog application selector + return this.name; + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceExternal.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceExternal.java new file mode 100644 index 0000000..3330548 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceExternal.java @@ -0,0 +1,146 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.app.IntentService; +import android.app.Notification; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; + +import androidx.core.app.NotificationCompat; +import androidx.preference.PreferenceManager; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.net.URLConnection; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class ServiceExternal extends IntentService { + private static final String TAG = "NetGuard.External"; + private static final String ACTION_DOWNLOAD_HOSTS_FILE = "eu.faircode.netguard.DOWNLOAD_HOSTS_FILE"; + + // am startservice -a eu.faircode.netguard.DOWNLOAD_HOSTS_FILE + + public ServiceExternal() { + super(TAG); + } + + @Override + protected void onHandleIntent(Intent intent) { + try { + startForeground(ServiceSinkhole.NOTIFY_EXTERNAL, getForegroundNotification(this)); + + Log.i(TAG, "Received " + intent); + Util.logExtras(intent); + + if (ACTION_DOWNLOAD_HOSTS_FILE.equals(intent.getAction())) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + + String hosts_url = prefs.getString("hosts_url", null); + if ("https://www.netguard.me/hosts".equals(hosts_url)) + hosts_url = BuildConfig.HOSTS_FILE_URI; + + File tmp = new File(getFilesDir(), "hosts.tmp"); + File hosts = new File(getFilesDir(), "hosts.txt"); + + InputStream in = null; + OutputStream out = null; + URLConnection connection = null; + try { + URL url = new URL(hosts_url); + connection = url.openConnection(); + connection.connect(); + + if (connection instanceof HttpURLConnection) { + HttpURLConnection httpConnection = (HttpURLConnection) connection; + if (httpConnection.getResponseCode() != HttpURLConnection.HTTP_OK) + throw new IOException(httpConnection.getResponseCode() + " " + httpConnection.getResponseMessage()); + } + + int contentLength = connection.getContentLength(); + Log.i(TAG, "Content length=" + contentLength); + in = connection.getInputStream(); + out = new FileOutputStream(tmp); + + long size = 0; + byte buffer[] = new byte[4096]; + int bytes; + while ((bytes = in.read(buffer)) != -1) { + out.write(buffer, 0, bytes); + size += bytes; + } + + Log.i(TAG, "Downloaded size=" + size); + + if (hosts.exists()) + hosts.delete(); + tmp.renameTo(hosts); + + String last = SimpleDateFormat.getDateTimeInstance().format(new Date().getTime()); + prefs.edit().putString("hosts_last_download", last).apply(); + + ServiceSinkhole.reload("hosts file download", this, false); + + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + + if (tmp.exists()) + tmp.delete(); + } finally { + try { + if (out != null) + out.close(); + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + try { + if (in != null) + in.close(); + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + if (connection instanceof HttpURLConnection) + ((HttpURLConnection) connection).disconnect(); + } + } + } finally { + stopForeground(true); + } + } + + private static Notification getForegroundNotification(Context context) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, "foreground"); + builder.setSmallIcon(R.drawable.ic_hourglass_empty_white_24dp); + builder.setPriority(NotificationCompat.PRIORITY_MIN); + builder.setCategory(NotificationCompat.CATEGORY_STATUS); + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + builder.setContentTitle(context.getString(R.string.app_name)); + return builder.build(); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceSinkhole.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceSinkhole.java new file mode 100644 index 0000000..865d97a --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceSinkhole.java @@ -0,0 +1,3335 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.annotation.TargetApi; +import android.app.AlarmManager; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.Typeface; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkRequest; +import android.net.TrafficStats; +import android.net.Uri; +import android.net.VpnService; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.os.ParcelFileDescriptor; +import android.os.PowerManager; +import android.os.Process; +import android.os.SystemClock; +import android.provider.Settings; +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.util.Log; +import android.util.Pair; +import android.util.TypedValue; +import android.widget.RemoteViews; + +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; +import androidx.core.content.ContextCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.preference.PreferenceManager; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.math.BigInteger; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.URL; +import java.net.UnknownHostException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import javax.net.ssl.HttpsURLConnection; + +public class ServiceSinkhole extends VpnService implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = "NetGuard.Service"; + + private boolean registeredUser = false; + private boolean registeredIdleState = false; + private boolean registeredConnectivityChanged = false; + private boolean registeredPackageChanged = false; + + private boolean phone_state = false; + private Object networkCallback = null; + + private boolean registeredInteractiveState = false; + private PhoneStateListener callStateListener = null; + + private State state = State.none; + private boolean user_foreground = true; + private boolean last_connected = false; + private boolean last_metered = true; + private boolean last_interactive = false; + + private int last_allowed = -1; + private int last_blocked = -1; + private int last_hosts = -1; + + private static Object jni_lock = new Object(); + private static long jni_context = 0; + private Thread tunnelThread = null; + private ServiceSinkhole.Builder last_builder = null; + private ParcelFileDescriptor vpn = null; + private boolean temporarilyStopped = false; + + private long last_hosts_modified = 0; + private Map mapHostsBlocked = new HashMap<>(); + private Map mapUidAllowed = new HashMap<>(); + private Map mapUidKnown = new HashMap<>(); + private final Map> mapUidIPFilters = new HashMap<>(); + private Map mapForward = new HashMap<>(); + private Map mapNotify = new HashMap<>(); + private ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); + + private volatile Looper commandLooper; + private volatile Looper logLooper; + private volatile Looper statsLooper; + private volatile CommandHandler commandHandler; + private volatile LogHandler logHandler; + private volatile StatsHandler statsHandler; + + private static final int NOTIFY_ENFORCING = 1; + private static final int NOTIFY_WAITING = 2; + private static final int NOTIFY_DISABLED = 3; + private static final int NOTIFY_LOCKDOWN = 4; + private static final int NOTIFY_AUTOSTART = 5; + private static final int NOTIFY_ERROR = 6; + private static final int NOTIFY_TRAFFIC = 7; + private static final int NOTIFY_UPDATE = 8; + public static final int NOTIFY_EXTERNAL = 9; + public static final int NOTIFY_DOWNLOAD = 10; + + public static final String EXTRA_COMMAND = "Command"; + private static final String EXTRA_REASON = "Reason"; + public static final String EXTRA_NETWORK = "Network"; + public static final String EXTRA_UID = "UID"; + public static final String EXTRA_PACKAGE = "Package"; + public static final String EXTRA_BLOCKED = "Blocked"; + public static final String EXTRA_INTERACTIVE = "Interactive"; + public static final String EXTRA_TEMPORARY = "Temporary"; + + private static final int MSG_STATS_START = 1; + private static final int MSG_STATS_STOP = 2; + private static final int MSG_STATS_UPDATE = 3; + private static final int MSG_PACKET = 4; + private static final int MSG_USAGE = 5; + + private enum State {none, waiting, enforcing, stats} + + public enum Command {run, start, reload, stop, stats, set, householding, watchdog} + + private static volatile PowerManager.WakeLock wlInstance = null; + + private ExecutorService executor = Executors.newCachedThreadPool(); + + private static final String ACTION_HOUSE_HOLDING = "eu.faircode.netguard.HOUSE_HOLDING"; + private static final String ACTION_SCREEN_OFF_DELAYED = "eu.faircode.netguard.SCREEN_OFF_DELAYED"; + private static final String ACTION_WATCHDOG = "eu.faircode.netguard.WATCHDOG"; + + private native long jni_init(int sdk); + + private native void jni_start(long context, int loglevel); + + private native void jni_run(long context, int tun, boolean fwd53, int rcode); + + private native void jni_stop(long context); + + private native void jni_clear(long context); + + private native int jni_get_mtu(); + + private native int[] jni_get_stats(long context); + + private static native void jni_pcap(String name, int record_size, int file_size); + + private native void jni_socks5(String addr, int port, String username, String password); + + private native void jni_done(long context); + + public static void setPcap(boolean enabled, Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + int record_size = 64; + try { + String r = prefs.getString("pcap_record_size", null); + if (TextUtils.isEmpty(r)) + r = "64"; + record_size = Integer.parseInt(r); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + int file_size = 2 * 1024 * 1024; + try { + String f = prefs.getString("pcap_file_size", null); + if (TextUtils.isEmpty(f)) + f = "2"; + file_size = Integer.parseInt(f) * 1024 * 1024; + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + File pcap = (enabled ? new File(context.getDir("data", MODE_PRIVATE), "netguard.pcap") : null); + jni_pcap(pcap == null ? null : pcap.getAbsolutePath(), record_size, file_size); + } + + synchronized private static PowerManager.WakeLock getLock(Context context) { + if (wlInstance == null) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + wlInstance = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, context.getString(R.string.app_name) + " wakelock"); + wlInstance.setReferenceCounted(true); + } + return wlInstance; + } + + synchronized private static void releaseLock(Context context) { + if (wlInstance != null) { + while (wlInstance.isHeld()) + wlInstance.release(); + wlInstance = null; + } + } + + private final class CommandHandler extends Handler { + public int queue = 0; + + public CommandHandler(Looper looper) { + super(looper); + } + + private void reportQueueSize() { + Intent ruleset = new Intent(ActivityMain.ACTION_QUEUE_CHANGED); + ruleset.putExtra(ActivityMain.EXTRA_SIZE, queue); + LocalBroadcastManager.getInstance(ServiceSinkhole.this).sendBroadcast(ruleset); + } + + public void queue(Intent intent) { + synchronized (this) { + queue++; + reportQueueSize(); + } + Command cmd = (Command) intent.getSerializableExtra(EXTRA_COMMAND); + Message msg = commandHandler.obtainMessage(); + msg.obj = intent; + msg.what = cmd.ordinal(); + commandHandler.sendMessage(msg); + } + + @Override + public void handleMessage(Message msg) { + try { + synchronized (ServiceSinkhole.this) { + handleIntent((Intent) msg.obj); + } + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } finally { + synchronized (this) { + queue--; + reportQueueSize(); + } + try { + PowerManager.WakeLock wl = getLock(ServiceSinkhole.this); + if (wl.isHeld()) + wl.release(); + else + Log.w(TAG, "Wakelock under-locked"); + Log.i(TAG, "Messages=" + hasMessages(0) + " wakelock=" + wlInstance.isHeld()); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + } + + private void handleIntent(Intent intent) { + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + + Command cmd = (Command) intent.getSerializableExtra(EXTRA_COMMAND); + String reason = intent.getStringExtra(EXTRA_REASON); + Log.i(TAG, "Executing intent=" + intent + " command=" + cmd + " reason=" + reason + + " vpn=" + (vpn != null) + " user=" + (Process.myUid() / 100000)); + + // Check if foreground + if (cmd != Command.stop) + if (!user_foreground) { + Log.i(TAG, "Command " + cmd + " ignored for background user"); + return; + } + + // Handle temporary stop + if (cmd == Command.stop) + temporarilyStopped = intent.getBooleanExtra(EXTRA_TEMPORARY, false); + else if (cmd == Command.start) + temporarilyStopped = false; + else if (cmd == Command.reload && temporarilyStopped) { + // Prevent network/interactive changes from restarting the VPN + Log.i(TAG, "Command " + cmd + " ignored because of temporary stop"); + return; + } + + // Optionally listen for interactive state changes + if (prefs.getBoolean("screen_on", true)) { + if (!registeredInteractiveState) { + Log.i(TAG, "Starting listening for interactive state changes"); + last_interactive = Util.isInteractive(ServiceSinkhole.this); + IntentFilter ifInteractive = new IntentFilter(); + ifInteractive.addAction(Intent.ACTION_SCREEN_ON); + ifInteractive.addAction(Intent.ACTION_SCREEN_OFF); + ifInteractive.addAction(ACTION_SCREEN_OFF_DELAYED); + registerReceiver(interactiveStateReceiver, ifInteractive); + registeredInteractiveState = true; + } + } else { + if (registeredInteractiveState) { + Log.i(TAG, "Stopping listening for interactive state changes"); + unregisterReceiver(interactiveStateReceiver); + registeredInteractiveState = false; + last_interactive = false; + } + } + + // Optionally listen for call state changes + TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); + if (prefs.getBoolean("disable_on_call", false)) { + if (tm != null && callStateListener == null && Util.hasPhoneStatePermission(ServiceSinkhole.this)) { + Log.i(TAG, "Starting listening for call states"); + PhoneStateListener listener = new PhoneStateListener() { + @Override + public void onCallStateChanged(int state, String incomingNumber) { + Log.i(TAG, "New call state=" + state); + if (prefs.getBoolean("enabled", false)) + if (state == TelephonyManager.CALL_STATE_IDLE) + ServiceSinkhole.start("call state", ServiceSinkhole.this); + else + ServiceSinkhole.stop("call state", ServiceSinkhole.this, true); + } + }; + tm.listen(listener, PhoneStateListener.LISTEN_CALL_STATE); + callStateListener = listener; + } + } else { + if (tm != null && callStateListener != null) { + Log.i(TAG, "Stopping listening for call states"); + tm.listen(callStateListener, PhoneStateListener.LISTEN_NONE); + callStateListener = null; + } + } + + // Watchdog + if (cmd == Command.start || cmd == Command.reload || cmd == Command.stop) { + Intent watchdogIntent = new Intent(ServiceSinkhole.this, ServiceSinkhole.class); + watchdogIntent.setAction(ACTION_WATCHDOG); + PendingIntent pi; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + pi = PendingIntent.getForegroundService(ServiceSinkhole.this, 1, watchdogIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + else + pi = PendingIntent.getService(ServiceSinkhole.this, 1, watchdogIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + + AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + am.cancel(pi); + + if (cmd != Command.stop) { + int watchdog = Integer.parseInt(prefs.getString("watchdog", "0")); + if (watchdog > 0) { + Log.i(TAG, "Watchdog " + watchdog + " minutes"); + am.setInexactRepeating(AlarmManager.RTC, SystemClock.elapsedRealtime() + watchdog * 60 * 1000, watchdog * 60 * 1000, pi); + } + } + } + + try { + switch (cmd) { + case run: + break; + + case start: + start(); + break; + + case reload: + reload(intent.getBooleanExtra(EXTRA_INTERACTIVE, false)); + break; + + case stop: + stop(temporarilyStopped); + break; + + case stats: + statsHandler.sendEmptyMessage(MSG_STATS_STOP); + statsHandler.sendEmptyMessage(MSG_STATS_START); + break; + + case householding: + householding(intent); + break; + + case watchdog: + watchdog(intent); + break; + + default: + Log.e(TAG, "Unknown command=" + cmd); + } + + if (cmd == Command.start || cmd == Command.reload) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + boolean filter = prefs.getBoolean("filter", false); + if (filter && isLockdownEnabled()) + showLockdownNotification(); + else + removeLockdownNotification(); + } + } + + if (cmd == Command.start || cmd == Command.reload || cmd == Command.stop) { + // Update main view + Intent ruleset = new Intent(ActivityMain.ACTION_RULES_CHANGED); + ruleset.putExtra(ActivityMain.EXTRA_CONNECTED, cmd == Command.stop ? false : last_connected); + ruleset.putExtra(ActivityMain.EXTRA_METERED, cmd == Command.stop ? false : last_metered); + LocalBroadcastManager.getInstance(ServiceSinkhole.this).sendBroadcast(ruleset); + + // Update widgets + WidgetMain.updateWidgets(ServiceSinkhole.this); + } + + // Stop service if needed + if (!commandHandler.hasMessages(Command.start.ordinal()) && + !commandHandler.hasMessages(Command.reload.ordinal()) && + !prefs.getBoolean("enabled", false) && + !prefs.getBoolean("show_stats", false)) + stopForeground(true); + + // Request garbage collection + System.gc(); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + + if (cmd == Command.start || cmd == Command.reload) { + if (VpnService.prepare(ServiceSinkhole.this) == null) { + Log.w(TAG, "VPN prepared connected=" + last_connected); + if (last_connected && !(ex instanceof StartFailedException)) { + //showAutoStartNotification(); + if (!Util.isPlayStoreInstall(ServiceSinkhole.this)) + showErrorNotification(ex.toString()); + } + // Retried on connectivity change + } else { + showErrorNotification(ex.toString()); + + // Disable firewall + if (!(ex instanceof StartFailedException)) { + prefs.edit().putBoolean("enabled", false).apply(); + WidgetMain.updateWidgets(ServiceSinkhole.this); + } + } + } else + showErrorNotification(ex.toString()); + } + } + + private void start() { + if (vpn == null) { + if (state != State.none) { + Log.d(TAG, "Stop foreground state=" + state.toString()); + stopForeground(true); + } + startForeground(NOTIFY_ENFORCING, getEnforcingNotification(-1, -1, -1)); + state = State.enforcing; + Log.d(TAG, "Start foreground state=" + state.toString()); + + List listRule = Rule.getRules(true, ServiceSinkhole.this); + List listAllowed = getAllowedRules(listRule); + + last_builder = getBuilder(listAllowed, listRule); + vpn = startVPN(last_builder); + if (vpn == null) + throw new StartFailedException(getString((R.string.msg_start_failed))); + + startNative(vpn, listAllowed, listRule); + + removeWarningNotifications(); + updateEnforcingNotification(listAllowed.size(), listRule.size()); + } + } + + private void reload(boolean interactive) { + List listRule = Rule.getRules(true, ServiceSinkhole.this); + + // Check if rules needs to be reloaded + if (interactive) { + boolean process = false; + for (Rule rule : listRule) { + boolean blocked = (last_metered ? rule.other_blocked : rule.wifi_blocked); + boolean screen = (last_metered ? rule.screen_other : rule.screen_wifi); + if (blocked && screen) { + process = true; + break; + } + } + if (!process) { + Log.i(TAG, "No changed rules on interactive state change"); + return; + } + } + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + + if (state != State.enforcing) { + if (state != State.none) { + Log.d(TAG, "Stop foreground state=" + state.toString()); + stopForeground(true); + } + startForeground(NOTIFY_ENFORCING, getEnforcingNotification(-1, -1, -1)); + state = State.enforcing; + Log.d(TAG, "Start foreground state=" + state.toString()); + } + + List listAllowed = getAllowedRules(listRule); + ServiceSinkhole.Builder builder = getBuilder(listAllowed, listRule); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) { + last_builder = builder; + Log.i(TAG, "Legacy restart"); + + if (vpn != null) { + stopNative(vpn); + stopVPN(vpn); + vpn = null; + try { + Thread.sleep(500); + } catch (InterruptedException ignored) { + } + } + vpn = startVPN(last_builder); + + } else { + if (vpn != null && prefs.getBoolean("filter", false) && builder.equals(last_builder)) { + Log.i(TAG, "Native restart"); + stopNative(vpn); + + } else { + last_builder = builder; + + boolean handover = prefs.getBoolean("handover", false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + handover = false; + Log.i(TAG, "VPN restart handover=" + handover); + + if (handover) { + // Attempt seamless handover + ParcelFileDescriptor prev = vpn; + vpn = startVPN(builder); + + if (prev != null && vpn == null) { + Log.w(TAG, "Handover failed"); + stopNative(prev); + stopVPN(prev); + prev = null; + try { + Thread.sleep(3000); + } catch (InterruptedException ignored) { + } + vpn = startVPN(last_builder); + if (vpn == null) + throw new IllegalStateException("Handover failed"); + } + + if (prev != null) { + stopNative(prev); + stopVPN(prev); + } + } else { + if (vpn != null) { + stopNative(vpn); + stopVPN(vpn); + } + + vpn = startVPN(builder); + } + } + } + + if (vpn == null) + throw new StartFailedException(getString((R.string.msg_start_failed))); + + startNative(vpn, listAllowed, listRule); + + removeWarningNotifications(); + updateEnforcingNotification(listAllowed.size(), listRule.size()); + } + + private void stop(boolean temporary) { + if (vpn != null) { + stopNative(vpn); + stopVPN(vpn); + vpn = null; + unprepare(); + } + if (state == State.enforcing && !temporary) { + Log.d(TAG, "Stop foreground state=" + state.toString()); + last_allowed = -1; + last_blocked = -1; + last_hosts = -1; + + stopForeground(true); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + if (prefs.getBoolean("show_stats", false)) { + startForeground(NOTIFY_WAITING, getWaitingNotification()); + state = State.waiting; + Log.d(TAG, "Start foreground state=" + state.toString()); + } else { + state = State.none; + stopSelf(); + } + } + } + + private void householding(Intent intent) { + // Keep log records for three days + DatabaseHelper.getInstance(ServiceSinkhole.this).cleanupLog(new Date().getTime() - 3 * 24 * 3600 * 1000L); + + // Clear expired DNS records + DatabaseHelper.getInstance(ServiceSinkhole.this).cleanupDns(); + + // Check for update + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + if (!Util.isPlayStoreInstall(ServiceSinkhole.this) && + Util.hasValidFingerprint(ServiceSinkhole.this) && + prefs.getBoolean("update_check", true)) + checkUpdate(); + } + + private void watchdog(Intent intent) { + if (vpn == null) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + if (prefs.getBoolean("enabled", false)) { + Log.e(TAG, "Service was killed"); + start(); + } + } + } + + private void checkUpdate() { + StringBuilder json = new StringBuilder(); + HttpsURLConnection urlConnection = null; + try { + URL url = new URL(BuildConfig.GITHUB_LATEST_API); + urlConnection = (HttpsURLConnection) url.openConnection(); + BufferedReader br = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); + + String line; + while ((line = br.readLine()) != null) + json.append(line); + + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } finally { + if (urlConnection != null) + urlConnection.disconnect(); + } + + try { + JSONObject jroot = new JSONObject(json.toString()); + if (jroot.has("tag_name") && jroot.has("html_url") && jroot.has("assets")) { + String url = jroot.getString("html_url"); + JSONArray jassets = jroot.getJSONArray("assets"); + if (jassets.length() > 0) { + JSONObject jasset = jassets.getJSONObject(0); + if (jasset.has("name")) { + String version = jroot.getString("tag_name"); + String name = jasset.getString("name"); + Log.i(TAG, "Tag " + version + " name " + name + " url " + url); + + Version current = new Version(Util.getSelfVersionName(ServiceSinkhole.this)); + Version available = new Version(version); + if (current.compareTo(available) < 0) { + Log.i(TAG, "Update available from " + current + " to " + available); + showUpdateNotification(name, url); + } else + Log.i(TAG, "Up-to-date current version " + current); + } + } + } + } catch (JSONException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + + private class StartFailedException extends IllegalStateException { + public StartFailedException(String msg) { + super(msg); + } + } + } + + private final class LogHandler extends Handler { + public int queue = 0; + + private static final int MAX_QUEUE = 250; + + public LogHandler(Looper looper) { + super(looper); + } + + public void queue(Packet packet) { + Message msg = obtainMessage(); + msg.obj = packet; + msg.what = MSG_PACKET; + msg.arg1 = (last_connected ? (last_metered ? 2 : 1) : 0); + msg.arg2 = (last_interactive ? 1 : 0); + + synchronized (this) { + if (queue > MAX_QUEUE) { + Log.w(TAG, "Log queue full"); + return; + } + + sendMessage(msg); + + queue++; + } + } + + public void account(Usage usage) { + Message msg = obtainMessage(); + msg.obj = usage; + msg.what = MSG_USAGE; + + synchronized (this) { + if (queue > MAX_QUEUE) { + Log.w(TAG, "Log queue full"); + return; + } + + sendMessage(msg); + + queue++; + } + } + + @Override + public void handleMessage(Message msg) { + try { + switch (msg.what) { + case MSG_PACKET: + log((Packet) msg.obj, msg.arg1, msg.arg2 > 0); + break; + + case MSG_USAGE: + usage((Usage) msg.obj); + break; + + default: + Log.e(TAG, "Unknown log message=" + msg.what); + } + + synchronized (this) { + queue--; + } + + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + + private void log(Packet packet, int connection, boolean interactive) { + // Get settings + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + boolean log = prefs.getBoolean("log", false); + boolean log_app = prefs.getBoolean("log_app", false); + + DatabaseHelper dh = DatabaseHelper.getInstance(ServiceSinkhole.this); + + // Get real name + String dname = dh.getQName(packet.uid, packet.daddr); + + // Traffic log + if (log) + dh.insertLog(packet, dname, connection, interactive); + + // Application log + if (log_app && packet.uid >= 0 && + !(packet.uid == 0 && (packet.protocol == 6 || packet.protocol == 17) && packet.dport == 53)) { + if (!(packet.protocol == 6 /* TCP */ || packet.protocol == 17 /* UDP */)) + packet.dport = 0; + if (dh.updateAccess(packet, dname, -1)) { + lock.readLock().lock(); + if (!mapNotify.containsKey(packet.uid) || mapNotify.get(packet.uid)) + showAccessNotification(packet.uid); + lock.readLock().unlock(); + } + } + } + + private void usage(Usage usage) { + if (usage.Uid >= 0 && !(usage.Uid == 0 && usage.Protocol == 17 && usage.DPort == 53)) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + boolean filter = prefs.getBoolean("filter", false); + boolean log_app = prefs.getBoolean("log_app", false); + boolean track_usage = prefs.getBoolean("track_usage", false); + if (filter && log_app && track_usage) { + DatabaseHelper dh = DatabaseHelper.getInstance(ServiceSinkhole.this); + String dname = dh.getQName(usage.Uid, usage.DAddr); + Log.i(TAG, "Usage account " + usage + " dname=" + dname); + dh.updateUsage(usage, dname); + } + } + } + } + + private final class StatsHandler extends Handler { + private boolean stats = false; + private long when; + + private long t = -1; + private long tx = -1; + private long rx = -1; + + private List gt = new ArrayList<>(); + private List gtx = new ArrayList<>(); + private List grx = new ArrayList<>(); + + private HashMap mapUidBytes = new HashMap<>(); + + public StatsHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + try { + switch (msg.what) { + case MSG_STATS_START: + startStats(); + break; + + case MSG_STATS_STOP: + stopStats(); + break; + + case MSG_STATS_UPDATE: + updateStats(); + break; + + default: + Log.e(TAG, "Unknown stats message=" + msg.what); + } + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + + private void startStats() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + boolean enabled = (!stats && prefs.getBoolean("show_stats", false)); + Log.i(TAG, "Stats start enabled=" + enabled); + if (enabled) { + when = new Date().getTime(); + t = -1; + tx = -1; + rx = -1; + gt.clear(); + gtx.clear(); + grx.clear(); + mapUidBytes.clear(); + stats = true; + updateStats(); + } + } + + private void stopStats() { + Log.i(TAG, "Stats stop"); + stats = false; + this.removeMessages(MSG_STATS_UPDATE); + if (state == State.stats) { + Log.d(TAG, "Stop foreground state=" + state.toString()); + stopForeground(true); + state = State.none; + } else + NotificationManagerCompat.from(ServiceSinkhole.this).cancel(NOTIFY_TRAFFIC); + } + + private void updateStats() { + RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.traffic); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + long frequency = Long.parseLong(prefs.getString("stats_frequency", "1000")); + long samples = Long.parseLong(prefs.getString("stats_samples", "90")); + boolean filter = prefs.getBoolean("filter", false); + boolean show_top = prefs.getBoolean("show_top", false); + int loglevel = Integer.parseInt(prefs.getString("loglevel", Integer.toString(Log.WARN))); + + // Schedule next update + this.sendEmptyMessageDelayed(MSG_STATS_UPDATE, frequency); + + long ct = SystemClock.elapsedRealtime(); + + // Cleanup + while (gt.size() > 0 && ct - gt.get(0) > samples * 1000) { + gt.remove(0); + gtx.remove(0); + grx.remove(0); + } + + // Calculate network speed + float txsec = 0; + float rxsec = 0; + long ttx = TrafficStats.getTotalTxBytes(); + long trx = TrafficStats.getTotalRxBytes(); + if (filter) { + ttx -= TrafficStats.getUidTxBytes(Process.myUid()); + trx -= TrafficStats.getUidRxBytes(Process.myUid()); + if (ttx < 0) + ttx = 0; + if (trx < 0) + trx = 0; + } + if (t > 0 && tx > 0 && rx > 0) { + float dt = (ct - t) / 1000f; + txsec = (ttx - tx) / dt; + rxsec = (trx - rx) / dt; + gt.add(ct); + gtx.add(txsec); + grx.add(rxsec); + } + + // Calculate application speeds + if (show_top) { + if (mapUidBytes.size() == 0) { + for (ApplicationInfo ainfo : getPackageManager().getInstalledApplications(0)) + if (ainfo.uid != Process.myUid()) + mapUidBytes.put(ainfo.uid, TrafficStats.getUidTxBytes(ainfo.uid) + TrafficStats.getUidRxBytes(ainfo.uid)); + + } else if (t > 0) { + TreeMap mapSpeedUid = new TreeMap<>(new Comparator() { + @Override + public int compare(Float value, Float other) { + return -value.compareTo(other); + } + }); + float dt = (ct - t) / 1000f; + for (int uid : mapUidBytes.keySet()) { + long bytes = TrafficStats.getUidTxBytes(uid) + TrafficStats.getUidRxBytes(uid); + float speed = (bytes - mapUidBytes.get(uid)) / dt; + if (speed > 0) { + mapSpeedUid.put(speed, uid); + mapUidBytes.put(uid, bytes); + } + } + + StringBuilder sb = new StringBuilder(); + int i = 0; + for (float speed : mapSpeedUid.keySet()) { + if (i++ >= 3) + break; + if (speed < 1000 * 1000) + sb.append(getString(R.string.msg_kbsec, speed / 1000)); + else + sb.append(getString(R.string.msg_mbsec, speed / 1000 / 1000)); + sb.append(' '); + List apps = Util.getApplicationNames(mapSpeedUid.get(speed), ServiceSinkhole.this); + sb.append(apps.size() > 0 ? apps.get(0) : "?"); + sb.append("\r\n"); + } + if (sb.length() > 0) + sb.setLength(sb.length() - 2); + remoteViews.setTextViewText(R.id.tvTop, sb.toString()); + } + } + + t = ct; + tx = ttx; + rx = trx; + + // Create bitmap + int height = Util.dips2pixels(96, ServiceSinkhole.this); + int width = Util.dips2pixels(96 * 5, ServiceSinkhole.this); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + + // Create canvas + Canvas canvas = new Canvas(bitmap); + canvas.drawColor(Color.TRANSPARENT); + + // Determine max + float max = 0; + long xmax = 0; + float ymax = 0; + for (int i = 0; i < gt.size(); i++) { + long t = gt.get(i); + float tx = gtx.get(i); + float rx = grx.get(i); + if (t > xmax) + xmax = t; + if (tx > max) + max = tx; + if (rx > max) + max = rx; + if (tx > ymax) + ymax = tx; + if (rx > ymax) + ymax = rx; + } + + // Build paths + Path ptx = new Path(); + Path prx = new Path(); + for (int i = 0; i < gtx.size(); i++) { + float x = width - width * (xmax - gt.get(i)) / 1000f / samples; + float ytx = height - height * gtx.get(i) / ymax; + float yrx = height - height * grx.get(i) / ymax; + if (i == 0) { + ptx.moveTo(x, ytx); + prx.moveTo(x, yrx); + } else { + ptx.lineTo(x, ytx); + prx.lineTo(x, yrx); + } + } + + // Build paint + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.STROKE); + + // Draw scale line + paint.setStrokeWidth(Util.dips2pixels(1, ServiceSinkhole.this)); + paint.setColor(ContextCompat.getColor(ServiceSinkhole.this, R.color.colorGrayed)); + float y = height / 2; + canvas.drawLine(0, y, width, y, paint); + + // Draw paths + paint.setStrokeWidth(Util.dips2pixels(2, ServiceSinkhole.this)); + paint.setColor(ContextCompat.getColor(ServiceSinkhole.this, R.color.colorSend)); + canvas.drawPath(ptx, paint); + paint.setColor(ContextCompat.getColor(ServiceSinkhole.this, R.color.colorReceive)); + canvas.drawPath(prx, paint); + + // Update remote view + remoteViews.setImageViewBitmap(R.id.ivTraffic, bitmap); + if (txsec < 1000 * 1000) + remoteViews.setTextViewText(R.id.tvTx, getString(R.string.msg_kbsec, txsec / 1000)); + else + remoteViews.setTextViewText(R.id.tvTx, getString(R.string.msg_mbsec, txsec / 1000 / 1000)); + + if (rxsec < 1000 * 1000) + remoteViews.setTextViewText(R.id.tvRx, getString(R.string.msg_kbsec, rxsec / 1000)); + else + remoteViews.setTextViewText(R.id.tvRx, getString(R.string.msg_mbsec, rxsec / 1000 / 1000)); + + if (max < 1000 * 1000) + remoteViews.setTextViewText(R.id.tvMax, getString(R.string.msg_kbsec, max / 2 / 1000)); + else + remoteViews.setTextViewText(R.id.tvMax, getString(R.string.msg_mbsec, max / 2 / 1000 / 1000)); + + // Show session/file count + if (filter && loglevel <= Log.WARN) { + int[] count = jni_get_stats(jni_context); + remoteViews.setTextViewText(R.id.tvSessions, count[0] + "/" + count[1] + "/" + count[2]); + remoteViews.setTextViewText(R.id.tvFiles, count[3] + "/" + count[4]); + } else { + remoteViews.setTextViewText(R.id.tvSessions, ""); + remoteViews.setTextViewText(R.id.tvFiles, ""); + } + + // Show notification + Intent main = new Intent(ServiceSinkhole.this, ActivityMain.class); + PendingIntent pi = PendingIntent.getActivity(ServiceSinkhole.this, 0, main, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + + TypedValue tv = new TypedValue(); + getTheme().resolveAttribute(R.attr.colorPrimary, tv, true); + NotificationCompat.Builder builder = new NotificationCompat.Builder(ServiceSinkhole.this, "notify"); + builder.setWhen(when) + .setSmallIcon(R.drawable.ic_equalizer_white_24dp) + .setContent(remoteViews) + .setContentIntent(pi) + .setColor(tv.data) + .setOngoing(true) + .setAutoCancel(false); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + builder.setCategory(NotificationCompat.CATEGORY_STATUS) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); + + if (state == State.none || state == State.waiting) { + if (state != State.none) { + Log.d(TAG, "Stop foreground state=" + state.toString()); + stopForeground(true); + } + startForeground(NOTIFY_TRAFFIC, builder.build()); + state = State.stats; + Log.d(TAG, "Start foreground state=" + state.toString()); + } else + NotificationManagerCompat.from(ServiceSinkhole.this).notify(NOTIFY_TRAFFIC, builder.build()); + } + } + + public static List getDns(Context context) { + List listDns = new ArrayList<>(); + List sysDns = Util.getDefaultDNS(context); + + // Get custom DNS servers + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean ip6 = prefs.getBoolean("ip6", true); + boolean filter = prefs.getBoolean("filter", false); + String vpnDns1 = prefs.getString("dns", null); + String vpnDns2 = prefs.getString("dns2", null); + Log.i(TAG, "DNS system=" + TextUtils.join(",", sysDns) + " config=" + vpnDns1 + "," + vpnDns2); + + if (vpnDns1 != null) + try { + InetAddress dns = InetAddress.getByName(vpnDns1); + if (!(dns.isLoopbackAddress() || dns.isAnyLocalAddress()) && + (ip6 || dns instanceof Inet4Address)) + listDns.add(dns); + } catch (Throwable ignored) { + } + + if (vpnDns2 != null) + try { + InetAddress dns = InetAddress.getByName(vpnDns2); + if (!(dns.isLoopbackAddress() || dns.isAnyLocalAddress()) && + (ip6 || dns instanceof Inet4Address)) + listDns.add(dns); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + if (listDns.size() == 2) + return listDns; + + for (String def_dns : sysDns) + try { + InetAddress ddns = InetAddress.getByName(def_dns); + if (!listDns.contains(ddns) && + !(ddns.isLoopbackAddress() || ddns.isAnyLocalAddress()) && + (ip6 || ddns instanceof Inet4Address)) + listDns.add(ddns); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + // Remove local DNS servers when not routing LAN + int count = listDns.size(); + boolean lan = prefs.getBoolean("lan", false); + boolean use_hosts = prefs.getBoolean("use_hosts", false); + if (lan && use_hosts && filter) + try { + List> subnets = new ArrayList<>(); + subnets.add(new Pair<>(InetAddress.getByName("10.0.0.0"), 8)); + subnets.add(new Pair<>(InetAddress.getByName("172.16.0.0"), 12)); + subnets.add(new Pair<>(InetAddress.getByName("192.168.0.0"), 16)); + + for (Pair subnet : subnets) { + InetAddress hostAddress = subnet.first; + BigInteger host = new BigInteger(1, hostAddress.getAddress()); + + int prefix = subnet.second; + BigInteger mask = BigInteger.valueOf(-1).shiftLeft(hostAddress.getAddress().length * 8 - prefix); + + for (InetAddress dns : new ArrayList<>(listDns)) + if (hostAddress.getAddress().length == dns.getAddress().length) { + BigInteger ip = new BigInteger(1, dns.getAddress()); + + if (host.and(mask).equals(ip.and(mask))) { + Log.i(TAG, "Local DNS server host=" + hostAddress + "/" + prefix + " dns=" + dns); + listDns.remove(dns); + } + } + } + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + // Always set DNS servers + if (listDns.size() == 0 || listDns.size() < count) + try { + listDns.add(InetAddress.getByName("8.8.8.8")); + listDns.add(InetAddress.getByName("8.8.4.4")); + if (ip6) { + listDns.add(InetAddress.getByName("2001:4860:4860::8888")); + listDns.add(InetAddress.getByName("2001:4860:4860::8844")); + } + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + Log.i(TAG, "Get DNS=" + TextUtils.join(",", listDns)); + + return listDns; + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private ParcelFileDescriptor startVPN(Builder builder) throws SecurityException { + try { + ParcelFileDescriptor pfd = builder.establish(); + + // Set underlying network + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); + Network active = (cm == null ? null : cm.getActiveNetwork()); + if (active != null) { + Log.i(TAG, "Setting underlying network=" + cm.getNetworkInfo(active)); + setUnderlyingNetworks(new Network[]{active}); + } + } + + return pfd; + } catch (SecurityException ex) { + throw ex; + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + return null; + } + } + + private Builder getBuilder(List listAllowed, List listRule) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean subnet = prefs.getBoolean("subnet", false); + boolean tethering = prefs.getBoolean("tethering", false); + boolean lan = prefs.getBoolean("lan", false); + boolean ip6 = prefs.getBoolean("ip6", true); + boolean filter = prefs.getBoolean("filter", false); + boolean system = prefs.getBoolean("manage_system", false); + + // Build VPN service + Builder builder = new Builder(); + builder.setSession(getString(R.string.app_name)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + builder.setMetered(Util.isMeteredNetwork(this)); + + // VPN address + String vpn4 = prefs.getString("vpn4", "10.1.10.1"); + Log.i(TAG, "Using VPN4=" + vpn4); + builder.addAddress(vpn4, 32); + if (ip6) { + String vpn6 = prefs.getString("vpn6", "fd00:1:fd00:1:fd00:1:fd00:1"); + Log.i(TAG, "Using VPN6=" + vpn6); + builder.addAddress(vpn6, 128); + } + + // DNS address + if (filter) + for (InetAddress dns : getDns(ServiceSinkhole.this)) { + if (ip6 || dns instanceof Inet4Address) { + Log.i(TAG, "Using DNS=" + dns); + builder.addDnsServer(dns); + } + } + + // Subnet routing + if (subnet) { + // Exclude IP ranges + List listExclude = new ArrayList<>(); + listExclude.add(new IPUtil.CIDR("127.0.0.0", 8)); // localhost + + if (tethering && !lan) { + // USB tethering 192.168.42.x + // Wi-Fi tethering 192.168.43.x + listExclude.add(new IPUtil.CIDR("192.168.42.0", 23)); + // Bluetooth tethering 192.168.44.x + listExclude.add(new IPUtil.CIDR("192.168.44.0", 24)); + // Wi-Fi direct 192.168.49.x + listExclude.add(new IPUtil.CIDR("192.168.49.0", 24)); + } + + if (lan) { + // https://tools.ietf.org/html/rfc1918 + listExclude.add(new IPUtil.CIDR("10.0.0.0", 8)); + listExclude.add(new IPUtil.CIDR("172.16.0.0", 12)); + listExclude.add(new IPUtil.CIDR("192.168.0.0", 16)); + } + + if (!filter) { + for (InetAddress dns : getDns(ServiceSinkhole.this)) + if (dns instanceof Inet4Address) + listExclude.add(new IPUtil.CIDR(dns.getHostAddress(), 32)); + + String dns_specifier = Util.getPrivateDnsSpecifier(ServiceSinkhole.this); + if (!TextUtils.isEmpty(dns_specifier)) + try { + Log.i(TAG, "Resolving private dns=" + dns_specifier); + for (InetAddress pdns : InetAddress.getAllByName(dns_specifier)) + if (pdns instanceof Inet4Address) + listExclude.add(new IPUtil.CIDR(pdns.getHostAddress(), 32)); + } catch (Throwable ex) { + Log.e(TAG, ex.toString()); + } + } + + // https://en.wikipedia.org/wiki/Mobile_country_code + Configuration config = getResources().getConfiguration(); + + // T-Mobile Wi-Fi calling + if (config.mcc == 310 && (config.mnc == 160 || + config.mnc == 200 || + config.mnc == 210 || + config.mnc == 220 || + config.mnc == 230 || + config.mnc == 240 || + config.mnc == 250 || + config.mnc == 260 || + config.mnc == 270 || + config.mnc == 310 || + config.mnc == 490 || + config.mnc == 660 || + config.mnc == 800)) { + listExclude.add(new IPUtil.CIDR("66.94.2.0", 24)); + listExclude.add(new IPUtil.CIDR("66.94.6.0", 23)); + listExclude.add(new IPUtil.CIDR("66.94.8.0", 22)); + listExclude.add(new IPUtil.CIDR("208.54.0.0", 16)); + } + + // Verizon wireless calling + if ((config.mcc == 310 && + (config.mnc == 4 || + config.mnc == 5 || + config.mnc == 6 || + config.mnc == 10 || + config.mnc == 12 || + config.mnc == 13 || + config.mnc == 350 || + config.mnc == 590 || + config.mnc == 820 || + config.mnc == 890 || + config.mnc == 910)) || + (config.mcc == 311 && (config.mnc == 12 || + config.mnc == 110 || + (config.mnc >= 270 && config.mnc <= 289) || + config.mnc == 390 || + (config.mnc >= 480 && config.mnc <= 489) || + config.mnc == 590)) || + (config.mcc == 312 && (config.mnc == 770))) { + listExclude.add(new IPUtil.CIDR("66.174.0.0", 16)); // 66.174.0.0 - 66.174.255.255 + listExclude.add(new IPUtil.CIDR("66.82.0.0", 15)); // 69.82.0.0 - 69.83.255.255 + listExclude.add(new IPUtil.CIDR("69.96.0.0", 13)); // 69.96.0.0 - 69.103.255.255 + listExclude.add(new IPUtil.CIDR("70.192.0.0", 11)); // 70.192.0.0 - 70.223.255.255 + listExclude.add(new IPUtil.CIDR("97.128.0.0", 9)); // 97.128.0.0 - 97.255.255.255 + listExclude.add(new IPUtil.CIDR("174.192.0.0", 9)); // 174.192.0.0 - 174.255.255.255 + listExclude.add(new IPUtil.CIDR("72.96.0.0", 9)); // 72.96.0.0 - 72.127.255.255 + listExclude.add(new IPUtil.CIDR("75.192.0.0", 9)); // 75.192.0.0 - 75.255.255.255 + listExclude.add(new IPUtil.CIDR("97.0.0.0", 10)); // 97.0.0.0 - 97.63.255.255 + } + + // SFR MMS + if (config.mnc == 10 && config.mcc == 208) + listExclude.add(new IPUtil.CIDR("10.151.0.0", 24)); + + // Broadcast + listExclude.add(new IPUtil.CIDR("224.0.0.0", 3)); + + Collections.sort(listExclude); + + try { + InetAddress start = InetAddress.getByName("0.0.0.0"); + for (IPUtil.CIDR exclude : listExclude) { + Log.i(TAG, "Exclude " + exclude.getStart().getHostAddress() + "..." + exclude.getEnd().getHostAddress()); + for (IPUtil.CIDR include : IPUtil.toCIDR(start, IPUtil.minus1(exclude.getStart()))) + try { + builder.addRoute(include.address, include.prefix); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + start = IPUtil.plus1(exclude.getEnd()); + } + String end = (lan ? "255.255.255.254" : "255.255.255.255"); + for (IPUtil.CIDR include : IPUtil.toCIDR("224.0.0.0", end)) + try { + builder.addRoute(include.address, include.prefix); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } catch (UnknownHostException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } else + builder.addRoute("0.0.0.0", 0); + + Log.i(TAG, "IPv6=" + ip6); + if (ip6) + builder.addRoute("2000::", 3); // unicast + + // MTU + int mtu = jni_get_mtu(); + Log.i(TAG, "MTU=" + mtu); + builder.setMtu(mtu); + + // Add list of allowed applications + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + try { + builder.addDisallowedApplication(getPackageName()); + } catch (PackageManager.NameNotFoundException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + if (last_connected && !filter) + for (Rule rule : listAllowed) + try { + builder.addDisallowedApplication(rule.packageName); + } catch (PackageManager.NameNotFoundException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + else if (filter) + for (Rule rule : listRule) + if (!rule.apply || (!system && rule.system)) + try { + Log.i(TAG, "Not routing " + rule.packageName); + builder.addDisallowedApplication(rule.packageName); + } catch (PackageManager.NameNotFoundException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + + // Build configure intent + Intent configure = new Intent(this, ActivityMain.class); + PendingIntent pi = PendingIntent.getActivity(this, 0, configure, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + builder.setConfigureIntent(pi); + + return builder; + } + + private void startNative(final ParcelFileDescriptor vpn, List listAllowed, List listRule) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + boolean log = prefs.getBoolean("log", false); + boolean log_app = prefs.getBoolean("log_app", false); + boolean filter = prefs.getBoolean("filter", false); + + Log.i(TAG, "Start native log=" + log + "/" + log_app + " filter=" + filter); + + // Prepare rules + if (filter) { + prepareUidAllowed(listAllowed, listRule); + prepareHostsBlocked(); + prepareUidIPFilters(null); + prepareForwarding(); + } else { + lock.writeLock().lock(); + mapUidAllowed.clear(); + mapUidKnown.clear(); + mapHostsBlocked.clear(); + mapUidIPFilters.clear(); + mapForward.clear(); + lock.writeLock().unlock(); + } + + if (log_app) + prepareNotify(listRule); + else { + lock.writeLock().lock(); + mapNotify.clear(); + lock.writeLock().unlock(); + } + + //if (log || log_app || filter) { + if (true) { + int prio = Integer.parseInt(prefs.getString("loglevel", Integer.toString(Log.WARN))); + final int rcode = Integer.parseInt(prefs.getString("rcode", "3")); + if (prefs.getBoolean("socks5_enabled", false)) + jni_socks5( + prefs.getString("socks5_addr", ""), + Integer.parseInt(prefs.getString("socks5_port", "0")), + prefs.getString("socks5_username", ""), + prefs.getString("socks5_password", "")); + else + jni_socks5("", 0, "", ""); + + if (tunnelThread == null) { + Log.i(TAG, "Starting tunnel thread context=" + jni_context); + jni_start(jni_context, prio); + + tunnelThread = new Thread(new Runnable() { + @Override + public void run() { + Log.i(TAG, "Running tunnel context=" + jni_context); + jni_run(jni_context, vpn.getFd(), mapForward.containsKey(53), rcode); + Log.i(TAG, "Tunnel exited"); + tunnelThread = null; + } + }); + //tunnelThread.setPriority(Thread.MAX_PRIORITY); + tunnelThread.start(); + + Log.i(TAG, "Started tunnel thread"); + } + } + } + + private void stopNative(ParcelFileDescriptor vpn) { + Log.i(TAG, "Stop native"); + + if (tunnelThread != null) { + Log.i(TAG, "Stopping tunnel thread"); + + jni_stop(jni_context); + + Thread thread = tunnelThread; + while (thread != null && thread.isAlive()) { + try { + Log.i(TAG, "Joining tunnel thread context=" + jni_context); + thread.join(); + } catch (InterruptedException ignored) { + Log.i(TAG, "Joined tunnel interrupted"); + } + thread = tunnelThread; + } + tunnelThread = null; + + jni_clear(jni_context); + + Log.i(TAG, "Stopped tunnel thread"); + } + } + + private void unprepare() { + lock.writeLock().lock(); + mapUidAllowed.clear(); + mapUidKnown.clear(); + mapHostsBlocked.clear(); + mapUidIPFilters.clear(); + mapForward.clear(); + mapNotify.clear(); + lock.writeLock().unlock(); + } + + private void prepareUidAllowed(List listAllowed, List listRule) { + lock.writeLock().lock(); + + mapUidAllowed.clear(); + for (Rule rule : listAllowed) + mapUidAllowed.put(rule.uid, true); + + mapUidKnown.clear(); + for (Rule rule : listRule) + mapUidKnown.put(rule.uid, rule.uid); + + lock.writeLock().unlock(); + } + + private void prepareHostsBlocked() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + boolean use_hosts = prefs.getBoolean("filter", false) && prefs.getBoolean("use_hosts", false); + File hosts = new File(getFilesDir(), "hosts.txt"); + if (!use_hosts || !hosts.exists() || !hosts.canRead()) { + Log.i(TAG, "Hosts file use=" + use_hosts + " exists=" + hosts.exists()); + lock.writeLock().lock(); + mapHostsBlocked.clear(); + lock.writeLock().unlock(); + return; + } + + boolean changed = (hosts.lastModified() != last_hosts_modified); + if (!changed && mapHostsBlocked.size() > 0) { + Log.i(TAG, "Hosts file unchanged"); + return; + } + last_hosts_modified = hosts.lastModified(); + + lock.writeLock().lock(); + + mapHostsBlocked.clear(); + + int count = 0; + BufferedReader br = null; + try { + br = new BufferedReader(new FileReader(hosts)); + String line; + while ((line = br.readLine()) != null) { + int hash = line.indexOf('#'); + if (hash >= 0) + line = line.substring(0, hash); + line = line.trim(); + if (line.length() > 0) { + String[] words = line.split("\\s+"); + if (words.length == 2) { + count++; + mapHostsBlocked.put(words[1], true); + } else + Log.i(TAG, "Invalid hosts file line: " + line); + } + } + mapHostsBlocked.put("test.netguard.me", true); + Log.i(TAG, count + " hosts read"); + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } finally { + if (br != null) + try { + br.close(); + } catch (IOException exex) { + Log.e(TAG, exex.toString() + "\n" + Log.getStackTraceString(exex)); + } + } + + lock.writeLock().unlock(); + } + + private void prepareUidIPFilters(String dname) { + SharedPreferences lockdown = getSharedPreferences("lockdown", Context.MODE_PRIVATE); + + lock.writeLock().lock(); + + if (dname == null) { + mapUidIPFilters.clear(); + if (!IAB.isPurchased(ActivityPro.SKU_FILTER, ServiceSinkhole.this)) { + lock.writeLock().unlock(); + return; + } + } + + try (Cursor cursor = DatabaseHelper.getInstance(ServiceSinkhole.this).getAccessDns(dname)) { + int colUid = cursor.getColumnIndex("uid"); + int colVersion = cursor.getColumnIndex("version"); + int colProtocol = cursor.getColumnIndex("protocol"); + int colDAddr = cursor.getColumnIndex("daddr"); + int colResource = cursor.getColumnIndex("resource"); + int colDPort = cursor.getColumnIndex("dport"); + int colBlock = cursor.getColumnIndex("block"); + int colTime = cursor.getColumnIndex("time"); + int colTTL = cursor.getColumnIndex("ttl"); + while (cursor.moveToNext()) { + int uid = cursor.getInt(colUid); + int version = cursor.getInt(colVersion); + int protocol = cursor.getInt(colProtocol); + String daddr = cursor.getString(colDAddr); + String dresource = (cursor.isNull(colResource) ? null : cursor.getString(colResource)); + int dport = cursor.getInt(colDPort); + boolean block = (cursor.getInt(colBlock) > 0); + long time = (cursor.isNull(colTime) ? new Date().getTime() : cursor.getLong(colTime)); + long ttl = (cursor.isNull(colTTL) ? 7 * 24 * 3600 * 1000L : cursor.getLong(colTTL)); + + if (isLockedDown(last_metered)) { + String[] pkg = getPackageManager().getPackagesForUid(uid); + if (pkg != null && pkg.length > 0) { + if (!lockdown.getBoolean(pkg[0], false)) + continue; + } + } + + IPKey key = new IPKey(version, protocol, dport, uid); + synchronized (mapUidIPFilters) { + if (!mapUidIPFilters.containsKey(key)) + mapUidIPFilters.put(key, new HashMap()); + + try { + String name = (dresource == null ? daddr : dresource); + if (Util.isNumericAddress(name)) { + InetAddress iname = InetAddress.getByName(name); + if (version == 4 && !(iname instanceof Inet4Address)) + continue; + if (version == 6 && !(iname instanceof Inet6Address)) + continue; + + boolean exists = mapUidIPFilters.get(key).containsKey(iname); + if (!exists || !mapUidIPFilters.get(key).get(iname).isBlocked()) { + IPRule rule = new IPRule(key, name + "/" + iname, block, time, ttl); + mapUidIPFilters.get(key).put(iname, rule); + if (exists) + Log.w(TAG, "Address conflict " + key + " " + daddr + "/" + dresource); + } else if (exists) { + mapUidIPFilters.get(key).get(iname).updateExpires(time, ttl); + if (dname != null && ttl > 60 * 1000L) + Log.w(TAG, "Address updated " + key + " " + daddr + "/" + dresource); + } else { + if (dname != null) + Log.i(TAG, "Ignored " + key + " " + daddr + "/" + dresource + "=" + block); + } + } else + Log.w(TAG, "Address not numeric " + name); + } catch (UnknownHostException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + } + } + + lock.writeLock().unlock(); + } + + private void prepareForwarding() { + lock.writeLock().lock(); + mapForward.clear(); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + if (prefs.getBoolean("filter", false)) { + try (Cursor cursor = DatabaseHelper.getInstance(ServiceSinkhole.this).getForwarding()) { + int colProtocol = cursor.getColumnIndex("protocol"); + int colDPort = cursor.getColumnIndex("dport"); + int colRAddr = cursor.getColumnIndex("raddr"); + int colRPort = cursor.getColumnIndex("rport"); + int colRUid = cursor.getColumnIndex("ruid"); + while (cursor.moveToNext()) { + Forward fwd = new Forward(); + fwd.protocol = cursor.getInt(colProtocol); + fwd.dport = cursor.getInt(colDPort); + fwd.raddr = cursor.getString(colRAddr); + fwd.rport = cursor.getInt(colRPort); + fwd.ruid = cursor.getInt(colRUid); + mapForward.put(fwd.dport, fwd); + Log.i(TAG, "Forward " + fwd); + } + } + } + lock.writeLock().unlock(); + } + + private void prepareNotify(List listRule) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean notify = prefs.getBoolean("notify_access", false); + boolean system = prefs.getBoolean("manage_system", false); + + lock.writeLock().lock(); + mapNotify.clear(); + for (Rule rule : listRule) + mapNotify.put(rule.uid, notify && rule.notify && (system || !rule.system)); + lock.writeLock().unlock(); + } + + private boolean isLockedDown(boolean metered) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + boolean lockdown = prefs.getBoolean("lockdown", false); + boolean lockdown_wifi = prefs.getBoolean("lockdown_wifi", true); + boolean lockdown_other = prefs.getBoolean("lockdown_other", true); + if (metered ? !lockdown_other : !lockdown_wifi) + lockdown = false; + + return lockdown; + } + + private List getAllowedRules(List listRule) { + List listAllowed = new ArrayList<>(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + + // Check state + boolean wifi = Util.isWifiActive(this); + boolean metered = Util.isMeteredNetwork(this); + boolean useMetered = prefs.getBoolean("use_metered", false); + Set ssidHomes = prefs.getStringSet("wifi_homes", new HashSet()); + String ssidNetwork = Util.getWifiSSID(this); + String generation = Util.getNetworkGeneration(this); + boolean unmetered_2g = prefs.getBoolean("unmetered_2g", false); + boolean unmetered_3g = prefs.getBoolean("unmetered_3g", false); + boolean unmetered_4g = prefs.getBoolean("unmetered_4g", false); + boolean roaming = Util.isRoaming(ServiceSinkhole.this); + boolean national = prefs.getBoolean("national_roaming", false); + boolean eu = prefs.getBoolean("eu_roaming", false); + boolean tethering = prefs.getBoolean("tethering", false); + boolean filter = prefs.getBoolean("filter", false); + + // Update connected state + last_connected = Util.isConnected(ServiceSinkhole.this); + + boolean org_metered = metered; + boolean org_roaming = roaming; + + // https://issuetracker.google.com/issues/70633700 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) + ssidHomes.clear(); + + // Update metered state + if (wifi && !useMetered) + metered = false; + if (wifi && ssidHomes.size() > 0 && + !(ssidHomes.contains(ssidNetwork) || ssidHomes.contains('"' + ssidNetwork + '"'))) { + metered = true; + Log.i(TAG, "!@home=" + ssidNetwork + " homes=" + TextUtils.join(",", ssidHomes)); + } + if (unmetered_2g && "2G".equals(generation)) + metered = false; + if (unmetered_3g && "3G".equals(generation)) + metered = false; + if (unmetered_4g && "4G".equals(generation)) + metered = false; + last_metered = metered; + + boolean lockdown = isLockedDown(last_metered); + + // Update roaming state + if (roaming && eu) + roaming = !Util.isEU(this); + if (roaming && national) + roaming = !Util.isNational(this); + + Log.i(TAG, "Get allowed" + + " connected=" + last_connected + + " wifi=" + wifi + + " home=" + TextUtils.join(",", ssidHomes) + + " network=" + ssidNetwork + + " metered=" + metered + "/" + org_metered + + " generation=" + generation + + " roaming=" + roaming + "/" + org_roaming + + " interactive=" + last_interactive + + " tethering=" + tethering + + " filter=" + filter + + " lockdown=" + lockdown); + + if (last_connected) + for (Rule rule : listRule) { + boolean blocked = (metered ? rule.other_blocked : rule.wifi_blocked); + boolean screen = (metered ? rule.screen_other : rule.screen_wifi); + if ((!blocked || (screen && last_interactive)) && + (!metered || !(rule.roaming && roaming)) && + (!lockdown || rule.lockdown)) + listAllowed.add(rule); + } + + Log.i(TAG, "Allowed " + listAllowed.size() + " of " + listRule.size()); + return listAllowed; + } + + private void stopVPN(ParcelFileDescriptor pfd) { + Log.i(TAG, "Stopping"); + try { + pfd.close(); + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + + // Called from native code + private void nativeExit(String reason) { + Log.w(TAG, "Native exit reason=" + reason); + if (reason != null) { + showErrorNotification(reason); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.edit().putBoolean("enabled", false).apply(); + WidgetMain.updateWidgets(this); + } + } + + // Called from native code + private void nativeError(int error, String message) { + Log.w(TAG, "Native error " + error + ": " + message); + showErrorNotification(message); + } + + // Called from native code + private void logPacket(Packet packet) { + logHandler.queue(packet); + } + + // Called from native code + private void dnsResolved(ResourceRecord rr) { + if (DatabaseHelper.getInstance(ServiceSinkhole.this).insertDns(rr)) { + Log.i(TAG, "New IP " + rr); + prepareUidIPFilters(rr.QName); + } + } + + // Called from native code + private boolean isDomainBlocked(String name) { + lock.readLock().lock(); + boolean blocked = (mapHostsBlocked.containsKey(name) && mapHostsBlocked.get(name)); + lock.readLock().unlock(); + return blocked; + } + + // Called from native code + @TargetApi(Build.VERSION_CODES.Q) + private int getUidQ(int version, int protocol, String saddr, int sport, String daddr, int dport) { + if (protocol != 6 /* TCP */ && protocol != 17 /* UDP */) + return Process.INVALID_UID; + + ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); + if (cm == null) + return Process.INVALID_UID; + + InetSocketAddress local = new InetSocketAddress(saddr, sport); + InetSocketAddress remote = new InetSocketAddress(daddr, dport); + + Log.i(TAG, "Get uid local=" + local + " remote=" + remote); + int uid = cm.getConnectionOwnerUid(protocol, local, remote); + Log.i(TAG, "Get uid=" + uid); + return uid; + } + + private boolean isSupported(int protocol) { + return (protocol == 1 /* ICMPv4 */ || + protocol == 58 /* ICMPv6 */ || + protocol == 6 /* TCP */ || + protocol == 17 /* UDP */); + } + + // Called from native code + private Allowed isAddressAllowed(Packet packet) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + + lock.readLock().lock(); + + System.out.println("BPB: check if allowed packet with source: " + packet.saddr + ":" + packet.sport + " -> " + packet.daddr + ":" + packet.dport); + + packet.allowed = false; + if (prefs.getBoolean("filter", false)) { + // https://android.googlesource.com/platform/system/core/+/master/include/private/android_filesystem_config.h + if (packet.protocol == 17 /* UDP */ && !prefs.getBoolean("filter_udp", false)) { + // Allow unfiltered UDP + packet.allowed = true; + Log.i(TAG, "Allowing UDP " + packet); + } else if (packet.uid < 2000 && + !last_connected && isSupported(packet.protocol) && false) { + // Allow system applications in disconnected state + packet.allowed = true; + Log.w(TAG, "Allowing disconnected system " + packet); + } else if (packet.uid < 2000 && + !mapUidKnown.containsKey(packet.uid) && isSupported(packet.protocol)) { + // Allow unknown system traffic + packet.allowed = true; + Log.w(TAG, "Allowing unknown system " + packet); + } else if (packet.uid == Process.myUid()) { + // Allow self + packet.allowed = true; + Log.w(TAG, "Allowing self " + packet); + } else { + boolean filtered = false; + IPKey key = new IPKey(packet.version, packet.protocol, packet.dport, packet.uid); + if (mapUidIPFilters.containsKey(key)) + try { + InetAddress iaddr = InetAddress.getByName(packet.daddr); + Map map = mapUidIPFilters.get(key); + if (map != null && map.containsKey(iaddr)) { + IPRule rule = map.get(iaddr); + if (rule.isExpired()) + Log.i(TAG, "DNS expired " + packet + " rule " + rule); + else { + filtered = true; + packet.allowed = !rule.isBlocked(); + Log.i(TAG, "Filtering " + packet + + " allowed=" + packet.allowed + " rule " + rule); + } + } + } catch (UnknownHostException ex) { + Log.w(TAG, "Allowed " + ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + if (!filtered) + if (mapUidAllowed.containsKey(packet.uid)) + packet.allowed = mapUidAllowed.get(packet.uid); + else + Log.w(TAG, "No rules for " + packet); + } + } + + Allowed allowed = null; + if (packet.allowed) { + if (mapForward.containsKey(packet.dport)) { + Forward fwd = mapForward.get(packet.dport); + if (fwd.ruid == packet.uid) { + allowed = new Allowed(); + } else { + allowed = new Allowed(fwd.raddr, fwd.rport); + packet.data = "> " + fwd.raddr + "/" + fwd.rport; + } + } else + allowed = new Allowed(); + } + + lock.readLock().unlock(); + + if (prefs.getBoolean("log", false) || prefs.getBoolean("log_app", false)) + if (packet.protocol != 6 /* TCP */ || !"".equals(packet.flags)) + if (packet.uid != Process.myUid()) + logPacket(packet); + + return allowed; + } + + // Called from native code + private void accountUsage(Usage usage) { + logHandler.account(usage); + } + + private BroadcastReceiver interactiveStateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(final Context context, final Intent intent) { + Log.i(TAG, "Received " + intent); + Util.logExtras(intent); + + executor.submit(new Runnable() { + @Override + public void run() { + AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent i = new Intent(ACTION_SCREEN_OFF_DELAYED); + i.setPackage(context.getPackageName()); + PendingIntent pi = PendingIntent.getBroadcast(context, 0, i, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + am.cancel(pi); + + try { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + int delay; + try { + delay = Integer.parseInt(prefs.getString("screen_delay", "0")); + } catch (NumberFormatException ignored) { + delay = 0; + } + boolean interactive = Intent.ACTION_SCREEN_ON.equals(intent.getAction()); + + if (interactive || delay == 0) { + last_interactive = interactive; + reload("interactive state changed", ServiceSinkhole.this, true); + } else { + if (ACTION_SCREEN_OFF_DELAYED.equals(intent.getAction())) { + last_interactive = interactive; + reload("interactive state changed", ServiceSinkhole.this, true); + } else { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + am.set(AlarmManager.RTC_WAKEUP, new Date().getTime() + delay * 60 * 1000L, pi); + else + am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, new Date().getTime() + delay * 60 * 1000L, pi); + } + } + + // Start/stop stats + statsHandler.sendEmptyMessage( + Util.isInteractive(ServiceSinkhole.this) ? MSG_STATS_START : MSG_STATS_STOP); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + am.set(AlarmManager.RTC_WAKEUP, new Date().getTime() + 15 * 1000L, pi); + else + am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, new Date().getTime() + 15 * 1000L, pi); + } + } + }); + } + }; + + private BroadcastReceiver userReceiver = new BroadcastReceiver() { + @Override + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "Received " + intent); + Util.logExtras(intent); + + user_foreground = Intent.ACTION_USER_FOREGROUND.equals(intent.getAction()); + Log.i(TAG, "User foreground=" + user_foreground + " user=" + (Process.myUid() / 100000)); + + if (user_foreground) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + if (prefs.getBoolean("enabled", false)) { + // Allow service of background user to stop + try { + Thread.sleep(3000); + } catch (InterruptedException ignored) { + } + + start("foreground", ServiceSinkhole.this); + } + } else + stop("background", ServiceSinkhole.this, true); + } + }; + + private BroadcastReceiver idleStateReceiver = new BroadcastReceiver() { + @Override + @TargetApi(Build.VERSION_CODES.M) + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "Received " + intent); + Util.logExtras(intent); + + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + Log.i(TAG, "device idle=" + pm.isDeviceIdleMode()); + + // Reload rules when coming from idle mode + if (!pm.isDeviceIdleMode()) + reload("idle state changed", ServiceSinkhole.this, false); + } + }; + + private BroadcastReceiver connectivityChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Filter VPN connectivity changes + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + int networkType = intent.getIntExtra(ConnectivityManager.EXTRA_NETWORK_TYPE, ConnectivityManager.TYPE_DUMMY); + if (networkType == ConnectivityManager.TYPE_VPN) + return; + } + + // Reload rules + Log.i(TAG, "Received " + intent); + Util.logExtras(intent); + reload("connectivity changed", ServiceSinkhole.this, false); + } + }; + + ConnectivityManager.NetworkCallback networkMonitorCallback = new ConnectivityManager.NetworkCallback() { + private String TAG = "NetGuard.Monitor"; + + private Map validated = new HashMap<>(); + + // https://android.googlesource.com/platform/frameworks/base/+/master/services/core/java/com/android/server/connectivity/NetworkMonitor.java + + @Override + public void onAvailable(Network network) { + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = cm.getNetworkInfo(network); + NetworkCapabilities capabilities = cm.getNetworkCapabilities(network); + Log.i(TAG, "Available network " + network + " " + ni); + Log.i(TAG, "Capabilities=" + capabilities); + checkConnectivity(network, ni, capabilities); + } + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities capabilities) { + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = cm.getNetworkInfo(network); + Log.i(TAG, "New capabilities network " + network + " " + ni); + Log.i(TAG, "Capabilities=" + capabilities); + checkConnectivity(network, ni, capabilities); + } + + @Override + public void onLosing(Network network, int maxMsToLive) { + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = cm.getNetworkInfo(network); + Log.i(TAG, "Losing network " + network + " within " + maxMsToLive + " ms " + ni); + } + + @Override + public void onLost(Network network) { + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = cm.getNetworkInfo(network); + Log.i(TAG, "Lost network " + network + " " + ni); + + synchronized (validated) { + validated.remove(network); + } + } + + @Override + public void onUnavailable() { + Log.i(TAG, "No networks available"); + } + + private void checkConnectivity(Network network, NetworkInfo ni, NetworkCapabilities capabilities) { + if (ni != null && capabilities != null && + ni.getDetailedState() != NetworkInfo.DetailedState.SUSPENDED && + ni.getDetailedState() != NetworkInfo.DetailedState.BLOCKED && + ni.getDetailedState() != NetworkInfo.DetailedState.DISCONNECTED && + capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) && + !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) { + + synchronized (validated) { + if (validated.containsKey(network) && + validated.get(network) + 20 * 1000 > new Date().getTime()) { + Log.i(TAG, "Already validated " + network + " " + ni); + return; + } + } + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + String host = prefs.getString("validate", "www.google.com"); + Log.i(TAG, "Validating " + network + " " + ni + " host=" + host); + + Socket socket = null; + try { + socket = network.getSocketFactory().createSocket(); + socket.connect(new InetSocketAddress(host, 443), 10000); + Log.i(TAG, "Validated " + network + " " + ni + " host=" + host); + synchronized (validated) { + validated.put(network, new Date().getTime()); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + cm.reportNetworkConnectivity(network, true); + Log.i(TAG, "Reported " + network + " " + ni); + } + } catch (IOException ex) { + Log.e(TAG, ex.toString()); + Log.i(TAG, "No connectivity " + network + " " + ni); + } finally { + if (socket != null) + try { + socket.close(); + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + } + } + }; + + private PhoneStateListener phoneStateListener = new PhoneStateListener() { + private String last_generation = null; + + @Override + public void onDataConnectionStateChanged(int state, int networkType) { + if (state == TelephonyManager.DATA_CONNECTED) { + String current_generation = Util.getNetworkGeneration(ServiceSinkhole.this); + Log.i(TAG, "Data connected generation=" + current_generation); + + if (last_generation == null || !last_generation.equals(current_generation)) { + Log.i(TAG, "New network generation=" + current_generation); + last_generation = current_generation; + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + if (prefs.getBoolean("unmetered_2g", false) || + prefs.getBoolean("unmetered_3g", false) || + prefs.getBoolean("unmetered_4g", false)) + reload("data connection state changed", ServiceSinkhole.this, false); + } + } + } + }; + + private BroadcastReceiver packageChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "Received " + intent); + Util.logExtras(intent); + + try { + if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())) { + // Application added + Rule.clearCache(context); + + if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + // Show notification + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + if (IAB.isPurchased(ActivityPro.SKU_NOTIFY, context) && prefs.getBoolean("install", true)) { + int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); + notifyNewApplication(uid); + } + } + + reload("package added", context, false); + + } else if (Intent.ACTION_PACKAGE_REMOVED.equals(intent.getAction())) { + // Application removed + Rule.clearCache(context); + + if (intent.getBooleanExtra(Intent.EXTRA_DATA_REMOVED, false)) { + // Remove settings + String packageName = intent.getData().getSchemeSpecificPart(); + Log.i(TAG, "Deleting settings package=" + packageName); + context.getSharedPreferences("wifi", Context.MODE_PRIVATE).edit().remove(packageName).apply(); + context.getSharedPreferences("other", Context.MODE_PRIVATE).edit().remove(packageName).apply(); + context.getSharedPreferences("screen_wifi", Context.MODE_PRIVATE).edit().remove(packageName).apply(); + context.getSharedPreferences("screen_other", Context.MODE_PRIVATE).edit().remove(packageName).apply(); + context.getSharedPreferences("roaming", Context.MODE_PRIVATE).edit().remove(packageName).apply(); + context.getSharedPreferences("lockdown", Context.MODE_PRIVATE).edit().remove(packageName).apply(); + context.getSharedPreferences("apply", Context.MODE_PRIVATE).edit().remove(packageName).apply(); + context.getSharedPreferences("notify", Context.MODE_PRIVATE).edit().remove(packageName).apply(); + + int uid = intent.getIntExtra(Intent.EXTRA_UID, 0); + if (uid > 0) { + DatabaseHelper dh = DatabaseHelper.getInstance(context); + dh.clearLog(uid); + dh.clearAccess(uid, false); + + NotificationManagerCompat.from(context).cancel(uid); // installed notification + NotificationManagerCompat.from(context).cancel(uid + 10000); // access notification + } + } + + reload("package deleted", context, false); + } + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + }; + + public void notifyNewApplication(int uid) { + if (uid < 0) + return; + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + try { + // Get application name + String name = TextUtils.join(", ", Util.getApplicationNames(uid, this)); + + // Get application info + PackageManager pm = getPackageManager(); + String[] packages = pm.getPackagesForUid(uid); + if (packages == null || packages.length < 1) + throw new PackageManager.NameNotFoundException(Integer.toString(uid)); + boolean internet = Util.hasInternet(uid, this); + + // Build notification + Intent main = new Intent(this, ActivityMain.class); + main.putExtra(ActivityMain.EXTRA_REFRESH, true); + main.putExtra(ActivityMain.EXTRA_SEARCH, Integer.toString(uid)); + PendingIntent pi = PendingIntent.getActivity(this, uid, main, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + + TypedValue tv = new TypedValue(); + getTheme().resolveAttribute(R.attr.colorPrimary, tv, true); + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "notify"); + builder.setSmallIcon(R.drawable.ic_security_white_24dp) + .setContentIntent(pi) + .setColor(tv.data) + .setAutoCancel(true); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + builder.setContentTitle(name) + .setContentText(getString(R.string.msg_installed_n)); + else + builder.setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.msg_installed, name)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + builder.setCategory(NotificationCompat.CATEGORY_STATUS) + .setVisibility(NotificationCompat.VISIBILITY_SECRET); + + // Get defaults + SharedPreferences prefs_wifi = getSharedPreferences("wifi", Context.MODE_PRIVATE); + SharedPreferences prefs_other = getSharedPreferences("other", Context.MODE_PRIVATE); + boolean wifi = prefs_wifi.getBoolean(packages[0], prefs.getBoolean("whitelist_wifi", true)); + boolean other = prefs_other.getBoolean(packages[0], prefs.getBoolean("whitelist_other", true)); + + // Build Wi-Fi action + Intent riWifi = new Intent(this, ServiceSinkhole.class); + riWifi.putExtra(ServiceSinkhole.EXTRA_COMMAND, ServiceSinkhole.Command.set); + riWifi.putExtra(ServiceSinkhole.EXTRA_NETWORK, "wifi"); + riWifi.putExtra(ServiceSinkhole.EXTRA_UID, uid); + riWifi.putExtra(ServiceSinkhole.EXTRA_PACKAGE, packages[0]); + riWifi.putExtra(ServiceSinkhole.EXTRA_BLOCKED, !wifi); + + PendingIntent piWifi = PendingIntent.getService(this, uid, riWifi, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + NotificationCompat.Action wAction = new NotificationCompat.Action.Builder( + wifi ? R.drawable.wifi_on : R.drawable.wifi_off, + getString(wifi ? R.string.title_allow_wifi : R.string.title_block_wifi), + piWifi + ).build(); + builder.addAction(wAction); + + // Build mobile action + Intent riOther = new Intent(this, ServiceSinkhole.class); + riOther.putExtra(ServiceSinkhole.EXTRA_COMMAND, ServiceSinkhole.Command.set); + riOther.putExtra(ServiceSinkhole.EXTRA_NETWORK, "other"); + riOther.putExtra(ServiceSinkhole.EXTRA_UID, uid); + riOther.putExtra(ServiceSinkhole.EXTRA_PACKAGE, packages[0]); + riOther.putExtra(ServiceSinkhole.EXTRA_BLOCKED, !other); + PendingIntent piOther = PendingIntent.getService(this, uid + 10000, riOther, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + NotificationCompat.Action oAction = new NotificationCompat.Action.Builder( + other ? R.drawable.other_on : R.drawable.other_off, + getString(other ? R.string.title_allow_other : R.string.title_block_other), + piOther + ).build(); + builder.addAction(oAction); + + // Show notification + if (internet) + NotificationManagerCompat.from(this).notify(uid, builder.build()); + else { + NotificationCompat.BigTextStyle expanded = new NotificationCompat.BigTextStyle(builder); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + expanded.bigText(getString(R.string.msg_installed_n)); + else + expanded.bigText(getString(R.string.msg_installed, name)); + expanded.setSummaryText(getString(R.string.title_internet)); + NotificationManagerCompat.from(this).notify(uid, expanded.build()); + } + + } catch (PackageManager.NameNotFoundException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + + @Override + public void onCreate() { + Log.i(TAG, "Create version=" + Util.getSelfVersionName(this) + "/" + Util.getSelfVersionCode(this)); + startForeground(NOTIFY_WAITING, getWaitingNotification()); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + + if (jni_context != 0) { + Log.w(TAG, "Create with context=" + jni_context); + jni_stop(jni_context); + synchronized (jni_lock) { + jni_done(jni_context); + jni_context = 0; + } + } + + // Native init + jni_context = jni_init(Build.VERSION.SDK_INT); + Log.i(TAG, "Created context=" + jni_context); + boolean pcap = prefs.getBoolean("pcap", false); + setPcap(pcap, this); + + prefs.registerOnSharedPreferenceChangeListener(this); + + Util.setTheme(this); + super.onCreate(); + + HandlerThread commandThread = new HandlerThread(getString(R.string.app_name) + " command", Process.THREAD_PRIORITY_FOREGROUND); + HandlerThread logThread = new HandlerThread(getString(R.string.app_name) + " log", Process.THREAD_PRIORITY_BACKGROUND); + HandlerThread statsThread = new HandlerThread(getString(R.string.app_name) + " stats", Process.THREAD_PRIORITY_BACKGROUND); + commandThread.start(); + logThread.start(); + statsThread.start(); + + commandLooper = commandThread.getLooper(); + logLooper = logThread.getLooper(); + statsLooper = statsThread.getLooper(); + + commandHandler = new CommandHandler(commandLooper); + logHandler = new LogHandler(logLooper); + statsHandler = new StatsHandler(statsLooper); + + // Listen for user switches + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + IntentFilter ifUser = new IntentFilter(); + ifUser.addAction(Intent.ACTION_USER_BACKGROUND); + ifUser.addAction(Intent.ACTION_USER_FOREGROUND); + registerReceiver(userReceiver, ifUser); + registeredUser = true; + } + + // Listen for idle mode state changes + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + IntentFilter ifIdle = new IntentFilter(); + ifIdle.addAction(PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED); + registerReceiver(idleStateReceiver, ifIdle); + registeredIdleState = true; + } + + // Listen for added/removed applications + IntentFilter ifPackage = new IntentFilter(); + ifPackage.addAction(Intent.ACTION_PACKAGE_ADDED); + ifPackage.addAction(Intent.ACTION_PACKAGE_REMOVED); + ifPackage.addDataScheme("package"); + registerReceiver(packageChangedReceiver, ifPackage); + registeredPackageChanged = true; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + try { + listenNetworkChanges(); + } catch (Throwable ex) { + Log.w(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + listenConnectivityChanges(); + } + else + listenConnectivityChanges(); + + // Monitor networks + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + cm.registerNetworkCallback( + new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build(), + networkMonitorCallback); + + // Setup house holding + Intent alarmIntent = new Intent(this, ServiceSinkhole.class); + alarmIntent.setAction(ACTION_HOUSE_HOLDING); + PendingIntent pi; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + pi = PendingIntent.getForegroundService(this, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + else + pi = PendingIntent.getService(this, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + + AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + am.setInexactRepeating(AlarmManager.RTC, SystemClock.elapsedRealtime() + 60 * 1000, AlarmManager.INTERVAL_HALF_DAY, pi); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void listenNetworkChanges() { + // Listen for network changes + Log.i(TAG, "Starting listening to network changes"); + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkRequest.Builder builder = new NetworkRequest.Builder(); + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + builder.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED); + + ConnectivityManager.NetworkCallback nc = new ConnectivityManager.NetworkCallback() { + private Boolean last_connected = null; + private Boolean last_unmetered = null; + private String last_generation = null; + private List last_dns = null; + + @Override + public void onAvailable(Network network) { + Log.i(TAG, "Available network=" + network); + last_connected = Util.isConnected(ServiceSinkhole.this); + reload("network available", ServiceSinkhole.this, false); + } + + @Override + public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties) { + Log.i(TAG, "Changed properties=" + network + " props=" + linkProperties); + + // Make sure the right DNS servers are being used + List dns = linkProperties.getDnsServers(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O + ? !same(last_dns, dns) + : prefs.getBoolean("reload_onconnectivity", false)) { + Log.i(TAG, "Changed link properties=" + linkProperties + + "DNS cur=" + TextUtils.join(",", dns) + + "DNS prv=" + (last_dns == null ? null : TextUtils.join(",", last_dns))); + last_dns = dns; + reload("link properties changed", ServiceSinkhole.this, false); + } + } + + @Override + public void onCapabilitiesChanged(Network network, NetworkCapabilities networkCapabilities) { + Log.i(TAG, "Changed capabilities=" + network + " caps=" + networkCapabilities); + + boolean connected = Util.isConnected(ServiceSinkhole.this); + boolean unmetered = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED); + String generation = Util.getNetworkGeneration(ServiceSinkhole.this); + Log.i(TAG, "Connected=" + connected + "/" + last_connected + + " unmetered=" + unmetered + "/" + last_unmetered + + " generation=" + generation + "/" + last_generation); + + if (last_connected != null && !last_connected.equals(connected)) + reload("Connected state changed", ServiceSinkhole.this, false); + + if (last_unmetered != null && !last_unmetered.equals(unmetered)) + reload("Unmetered state changed", ServiceSinkhole.this, false); + + if (last_generation != null && !last_generation.equals(generation)) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + if (prefs.getBoolean("unmetered_2g", false) || + prefs.getBoolean("unmetered_3g", false) || + prefs.getBoolean("unmetered_4g", false)) + reload("Generation changed", ServiceSinkhole.this, false); + } + + last_connected = connected; + last_unmetered = unmetered; + last_generation = generation; + } + + @Override + public void onLost(Network network) { + Log.i(TAG, "Lost network=" + network); + last_connected = Util.isConnected(ServiceSinkhole.this); + reload("network lost", ServiceSinkhole.this, false); + } + + boolean same(List last, List current) { + if (last == null || current == null) + return false; + if (last == null || last.size() != current.size()) + return false; + + for (int i = 0; i < current.size(); i++) + if (!last.get(i).equals(current.get(i))) + return false; + + return true; + } + }; + cm.registerNetworkCallback(builder.build(), nc); + networkCallback = nc; + } + + private void listenConnectivityChanges() { + // Listen for connectivity updates + Log.i(TAG, "Starting listening to connectivity changes"); + IntentFilter ifConnectivity = new IntentFilter(); + ifConnectivity.addAction(ConnectivityManager.CONNECTIVITY_ACTION); + registerReceiver(connectivityChangedReceiver, ifConnectivity); + registeredConnectivityChanged = true; + + // Listen for phone state changes + Log.i(TAG, "Starting listening to service state changes"); + TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); + if (tm != null) { + tm.listen(phoneStateListener, PhoneStateListener.LISTEN_DATA_CONNECTION_STATE); + phone_state = true; + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String name) { + if ("theme".equals(name)) { + Log.i(TAG, "Theme changed"); + Util.setTheme(this); + if (state != State.none) { + Log.d(TAG, "Stop foreground state=" + state.toString()); + stopForeground(true); + } + if (state == State.enforcing) + startForeground(NOTIFY_ENFORCING, getEnforcingNotification(-1, -1, -1)); + else if (state != State.none) + startForeground(NOTIFY_WAITING, getWaitingNotification()); + Log.d(TAG, "Start foreground state=" + state.toString()); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (state == State.enforcing) + startForeground(NOTIFY_ENFORCING, getEnforcingNotification(-1, -1, -1)); + else + startForeground(NOTIFY_WAITING, getWaitingNotification()); + + Log.i(TAG, "Received " + intent); + Util.logExtras(intent); + + // Check for set command + if (intent != null && intent.hasExtra(EXTRA_COMMAND) && + intent.getSerializableExtra(EXTRA_COMMAND) == Command.set) { + set(intent); + return START_STICKY; + } + + // Keep awake + getLock(this).acquire(); + + // Get state + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean enabled = prefs.getBoolean("enabled", false); + + // Handle service restart + if (intent == null) { + Log.i(TAG, "Restart"); + + // Recreate intent + intent = new Intent(this, ServiceSinkhole.class); + intent.putExtra(EXTRA_COMMAND, enabled ? Command.start : Command.stop); + } + + if (ACTION_HOUSE_HOLDING.equals(intent.getAction())) + intent.putExtra(EXTRA_COMMAND, Command.householding); + if (ACTION_WATCHDOG.equals(intent.getAction())) + intent.putExtra(EXTRA_COMMAND, Command.watchdog); + + Command cmd = (Command) intent.getSerializableExtra(EXTRA_COMMAND); + if (cmd == null) + intent.putExtra(EXTRA_COMMAND, enabled ? Command.start : Command.stop); + String reason = intent.getStringExtra(EXTRA_REASON); + Log.i(TAG, "Start intent=" + intent + " command=" + cmd + " reason=" + reason + + " vpn=" + (vpn != null) + " user=" + (Process.myUid() / 100000)); + + commandHandler.queue(intent); + + return START_STICKY; + } + + private void set(Intent intent) { + // Get arguments + int uid = intent.getIntExtra(EXTRA_UID, 0); + String network = intent.getStringExtra(EXTRA_NETWORK); + String pkg = intent.getStringExtra(EXTRA_PACKAGE); + boolean blocked = intent.getBooleanExtra(EXTRA_BLOCKED, false); + Log.i(TAG, "Set " + pkg + " " + network + "=" + blocked); + + // Get defaults + SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(ServiceSinkhole.this); + boolean default_wifi = settings.getBoolean("whitelist_wifi", true); + boolean default_other = settings.getBoolean("whitelist_other", true); + + // Update setting + SharedPreferences prefs = getSharedPreferences(network, Context.MODE_PRIVATE); + if (blocked == ("wifi".equals(network) ? default_wifi : default_other)) + prefs.edit().remove(pkg).apply(); + else + prefs.edit().putBoolean(pkg, blocked).apply(); + + // Apply rules + ServiceSinkhole.reload("notification", ServiceSinkhole.this, false); + + // Update notification + notifyNewApplication(uid); + + // Update UI + Intent ruleset = new Intent(ActivityMain.ACTION_RULES_CHANGED); + LocalBroadcastManager.getInstance(ServiceSinkhole.this).sendBroadcast(ruleset); + } + + @Override + public void onRevoke() { + Log.i(TAG, "Revoke"); + + // Disable firewall (will result in stop command) + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.edit().putBoolean("enabled", false).apply(); + + // Feedback + showDisabledNotification(); + WidgetMain.updateWidgets(this); + + super.onRevoke(); + } + + @Override + public void onDestroy() { + synchronized (this) { + Log.i(TAG, "Destroy"); + commandLooper.quit(); + logLooper.quit(); + statsLooper.quit(); + + for (Command command : Command.values()) + commandHandler.removeMessages(command.ordinal()); + releaseLock(this); + + // Registered in command loop + if (registeredInteractiveState) { + unregisterReceiver(interactiveStateReceiver); + registeredInteractiveState = false; + } + if (callStateListener != null) { + TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); + tm.listen(callStateListener, PhoneStateListener.LISTEN_NONE); + callStateListener = null; + } + + // Register in onCreate + if (registeredUser) { + unregisterReceiver(userReceiver); + registeredUser = false; + } + if (registeredIdleState) { + unregisterReceiver(idleStateReceiver); + registeredIdleState = false; + } + if (registeredPackageChanged) { + unregisterReceiver(packageChangedReceiver); + registeredPackageChanged = false; + } + + if (networkCallback != null) { + unlistenNetworkChanges(); + networkCallback = null; + } + if (registeredConnectivityChanged) { + unregisterReceiver(connectivityChangedReceiver); + registeredConnectivityChanged = false; + } + + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + cm.unregisterNetworkCallback(networkMonitorCallback); + + if (phone_state) { + TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); + tm.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE); + phone_state = false; + } + + try { + if (vpn != null) { + stopNative(vpn); + stopVPN(vpn); + vpn = null; + unprepare(); + } + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + + Log.i(TAG, "Destroy context=" + jni_context); + synchronized (jni_lock) { + jni_done(jni_context); + jni_context = 0; + } + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.unregisterOnSharedPreferenceChangeListener(this); + } + + super.onDestroy(); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private void unlistenNetworkChanges() { + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + cm.unregisterNetworkCallback((ConnectivityManager.NetworkCallback) networkCallback); + } + + private Notification getEnforcingNotification(int allowed, int blocked, int hosts) { + Intent main = new Intent(this, ActivityMain.class); + PendingIntent pi = PendingIntent.getActivity(this, 0, main, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + + TypedValue tv = new TypedValue(); + getTheme().resolveAttribute(R.attr.colorPrimary, tv, true); + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "foreground"); + builder.setSmallIcon(isLockedDown(last_metered) ? R.drawable.ic_lock_outline_white_24dp : R.drawable.ic_security_white_24dp) + .setContentIntent(pi) + .setColor(tv.data) + .setOngoing(true) + .setAutoCancel(false); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + builder.setContentTitle(getString(R.string.msg_started)); + else + builder.setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.msg_started)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + builder.setCategory(NotificationCompat.CATEGORY_STATUS) + .setVisibility(NotificationCompat.VISIBILITY_SECRET) + .setPriority(NotificationCompat.PRIORITY_MIN); + + if (allowed >= 0) + last_allowed = allowed; + else + allowed = last_allowed; + if (blocked >= 0) + last_blocked = blocked; + else + blocked = last_blocked; + if (hosts >= 0) + last_hosts = hosts; + else + hosts = last_hosts; + + if (allowed >= 0 || blocked >= 0 || hosts >= 0) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (Util.isPlayStoreInstall(this)) + builder.setContentText(getString(R.string.msg_packages, allowed, blocked)); + else + builder.setContentText(getString(R.string.msg_hosts, allowed, blocked, hosts)); + return builder.build(); + } else { + NotificationCompat.BigTextStyle notification = new NotificationCompat.BigTextStyle(builder); + notification.bigText(getString(R.string.msg_started)); + if (Util.isPlayStoreInstall(this)) + notification.setSummaryText(getString(R.string.msg_packages, allowed, blocked)); + else + notification.setSummaryText(getString(R.string.msg_hosts, allowed, blocked, hosts)); + return notification.build(); + } + } else + return builder.build(); + } + + private void updateEnforcingNotification(int allowed, int total) { + // Update notification + Notification notification = getEnforcingNotification(allowed, total - allowed, mapHostsBlocked.size()); + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.notify(NOTIFY_ENFORCING, notification); + } + + private Notification getWaitingNotification() { + Intent main = new Intent(this, ActivityMain.class); + PendingIntent pi = PendingIntent.getActivity(this, 0, main, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + + TypedValue tv = new TypedValue(); + getTheme().resolveAttribute(R.attr.colorPrimary, tv, true); + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "foreground"); + builder.setSmallIcon(R.drawable.ic_security_white_24dp) + .setContentIntent(pi) + .setColor(tv.data) + .setOngoing(true) + .setAutoCancel(false); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + builder.setContentTitle(getString(R.string.msg_waiting)); + else + builder.setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.msg_waiting)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + builder.setCategory(NotificationCompat.CATEGORY_STATUS) + .setVisibility(NotificationCompat.VISIBILITY_SECRET) + .setPriority(NotificationCompat.PRIORITY_MIN); + + return builder.build(); + } + + private void showDisabledNotification() { + Intent main = new Intent(this, ActivityMain.class); + PendingIntent pi = PendingIntent.getActivity(this, 0, main, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + + TypedValue tv = new TypedValue(); + getTheme().resolveAttribute(R.attr.colorOff, tv, true); + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "notify"); + builder.setSmallIcon(R.drawable.ic_error_white_24dp) + .setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.msg_revoked)) + .setContentIntent(pi) + .setColor(tv.data) + .setOngoing(false) + .setAutoCancel(true); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + builder.setCategory(NotificationCompat.CATEGORY_STATUS) + .setVisibility(NotificationCompat.VISIBILITY_SECRET); + + NotificationCompat.BigTextStyle notification = new NotificationCompat.BigTextStyle(builder); + notification.bigText(getString(R.string.msg_revoked)); + + NotificationManagerCompat.from(this).notify(NOTIFY_DISABLED, notification.build()); + } + + private void showLockdownNotification() { + Intent intent = new Intent(Settings.ACTION_VPN_SETTINGS); + PendingIntent pi = PendingIntent.getActivity(this, NOTIFY_LOCKDOWN, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + + TypedValue tv = new TypedValue(); + getTheme().resolveAttribute(R.attr.colorOff, tv, true); + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "notify"); + builder.setSmallIcon(R.drawable.ic_error_white_24dp) + .setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.msg_always_on_lockdown)) + .setContentIntent(pi) + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setColor(tv.data) + .setOngoing(false) + .setAutoCancel(true); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + builder.setCategory(NotificationCompat.CATEGORY_STATUS) + .setVisibility(NotificationCompat.VISIBILITY_SECRET); + + NotificationCompat.BigTextStyle notification = new NotificationCompat.BigTextStyle(builder); + notification.bigText(getString(R.string.msg_always_on_lockdown)); + + NotificationManagerCompat.from(this).notify(NOTIFY_LOCKDOWN, notification.build()); + } + + private void removeLockdownNotification() { + NotificationManagerCompat.from(this).cancel(NOTIFY_LOCKDOWN); + } + + private void showAutoStartNotification() { + Intent main = new Intent(this, ActivityMain.class); + main.putExtra(ActivityMain.EXTRA_APPROVE, true); + PendingIntent pi = PendingIntent.getActivity(this, NOTIFY_AUTOSTART, main, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + + TypedValue tv = new TypedValue(); + getTheme().resolveAttribute(R.attr.colorOff, tv, true); + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "notify"); + builder.setSmallIcon(R.drawable.ic_error_white_24dp) + .setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.msg_autostart)) + .setContentIntent(pi) + .setColor(tv.data) + .setOngoing(false) + .setAutoCancel(true); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + builder.setCategory(NotificationCompat.CATEGORY_STATUS) + .setVisibility(NotificationCompat.VISIBILITY_SECRET); + + NotificationCompat.BigTextStyle notification = new NotificationCompat.BigTextStyle(builder); + notification.bigText(getString(R.string.msg_autostart)); + + NotificationManagerCompat.from(this).notify(NOTIFY_AUTOSTART, notification.build()); + } + + private void showErrorNotification(String message) { + Intent main = new Intent(this, ActivityMain.class); + PendingIntent pi = PendingIntent.getActivity(this, 0, main, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + + TypedValue tv = new TypedValue(); + getTheme().resolveAttribute(R.attr.colorOff, tv, true); + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "notify"); + builder.setSmallIcon(R.drawable.ic_error_white_24dp) + .setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.msg_error, message)) + .setContentIntent(pi) + .setColor(tv.data) + .setOngoing(false) + .setAutoCancel(true); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + builder.setCategory(NotificationCompat.CATEGORY_STATUS) + .setVisibility(NotificationCompat.VISIBILITY_SECRET); + + NotificationCompat.BigTextStyle notification = new NotificationCompat.BigTextStyle(builder); + notification.bigText(getString(R.string.msg_error, message)); + notification.setSummaryText(message); + + NotificationManagerCompat.from(this).notify(NOTIFY_ERROR, notification.build()); + } + + private void showAccessNotification(int uid) { + String name = TextUtils.join(", ", Util.getApplicationNames(uid, ServiceSinkhole.this)); + + Intent main = new Intent(ServiceSinkhole.this, ActivityMain.class); + main.putExtra(ActivityMain.EXTRA_SEARCH, Integer.toString(uid)); + PendingIntent pi = PendingIntent.getActivity(ServiceSinkhole.this, uid + 10000, main, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + + TypedValue tv = new TypedValue(); + getTheme().resolveAttribute(R.attr.colorOn, tv, true); + int colorOn = tv.data; + getTheme().resolveAttribute(R.attr.colorOff, tv, true); + int colorOff = tv.data; + + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "access"); + builder.setSmallIcon(R.drawable.ic_cloud_upload_white_24dp) + .setGroup("AccessAttempt") + .setContentIntent(pi) + .setColor(colorOff) + .setOngoing(false) + .setAutoCancel(true); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + builder.setContentTitle(name) + .setContentText(getString(R.string.msg_access_n)); + else + builder.setContentTitle(getString(R.string.app_name)) + .setContentText(getString(R.string.msg_access, name)); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + builder.setCategory(NotificationCompat.CATEGORY_STATUS) + .setVisibility(NotificationCompat.VISIBILITY_SECRET); + + DateFormat df = new SimpleDateFormat("dd HH:mm"); + + NotificationCompat.InboxStyle notification = new NotificationCompat.InboxStyle(builder); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + notification.addLine(getString(R.string.msg_access_n)); + else { + String sname = getString(R.string.msg_access, name); + int pos = sname.indexOf(name); + Spannable sp = new SpannableString(sname); + sp.setSpan(new StyleSpan(Typeface.BOLD), pos, pos + name.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + notification.addLine(sp); + } + + long since = 0; + PackageManager pm = getPackageManager(); + String[] packages = pm.getPackagesForUid(uid); + if (packages != null && packages.length > 0) + try { + since = pm.getPackageInfo(packages[0], 0).firstInstallTime; + } catch (PackageManager.NameNotFoundException ignored) { + } + + try (Cursor cursor = DatabaseHelper.getInstance(ServiceSinkhole.this).getAccessUnset(uid, 7, since)) { + int colDAddr = cursor.getColumnIndex("daddr"); + int colTime = cursor.getColumnIndex("time"); + int colAllowed = cursor.getColumnIndex("allowed"); + while (cursor.moveToNext()) { + StringBuilder sb = new StringBuilder(); + sb.append(df.format(cursor.getLong(colTime))).append(' '); + + String daddr = cursor.getString(colDAddr); + if (Util.isNumericAddress(daddr)) + try { + daddr = InetAddress.getByName(daddr).getHostName(); + } catch (UnknownHostException ignored) { + } + sb.append(daddr); + + int allowed = cursor.getInt(colAllowed); + if (allowed >= 0) { + int pos = sb.indexOf(daddr); + Spannable sp = new SpannableString(sb); + ForegroundColorSpan fgsp = new ForegroundColorSpan(allowed > 0 ? colorOn : colorOff); + sp.setSpan(fgsp, pos, pos + daddr.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + notification.addLine(sp); + } else + notification.addLine(sb); + } + } + + NotificationManagerCompat.from(this).notify(uid + 10000, notification.build()); + } + + private void showUpdateNotification(String name, String url) { + Intent download = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + PendingIntent pi = PendingIntent.getActivity(this, 0, download, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + + TypedValue tv = new TypedValue(); + getTheme().resolveAttribute(R.attr.colorPrimary, tv, true); + NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "notify"); + builder.setSmallIcon(R.drawable.ic_security_white_24dp) + .setContentTitle(name) + .setContentText(getString(R.string.msg_update)) + .setContentIntent(pi) + .setColor(tv.data) + .setOngoing(false) + .setAutoCancel(true); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + builder.setCategory(NotificationCompat.CATEGORY_STATUS) + .setVisibility(NotificationCompat.VISIBILITY_SECRET); + + NotificationManagerCompat.from(this).notify(NOTIFY_UPDATE, builder.build()); + } + + private void removeWarningNotifications() { + NotificationManagerCompat.from(this).cancel(NOTIFY_DISABLED); + NotificationManagerCompat.from(this).cancel(NOTIFY_AUTOSTART); + NotificationManagerCompat.from(this).cancel(NOTIFY_ERROR); + } + + private class Builder extends VpnService.Builder { + private NetworkInfo networkInfo; + private int mtu; + private List listAddress = new ArrayList<>(); + private List listRoute = new ArrayList<>(); + private List listDns = new ArrayList<>(); + private List listDisallowed = new ArrayList<>(); + + private Builder() { + super(); + ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); + networkInfo = cm.getActiveNetworkInfo(); + } + + @Override + public VpnService.Builder setMtu(int mtu) { + this.mtu = mtu; + super.setMtu(mtu); + return this; + } + + @Override + public Builder addAddress(String address, int prefixLength) { + listAddress.add(address + "/" + prefixLength); + super.addAddress(address, prefixLength); + return this; + } + + @Override + public Builder addRoute(String address, int prefixLength) { + System.out.println("BPB: adding new route: " + address + "/" + prefixLength); + listRoute.add(address + "/" + prefixLength); + super.addRoute(address, prefixLength); + return this; + } + + @Override + public Builder addRoute(InetAddress address, int prefixLength) { + listRoute.add(address.getHostAddress() + "/" + prefixLength); + super.addRoute(address, prefixLength); + return this; + } + + @Override + public Builder addDnsServer(InetAddress address) { + listDns.add(address); + super.addDnsServer(address); + return this; + } + + @Override + public Builder addDisallowedApplication(String packageName) throws PackageManager.NameNotFoundException { + listDisallowed.add(packageName); + super.addDisallowedApplication(packageName); + return this; + } + + @Override + public boolean equals(Object obj) { + Builder other = (Builder) obj; + + if (other == null) + return false; + + if (this.networkInfo == null || other.networkInfo == null || + this.networkInfo.getType() != other.networkInfo.getType()) + return false; + + if (this.mtu != other.mtu) + return false; + + if (this.listAddress.size() != other.listAddress.size()) + return false; + + if (this.listRoute.size() != other.listRoute.size()) + return false; + + if (this.listDns.size() != other.listDns.size()) + return false; + + if (this.listDisallowed.size() != other.listDisallowed.size()) + return false; + + for (String address : this.listAddress) + if (!other.listAddress.contains(address)) + return false; + + for (String route : this.listRoute) + if (!other.listRoute.contains(route)) + return false; + + for (InetAddress dns : this.listDns) + if (!other.listDns.contains(dns)) + return false; + + for (String pkg : this.listDisallowed) + if (!other.listDisallowed.contains(pkg)) + return false; + + return true; + } + } + + private class IPKey { + int version; + int protocol; + int dport; + int uid; + + public IPKey(int version, int protocol, int dport, int uid) { + this.version = version; + this.protocol = protocol; + // Only TCP (6) and UDP (17) have port numbers + this.dport = (protocol == 6 || protocol == 17 ? dport : 0); + this.uid = uid; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof IPKey)) + return false; + IPKey other = (IPKey) obj; + return (this.version == other.version && + this.protocol == other.protocol && + this.dport == other.dport && + this.uid == other.uid); + } + + @Override + public int hashCode() { + return (version << 40) | (protocol << 32) | (dport << 16) | uid; + } + + @Override + public String toString() { + return "v" + version + " p" + protocol + " port=" + dport + " uid=" + uid; + } + } + + private class IPRule { + private IPKey key; + private String name; + private boolean block; + private long time; + private long ttl; + + public IPRule(IPKey key, String name, boolean block, long time, long ttl) { + this.key = key; + this.name = name; + this.block = block; + this.time = time; + this.ttl = ttl; + } + + public boolean isBlocked() { + return this.block; + } + + public boolean isExpired() { + return System.currentTimeMillis() > (this.time + this.ttl * 2); + } + + public void updateExpires(long time, long ttl) { + this.time = time; + this.ttl = ttl; + } + + @Override + public boolean equals(Object obj) { + IPRule other = (IPRule) obj; + return (this.block == other.block && + this.time == other.time && + this.ttl == other.ttl); + } + + @Override + public String toString() { + return this.key + " " + this.name; + } + } + + public static void run(String reason, Context context) { + Intent intent = new Intent(context, ServiceSinkhole.class); + intent.putExtra(EXTRA_COMMAND, Command.run); + intent.putExtra(EXTRA_REASON, reason); + ContextCompat.startForegroundService(context, intent); + } + + public static void start(String reason, Context context) { + Intent intent = new Intent(context, ServiceSinkhole.class); + intent.putExtra(EXTRA_COMMAND, Command.start); + intent.putExtra(EXTRA_REASON, reason); + ContextCompat.startForegroundService(context, intent); + } + + public static void reload(String reason, Context context, boolean interactive) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + if (prefs.getBoolean("enabled", false)) { + Intent intent = new Intent(context, ServiceSinkhole.class); + intent.putExtra(EXTRA_COMMAND, Command.reload); + intent.putExtra(EXTRA_REASON, reason); + intent.putExtra(EXTRA_INTERACTIVE, interactive); + ContextCompat.startForegroundService(context, intent); + } + } + + public static void stop(String reason, Context context, boolean vpnonly) { + Intent intent = new Intent(context, ServiceSinkhole.class); + intent.putExtra(EXTRA_COMMAND, Command.stop); + intent.putExtra(EXTRA_REASON, reason); + intent.putExtra(EXTRA_TEMPORARY, vpnonly); + ContextCompat.startForegroundService(context, intent); + } + + public static void reloadStats(String reason, Context context) { + Intent intent = new Intent(context, ServiceSinkhole.class); + intent.putExtra(EXTRA_COMMAND, Command.stats); + intent.putExtra(EXTRA_REASON, reason); + ContextCompat.startForegroundService(context, intent); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileFilter.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileFilter.java new file mode 100644 index 0000000..3ffd0d4 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileFilter.java @@ -0,0 +1,81 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + + +import android.annotation.TargetApi; +import android.content.SharedPreferences; +import android.graphics.drawable.Icon; +import android.os.Build; +import android.service.quicksettings.Tile; +import android.service.quicksettings.TileService; +import android.util.Log; +import android.widget.Toast; + +import androidx.preference.PreferenceManager; + +@TargetApi(Build.VERSION_CODES.N) +public class ServiceTileFilter extends TileService implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = "NetGuard.TileFilter"; + + public void onStartListening() { + Log.i(TAG, "Start listening"); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.registerOnSharedPreferenceChangeListener(this); + update(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + if ("filter".equals(key)) + update(); + } + + private void update() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean filter = prefs.getBoolean("filter", false); + Tile tile = getQsTile(); + if (tile != null) { + tile.setState(filter ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE); + tile.setIcon(Icon.createWithResource(this, filter ? R.drawable.ic_filter_list_white_24dp : R.drawable.ic_filter_list_white_24dp_60)); + tile.updateTile(); + } + } + + public void onStopListening() { + Log.i(TAG, "Stop listening"); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.unregisterOnSharedPreferenceChangeListener(this); + } + + public void onClick() { + Log.i(TAG, "Click"); + + if (Util.canFilter(this)) { + if (IAB.isPurchased(ActivityPro.SKU_FILTER, this)) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.edit().putBoolean("filter", !prefs.getBoolean("filter", false)).apply(); + ServiceSinkhole.reload("tile", this, false); + } else + Toast.makeText(this, R.string.title_pro_feature, Toast.LENGTH_SHORT).show(); + } else + Toast.makeText(this, R.string.msg_unavailable, Toast.LENGTH_SHORT).show(); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileGraph.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileGraph.java new file mode 100644 index 0000000..30c38ca --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileGraph.java @@ -0,0 +1,80 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + + +import android.annotation.TargetApi; +import android.content.SharedPreferences; +import android.graphics.drawable.Icon; +import android.os.Build; +import android.service.quicksettings.Tile; +import android.service.quicksettings.TileService; +import android.util.Log; +import android.widget.Toast; + +import androidx.preference.PreferenceManager; + +@TargetApi(Build.VERSION_CODES.N) +public class ServiceTileGraph extends TileService implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = "NetGuard.TileGraph"; + + public void onStartListening() { + Log.i(TAG, "Start listening"); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.registerOnSharedPreferenceChangeListener(this); + update(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + if ("show_stats".equals(key)) + update(); + } + + private void update() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean stats = prefs.getBoolean("show_stats", false); + Tile tile = getQsTile(); + if (tile != null) { + tile.setState(stats ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE); + tile.setIcon(Icon.createWithResource(this, stats ? R.drawable.ic_equalizer_white_24dp : R.drawable.ic_equalizer_white_24dp_60)); + tile.updateTile(); + } + } + + public void onStopListening() { + Log.i(TAG, "Stop listening"); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.unregisterOnSharedPreferenceChangeListener(this); + } + + public void onClick() { + Log.i(TAG, "Click"); + + // Check state + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean stats = !prefs.getBoolean("show_stats", false); + if (stats && !IAB.isPurchased(ActivityPro.SKU_SPEED, this)) + Toast.makeText(this, R.string.title_pro_feature, Toast.LENGTH_SHORT).show(); + else + prefs.edit().putBoolean("show_stats", stats).apply(); + ServiceSinkhole.reloadStats("tile", this); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileLockdown.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileLockdown.java new file mode 100644 index 0000000..1aaffea --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileLockdown.java @@ -0,0 +1,75 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + + +import android.annotation.TargetApi; +import android.content.SharedPreferences; +import android.graphics.drawable.Icon; +import android.os.Build; +import android.service.quicksettings.Tile; +import android.service.quicksettings.TileService; +import android.util.Log; + +import androidx.preference.PreferenceManager; + +@TargetApi(Build.VERSION_CODES.N) +public class ServiceTileLockdown extends TileService implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = "NetGuard.TileLockdown"; + + public void onStartListening() { + Log.i(TAG, "Start listening"); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.registerOnSharedPreferenceChangeListener(this); + update(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + if ("lockdown".equals(key)) + update(); + } + + private void update() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean lockdown = prefs.getBoolean("lockdown", false); + Tile tile = getQsTile(); + if (tile != null) { + tile.setState(lockdown ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE); + tile.setIcon(Icon.createWithResource(this, lockdown ? R.drawable.ic_lock_outline_white_24dp : R.drawable.ic_lock_outline_white_24dp_60)); + tile.updateTile(); + } + } + + public void onStopListening() { + Log.i(TAG, "Stop listening"); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.unregisterOnSharedPreferenceChangeListener(this); + } + + public void onClick() { + Log.i(TAG, "Click"); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.edit().putBoolean("lockdown", !prefs.getBoolean("lockdown", false)).apply(); + ServiceSinkhole.reload("tile", this, false); + WidgetLockdown.updateWidgets(this); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileMain.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileMain.java new file mode 100644 index 0000000..655819f --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileMain.java @@ -0,0 +1,103 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + + +import android.annotation.TargetApi; +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.drawable.Icon; +import android.os.Build; +import android.service.quicksettings.Tile; +import android.service.quicksettings.TileService; +import android.util.Log; + +import androidx.preference.PreferenceManager; + +import java.util.Date; + +@TargetApi(Build.VERSION_CODES.N) +public class ServiceTileMain extends TileService implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = "NetGuard.TileMain"; + + public void onStartListening() { + Log.i(TAG, "Start listening"); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.registerOnSharedPreferenceChangeListener(this); + update(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + if ("enabled".equals(key)) + update(); + } + + private void update() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean enabled = prefs.getBoolean("enabled", false); + Tile tile = getQsTile(); + if (tile != null) { + tile.setState(enabled ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE); + tile.setIcon(Icon.createWithResource(this, enabled ? R.drawable.ic_security_white_24dp : R.drawable.ic_security_white_24dp_60)); + tile.updateTile(); + } + } + + public void onStopListening() { + Log.i(TAG, "Stop listening"); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + prefs.unregisterOnSharedPreferenceChangeListener(this); + } + + public void onClick() { + Log.i(TAG, "Click"); + + // Cancel set alarm + AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(WidgetAdmin.INTENT_ON); + intent.setPackage(getPackageName()); + PendingIntent pi = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + am.cancel(pi); + + // Check state + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean enabled = !prefs.getBoolean("enabled", false); + prefs.edit().putBoolean("enabled", enabled).apply(); + if (enabled) + ServiceSinkhole.start("tile", this); + else { + ServiceSinkhole.stop("tile", this, false); + + // Auto enable + int auto = Integer.parseInt(prefs.getString("auto_enable", "0")); + if (auto > 0) { + Log.i(TAG, "Scheduling enabled after minutes=" + auto); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + am.set(AlarmManager.RTC_WAKEUP, new Date().getTime() + auto * 60 * 1000L, pi); + else + am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, new Date().getTime() + auto * 60 * 1000L, pi); + } + } + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/SwitchPreference.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/SwitchPreference.java new file mode 100644 index 0000000..c8192c6 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/SwitchPreference.java @@ -0,0 +1,39 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.content.Context; +import android.util.AttributeSet; + +// https://code.google.com/p/android/issues/detail?id=26194 + +public class SwitchPreference extends android.preference.SwitchPreference { + public SwitchPreference(Context context) { + this(context, null); + } + + public SwitchPreference(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.switchPreferenceStyle); + } + + public SwitchPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/Usage.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/Usage.java new file mode 100644 index 0000000..ccfcb7e --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/Usage.java @@ -0,0 +1,46 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class Usage { + public long Time; + public int Version; + public int Protocol; + public String DAddr; + public int DPort; + public int Uid; + public long Sent; + public long Received; + + private static DateFormat formatter = SimpleDateFormat.getDateTimeInstance(); + + @Override + public String toString() { + return formatter.format(new Date(Time).getTime()) + + " v" + Version + " p" + Protocol + + " " + DAddr + "/" + DPort + + " uid " + Uid + + " out " + Sent + " in " + Received; + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/Util.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/Util.java new file mode 100644 index 0000000..1cae9fb --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/Util.java @@ -0,0 +1,1075 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.Manifest; +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ApplicationErrorReport; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.ConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkInfo; +import android.net.Uri; +import android.net.VpnService; +import android.net.wifi.WifiManager; +import android.os.AsyncTask; +import android.os.Build; +import android.os.Bundle; +import android.os.Debug; +import android.os.PowerManager; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.appcompat.app.AlertDialog; +import androidx.core.net.ConnectivityManagerCompat; +import androidx.preference.PreferenceManager; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.DateFormat; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class Util { + private static final String TAG = "NetGuard.Util"; + + // Roam like at home + private static final List listEU = Arrays.asList( + "AT", // Austria + "BE", // Belgium + "BG", // Bulgaria + "HR", // Croatia + "CY", // Cyprus + "CZ", // Czech Republic + "DK", // Denmark + "EE", // Estonia + "FI", // Finland + "FR", // France + "DE", // Germany + "GR", // Greece + "HU", // Hungary + "IS", // Iceland + "IE", // Ireland + "IT", // Italy + "LV", // Latvia + "LI", // Liechtenstein + "LT", // Lithuania + "LU", // Luxembourg + "MT", // Malta + "NL", // Netherlands + "NO", // Norway + "PL", // Poland + "PT", // Portugal + "RE", // La Réunion + "RO", // Romania + "SK", // Slovakia + "SI", // Slovenia + "ES", // Spain + "SE" // Sweden + ); + + private static native String jni_getprop(String name); + + private static native boolean is_numeric_address(String ip); + + private static native void dump_memory_profile(); + + static { + try { + System.loadLibrary("netguard"); + } catch (UnsatisfiedLinkError ignored) { + System.exit(1); + } + } + + public static String getSelfVersionName(Context context) { + try { + PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + return pInfo.versionName; + } catch (PackageManager.NameNotFoundException ex) { + return ex.toString(); + } + } + + public static int getSelfVersionCode(Context context) { + try { + PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); + return pInfo.versionCode; + } catch (PackageManager.NameNotFoundException ex) { + return -1; + } + } + + public static boolean isNetworkActive(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + return (cm != null && cm.getActiveNetworkInfo() != null); + } + + public static boolean isConnected(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm == null) + return false; + + NetworkInfo ni = cm.getActiveNetworkInfo(); + if (ni != null && ni.isConnected()) + return true; + + Network[] networks = cm.getAllNetworks(); + if (networks == null) + return false; + + for (Network network : networks) { + ni = cm.getNetworkInfo(network); + if (ni != null && ni.getType() != ConnectivityManager.TYPE_VPN && ni.isConnected()) + return true; + } + + return false; + } + + public static boolean isWifiActive(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = (cm == null ? null : cm.getActiveNetworkInfo()); + return (ni != null && ni.getType() == ConnectivityManager.TYPE_WIFI); + } + + public static boolean isMeteredNetwork(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + return (cm != null && ConnectivityManagerCompat.isActiveNetworkMetered(cm)); + } + + public static String getWifiSSID(Context context) { + WifiManager wm = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + String ssid = (wm == null ? null : wm.getConnectionInfo().getSSID()); + return (ssid == null ? "NULL" : ssid); + } + + public static int getNetworkType(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = (cm == null ? null : cm.getActiveNetworkInfo()); + return (ni == null ? TelephonyManager.NETWORK_TYPE_UNKNOWN : ni.getSubtype()); + } + + public static String getNetworkGeneration(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = cm.getActiveNetworkInfo(); + return (ni != null && ni.getType() == ConnectivityManager.TYPE_MOBILE ? getNetworkGeneration(ni.getSubtype()) : null); + } + + public static boolean isRoaming(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo ni = (cm == null ? null : cm.getActiveNetworkInfo()); + return (ni != null && ni.isRoaming()); + } + + public static boolean isNational(Context context) { + try { + TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + return (tm != null && tm.getSimCountryIso() != null && tm.getSimCountryIso().equals(tm.getNetworkCountryIso())); + } catch (Throwable ignored) { + return false; + } + } + + public static boolean isEU(Context context) { + try { + TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + return (tm != null && isEU(tm.getSimCountryIso()) && isEU(tm.getNetworkCountryIso())); + } catch (Throwable ignored) { + return false; + } + } + + public static boolean isEU(String country) { + return (country != null && listEU.contains(country.toUpperCase())); + } + + public static boolean isPrivateDns(Context context) { + String dns_mode = Settings.Global.getString(context.getContentResolver(), "private_dns_mode"); + Log.i(TAG, "Private DNS mode=" + dns_mode); + if (dns_mode == null) + dns_mode = "off"; + return (!"off".equals(dns_mode)); + } + + public static String getPrivateDnsSpecifier(Context context) { + String dns_mode = Settings.Global.getString(context.getContentResolver(), "private_dns_mode"); + if ("hostname".equals(dns_mode)) + return Settings.Global.getString(context.getContentResolver(), "private_dns_specifier"); + else + return null; + } + + public static String getNetworkGeneration(int networkType) { + switch (networkType) { + case TelephonyManager.NETWORK_TYPE_1xRTT: + case TelephonyManager.NETWORK_TYPE_CDMA: + case TelephonyManager.NETWORK_TYPE_EDGE: + case TelephonyManager.NETWORK_TYPE_GPRS: + case TelephonyManager.NETWORK_TYPE_IDEN: + case TelephonyManager.NETWORK_TYPE_GSM: + return "2G"; + + case TelephonyManager.NETWORK_TYPE_EHRPD: + case TelephonyManager.NETWORK_TYPE_EVDO_0: + case TelephonyManager.NETWORK_TYPE_EVDO_A: + case TelephonyManager.NETWORK_TYPE_EVDO_B: + case TelephonyManager.NETWORK_TYPE_HSDPA: + case TelephonyManager.NETWORK_TYPE_HSPA: + case TelephonyManager.NETWORK_TYPE_HSPAP: + case TelephonyManager.NETWORK_TYPE_HSUPA: + case TelephonyManager.NETWORK_TYPE_UMTS: + case TelephonyManager.NETWORK_TYPE_TD_SCDMA: + return "3G"; + + case TelephonyManager.NETWORK_TYPE_LTE: + case TelephonyManager.NETWORK_TYPE_IWLAN: + return "4G"; + + default: + return "?G"; + } + } + + public static boolean hasPhoneStatePermission(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + return (context.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED); + else + return true; + } + + public static List getDefaultDNS(Context context) { + List listDns = new ArrayList<>(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + Network an = cm.getActiveNetwork(); + if (an != null) { + LinkProperties lp = cm.getLinkProperties(an); + if (lp != null) { + List dns = lp.getDnsServers(); + if (dns != null) + for (InetAddress d : dns) { + Log.i(TAG, "DNS from LP: " + d.getHostAddress()); + listDns.add(d.getHostAddress().split("%")[0]); + } + } + } + } else { + String dns1 = jni_getprop("net.dns1"); + String dns2 = jni_getprop("net.dns2"); + if (dns1 != null) + listDns.add(dns1.split("%")[0]); + if (dns2 != null) + listDns.add(dns2.split("%")[0]); + } + + return listDns; + } + + public static boolean isNumericAddress(String ip) { + return is_numeric_address(ip); + } + + public static boolean isInteractive(Context context) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT_WATCH) + return (pm != null && pm.isScreenOn()); + else + return (pm != null && pm.isInteractive()); + } + + public static boolean isPackageInstalled(String packageName, Context context) { + try { + context.getPackageManager().getPackageInfo(packageName, 0); + return true; + } catch (PackageManager.NameNotFoundException ignored) { + return false; + } + } + + public static boolean isSystem(int uid, Context context) { + PackageManager pm = context.getPackageManager(); + String[] pkgs = pm.getPackagesForUid(uid); + if (pkgs != null) + for (String pkg : pkgs) + if (isSystem(pkg, context)) + return true; + return false; + } + + public static boolean isSystem(String packageName, Context context) { + try { + PackageManager pm = context.getPackageManager(); + PackageInfo info = pm.getPackageInfo(packageName, 0); + return ((info.applicationInfo.flags & (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0); + /* + PackageInfo pkg = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES); + PackageInfo sys = pm.getPackageInfo("android", PackageManager.GET_SIGNATURES); + return (pkg != null && pkg.signatures != null && pkg.signatures.length > 0 && + sys.signatures.length > 0 && sys.signatures[0].equals(pkg.signatures[0])); + */ + } catch (PackageManager.NameNotFoundException ignore) { + return false; + } + } + + public static boolean hasInternet(String packageName, Context context) { + PackageManager pm = context.getPackageManager(); + return (pm.checkPermission("android.permission.INTERNET", packageName) == PackageManager.PERMISSION_GRANTED); + } + + public static boolean hasInternet(int uid, Context context) { + PackageManager pm = context.getPackageManager(); + String[] pkgs = pm.getPackagesForUid(uid); + if (pkgs != null) + for (String pkg : pkgs) + if (hasInternet(pkg, context)) + return true; + return false; + } + + public static boolean isEnabled(PackageInfo info, Context context) { + int setting; + try { + PackageManager pm = context.getPackageManager(); + setting = pm.getApplicationEnabledSetting(info.packageName); + } catch (IllegalArgumentException ex) { + setting = PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; + Log.w(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + if (setting == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) + return info.applicationInfo.enabled; + else + return (setting == PackageManager.COMPONENT_ENABLED_STATE_ENABLED); + } + + public static List getApplicationNames(int uid, Context context) { + List listResult = new ArrayList<>(); + if (uid == 0) + listResult.add(context.getString(R.string.title_root)); + else if (uid == 1013) + listResult.add(context.getString(R.string.title_mediaserver)); + else if (uid == 9999) + listResult.add(context.getString(R.string.title_nobody)); + else { + PackageManager pm = context.getPackageManager(); + String[] pkgs = pm.getPackagesForUid(uid); + if (pkgs == null) + listResult.add(Integer.toString(uid)); + else + for (String pkg : pkgs) + try { + ApplicationInfo info = pm.getApplicationInfo(pkg, 0); + listResult.add(pm.getApplicationLabel(info).toString()); + } catch (PackageManager.NameNotFoundException ignored) { + } + Collections.sort(listResult); + } + return listResult; + } + + public static boolean canFilter(Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + return true; + + // https://android-review.googlesource.com/#/c/206710/1/untrusted_app.te + File tcp = new File("/proc/net/tcp"); + File tcp6 = new File("/proc/net/tcp6"); + try { + if (tcp.exists() && tcp.canRead()) + return true; + } catch (SecurityException ignored) { + } + try { + return (tcp6.exists() && tcp6.canRead()); + } catch (SecurityException ignored) { + return false; + } + } + + public static boolean isDebuggable(Context context) { + return ((context.getApplicationContext().getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0); + } + + public static boolean isPlayStoreInstall(Context context) { + if (BuildConfig.PLAY_STORE_RELEASE) + return true; + try { + return "com.android.vending".equals(context.getPackageManager().getInstallerPackageName(context.getPackageName())); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + return false; + } + } + + public static boolean hasXposed(Context context) { + if (true || !isPlayStoreInstall(context)) + return false; + for (StackTraceElement ste : Thread.currentThread().getStackTrace()) + if (ste.getClassName().startsWith("de.robv.android.xposed")) + return true; + return false; + } + + public static boolean ownFault(Context context, Throwable ex) { + if (ex instanceof OutOfMemoryError) + return false; + if (ex.getCause() != null) + ex = ex.getCause(); + for (StackTraceElement ste : ex.getStackTrace()) + if (ste.getClassName().startsWith(context.getPackageName())) + return true; + return false; + } + + public static String getFingerprint(Context context) { + try { + PackageManager pm = context.getPackageManager(); + String pkg = context.getPackageName(); + PackageInfo info = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES); + byte[] cert = info.signatures[0].toByteArray(); + MessageDigest digest = MessageDigest.getInstance("SHA1"); + byte[] bytes = digest.digest(cert); + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) + sb.append(Integer.toString(b & 0xff, 16).toLowerCase()); + return sb.toString(); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + return null; + } + } + + public static boolean hasValidFingerprint(Context context) { + String calculated = getFingerprint(context); + String expected = context.getString(R.string.fingerprint); + return (calculated != null && calculated.equals(expected)); + } + + public static void setTheme(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean dark = prefs.getBoolean("dark_theme", false); + String theme = prefs.getString("theme", "teal"); + if (theme.equals("teal")) + context.setTheme(dark ? R.style.AppThemeTealDark : R.style.AppThemeTeal); + else if (theme.equals("blue")) + context.setTheme(dark ? R.style.AppThemeBlueDark : R.style.AppThemeBlue); + else if (theme.equals("purple")) + context.setTheme(dark ? R.style.AppThemePurpleDark : R.style.AppThemePurple); + else if (theme.equals("amber")) + context.setTheme(dark ? R.style.AppThemeAmberDark : R.style.AppThemeAmber); + else if (theme.equals("orange")) + context.setTheme(dark ? R.style.AppThemeOrangeDark : R.style.AppThemeOrange); + else if (theme.equals("green")) + context.setTheme(dark ? R.style.AppThemeGreenDark : R.style.AppThemeGreen); + + if (context instanceof Activity && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + setTaskColor(context); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static void setTaskColor(Context context) { + TypedValue tv = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.colorPrimary, tv, true); + ((Activity) context).setTaskDescription(new ActivityManager.TaskDescription(null, null, tv.data)); + } + + public static int dips2pixels(int dips, Context context) { + return Math.round(dips * context.getResources().getDisplayMetrics().density + 0.5f); + } + + private static int calculateInSampleSize( + BitmapFactory.Options options, int reqWidth, int reqHeight) { + int height = options.outHeight; + int width = options.outWidth; + int inSampleSize = 1; + + if (height > reqHeight || width > reqWidth) { + int halfHeight = height / 2; + int halfWidth = width / 2; + + while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) + inSampleSize *= 2; + } + + return inSampleSize; + } + + public static Bitmap decodeSampledBitmapFromResource( + Resources resources, int resourceId, int reqWidth, int reqHeight) { + + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeResource(resources, resourceId, options); + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + options.inJustDecodeBounds = false; + + return BitmapFactory.decodeResource(resources, resourceId, options); + } + + public static String getProtocolName(int protocol, int version, boolean brief) { + // https://en.wikipedia.org/wiki/List_of_IP_protocol_numbers + String p = null; + String b = null; + switch (protocol) { + case 0: + p = "HOPO"; + b = "H"; + break; + case 2: + p = "IGMP"; + b = "G"; + break; + case 1: + case 58: + p = "ICMP"; + b = "I"; + break; + case 6: + p = "TCP"; + b = "T"; + break; + case 17: + p = "UDP"; + b = "U"; + break; + case 50: + p = "ESP"; + b = "E"; + break; + } + if (p == null) + return Integer.toString(protocol) + "/" + version; + return ((brief ? b : p) + (version > 0 ? version : "")); + } + + public interface DoubtListener { + void onSure(); + } + + public static void areYouSure(Context context, int explanation, final DoubtListener listener) { + LayoutInflater inflater = LayoutInflater.from(context); + View view = inflater.inflate(R.layout.sure, null, false); + TextView tvExplanation = view.findViewById(R.id.tvExplanation); + tvExplanation.setText(explanation); + new AlertDialog.Builder(context) + .setView(view) + .setCancelable(true) + .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + listener.onSure(); + } + }) + .setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Do nothing + } + }) + .create().show(); + } + + private static final Map mapIPOrganization = new HashMap<>(); + + public static String getOrganization(String ip) throws Exception { + synchronized (mapIPOrganization) { + if (mapIPOrganization.containsKey(ip)) + return mapIPOrganization.get(ip); + } + BufferedReader reader = null; + try { + URL url = new URL("https://ipinfo.io/" + ip + "/org"); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod("GET"); + connection.setReadTimeout(15 * 1000); + connection.connect(); + reader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String organization = reader.readLine(); + if ("undefined".equals(organization)) + organization = null; + synchronized (mapIPOrganization) { + mapIPOrganization.put(ip, organization); + } + return organization; + } finally { + if (reader != null) + reader.close(); + } + } + + public static String md5(String text, String salt) throws NoSuchAlgorithmException, UnsupportedEncodingException { + // MD5 + byte[] bytes = MessageDigest.getInstance("MD5").digest((text + salt).getBytes("UTF-8")); + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) + sb.append(String.format("%02X", b)); + return sb.toString(); + } + + public static void logExtras(Intent intent) { + if (intent != null) + logBundle(intent.getExtras()); + } + + public static void logBundle(Bundle data) { + if (data != null) { + Set keys = data.keySet(); + StringBuilder stringBuilder = new StringBuilder(); + for (String key : keys) { + Object value = data.get(key); + stringBuilder.append(key) + .append("=") + .append(value) + .append(value == null ? "" : " (" + value.getClass().getSimpleName() + ")") + .append("\r\n"); + } + Log.d(TAG, stringBuilder.toString()); + } + } + + public static StringBuilder readString(InputStreamReader reader) { + StringBuilder sb = new StringBuilder(2048); + char[] read = new char[128]; + try { + for (int i; (i = reader.read(read)) >= 0; sb.append(read, 0, i)) ; + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + return sb; + } + + public static void sendCrashReport(Throwable ex, final Context context) { + if (!isPlayStoreInstall(context) || Util.isDebuggable(context)) + return; + + try { + ApplicationErrorReport report = new ApplicationErrorReport(); + report.packageName = report.processName = context.getPackageName(); + report.time = System.currentTimeMillis(); + report.type = ApplicationErrorReport.TYPE_CRASH; + report.systemApp = false; + + ApplicationErrorReport.CrashInfo crash = new ApplicationErrorReport.CrashInfo(); + crash.exceptionClassName = ex.getClass().getSimpleName(); + crash.exceptionMessage = ex.getMessage(); + + StringWriter writer = new StringWriter(); + PrintWriter printer = new PrintWriter(writer); + ex.printStackTrace(printer); + + crash.stackTrace = writer.toString(); + + StackTraceElement stack = ex.getStackTrace()[0]; + crash.throwClassName = stack.getClassName(); + crash.throwFileName = stack.getFileName(); + crash.throwLineNumber = stack.getLineNumber(); + crash.throwMethodName = stack.getMethodName(); + + report.crashInfo = crash; + + final Intent bug = new Intent(Intent.ACTION_APP_ERROR); + bug.putExtra(Intent.EXTRA_BUG_REPORT, report); + bug.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (bug.resolveActivity(context.getPackageManager()) != null) + context.startActivity(bug); + } catch (Throwable exex) { + Log.e(TAG, exex.toString() + "\n" + Log.getStackTraceString(exex)); + } + } + + public static String getGeneralInfo(Context context) { + StringBuilder sb = new StringBuilder(); + TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + + sb.append(String.format("Interactive %B\r\n", isInteractive(context))); + sb.append(String.format("Connected %B\r\n", isConnected(context))); + sb.append(String.format("WiFi %B\r\n", isWifiActive(context))); + sb.append(String.format("Metered %B\r\n", isMeteredNetwork(context))); + sb.append(String.format("Roaming %B\r\n", isRoaming(context))); + + if (tm.getSimState() == TelephonyManager.SIM_STATE_READY) + sb.append(String.format("SIM %s/%s/%s\r\n", tm.getSimCountryIso(), tm.getSimOperatorName(), tm.getSimOperator())); + //if (tm.getNetworkType() != TelephonyManager.NETWORK_TYPE_UNKNOWN) + try { + sb.append(String.format("Network %s/%s/%s\r\n", tm.getNetworkCountryIso(), tm.getNetworkOperatorName(), tm.getNetworkOperator())); + } catch (Throwable ex) { + /* + 06-14 13:02:41.331 19703 19703 W ircode.netguar: Accessing hidden method Landroid/view/View;->computeFitSystemWindows(Landroid/graphics/Rect;Landroid/graphics/Rect;)Z (greylist, reflection, allowed) + 06-14 13:02:41.332 19703 19703 W ircode.netguar: Accessing hidden method Landroid/view/ViewGroup;->makeOptionalFitsSystemWindows()V (greylist, reflection, allowed) + 06-14 13:02:41.495 19703 19703 I TetheringManager: registerTetheringEventCallback:eu.faircode.netguard + 06-14 13:02:41.518 19703 19703 E AndroidRuntime: Process: eu.faircode.netguard, PID: 19703 + 06-14 13:02:41.518 19703 19703 E AndroidRuntime: at eu.faircode.netguard.Util.getGeneralInfo(SourceFile:744) + 06-14 13:02:41.518 19703 19703 E AndroidRuntime: at eu.faircode.netguard.ActivitySettings.updateTechnicalInfo(SourceFile:858) + 06-14 13:02:41.518 19703 19703 E AndroidRuntime: at eu.faircode.netguard.ActivitySettings.onPostCreate(SourceFile:425) + 06-14 13:02:41.520 19703 19703 W NetGuard.App: java.lang.SecurityException: getDataNetworkTypeForSubscriber + 06-14 13:02:41.520 19703 19703 W NetGuard.App: java.lang.SecurityException: getDataNetworkTypeForSubscriber + 06-14 13:02:41.520 19703 19703 W NetGuard.App: at android.os.Parcel.createExceptionOrNull(Parcel.java:2373) + 06-14 13:02:41.520 19703 19703 W NetGuard.App: at android.os.Parcel.createException(Parcel.java:2357) + 06-14 13:02:41.520 19703 19703 W NetGuard.App: at android.os.Parcel.readException(Parcel.java:2340) + 06-14 13:02:41.520 19703 19703 W NetGuard.App: at android.os.Parcel.readException(Parcel.java:2282) + 06-14 13:02:41.520 19703 19703 W NetGuard.App: at com.android.internal.telephony.ITelephony$Stub$Proxy.getNetworkTypeForSubscriber(ITelephony.java:8711) + 06-14 13:02:41.520 19703 19703 W NetGuard.App: at android.telephony.TelephonyManager.getNetworkType(TelephonyManager.java:2945) + 06-14 13:02:41.520 19703 19703 W NetGuard.App: at android.telephony.TelephonyManager.getNetworkType(TelephonyManager.java:2909) + */ + } + + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + sb.append(String.format("Power saving %B\r\n", pm.isPowerSaveMode())); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + sb.append(String.format("Battery optimizing %B\r\n", batteryOptimizing(context))); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + sb.append(String.format("Data saving %B\r\n", dataSaving(context))); + + if (sb.length() > 2) + sb.setLength(sb.length() - 2); + + return sb.toString(); + } + + public static String getNetworkInfo(Context context) { + StringBuilder sb = new StringBuilder(); + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + + NetworkInfo ani = cm.getActiveNetworkInfo(); + List listNI = new ArrayList<>(); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + listNI.addAll(Arrays.asList(cm.getAllNetworkInfo())); + else + for (Network network : cm.getAllNetworks()) { + NetworkInfo ni = cm.getNetworkInfo(network); + if (ni != null) + listNI.add(ni); + } + + for (NetworkInfo ni : listNI) { + sb.append(ni.getTypeName()).append('/').append(ni.getSubtypeName()) + .append(' ').append(ni.getDetailedState()) + .append(TextUtils.isEmpty(ni.getExtraInfo()) ? "" : " " + ni.getExtraInfo()) + .append(ni.getType() == ConnectivityManager.TYPE_MOBILE ? " " + Util.getNetworkGeneration(ni.getSubtype()) : "") + .append(ni.isRoaming() ? " R" : "") + .append(ani != null && ni.getType() == ani.getType() && ni.getSubtype() == ani.getSubtype() ? " *" : "") + .append("\r\n"); + } + + try { + Enumeration nis = NetworkInterface.getNetworkInterfaces(); + if (nis != null) + while (nis.hasMoreElements()) { + NetworkInterface ni = nis.nextElement(); + if (ni != null && !ni.isLoopback()) { + List ias = ni.getInterfaceAddresses(); + if (ias != null) + for (InterfaceAddress ia : ias) + sb.append(ni.getName()) + .append(' ').append(ia.getAddress().getHostAddress()) + .append('/').append(ia.getNetworkPrefixLength()) + .append(' ').append(ni.getMTU()) + .append(' ').append(ni.isUp() ? '^' : 'v') + .append("\r\n"); + } + } + } catch (Throwable ex) { + sb.append(ex.toString()).append("\r\n"); + } + + if (sb.length() > 2) + sb.setLength(sb.length() - 2); + + return sb.toString(); + } + + @TargetApi(Build.VERSION_CODES.M) + public static boolean batteryOptimizing(Context context) { + PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return !pm.isIgnoringBatteryOptimizations(context.getPackageName()); + } + + @TargetApi(Build.VERSION_CODES.N) + public static boolean dataSaving(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + return (cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED); + } + + public static void sendLogcat(final Uri uri, final Context context) { + AsyncTask task = new AsyncTask() { + @Override + protected Intent doInBackground(Object... objects) { + StringBuilder sb = new StringBuilder(); + sb.append(context.getString(R.string.msg_issue)); + sb.append("\r\n\r\n\r\n\r\n"); + + // Get version info + String version = getSelfVersionName(context); + sb.append(String.format("NetGuard: %s/%d\r\n", version, getSelfVersionCode(context))); + sb.append(String.format("Android: %s (SDK %d)\r\n", Build.VERSION.RELEASE, Build.VERSION.SDK_INT)); + sb.append("\r\n"); + + // Get device info + sb.append(String.format("Brand: %s\r\n", Build.BRAND)); + sb.append(String.format("Manufacturer: %s\r\n", Build.MANUFACTURER)); + sb.append(String.format("Model: %s\r\n", Build.MODEL)); + sb.append(String.format("Product: %s\r\n", Build.PRODUCT)); + sb.append(String.format("Device: %s\r\n", Build.DEVICE)); + sb.append(String.format("Host: %s\r\n", Build.HOST)); + sb.append(String.format("Display: %s\r\n", Build.DISPLAY)); + sb.append(String.format("Id: %s\r\n", Build.ID)); + sb.append(String.format("Fingerprint: %B\r\n", hasValidFingerprint(context))); + + String abi; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + abi = Build.CPU_ABI; + else + abi = (Build.SUPPORTED_ABIS.length > 0 ? Build.SUPPORTED_ABIS[0] : "?"); + sb.append(String.format("ABI: %s\r\n", abi)); + + Runtime rt = Runtime.getRuntime(); + long hused = (rt.totalMemory() - rt.freeMemory()) / 1024L; + long hmax = rt.maxMemory() / 1024L; + long nheap = Debug.getNativeHeapAllocatedSize() / 1024L; + NumberFormat nf = NumberFormat.getIntegerInstance(); + sb.append(String.format("Heap usage: %s/%s KiB native: %s KiB\r\n", + nf.format(hused), nf.format(hmax), nf.format(nheap))); + + sb.append("\r\n"); + + sb.append(String.format("VPN dialogs: %B\r\n", isPackageInstalled("com.android.vpndialogs", context))); + try { + sb.append(String.format("Prepared: %B\r\n", VpnService.prepare(context) == null)); + } catch (Throwable ex) { + sb.append("Prepared: ").append((ex.toString())).append("\r\n").append(Log.getStackTraceString(ex)); + } + sb.append("\r\n"); + + sb.append(getGeneralInfo(context)); + sb.append("\r\n\r\n"); + sb.append(getNetworkInfo(context)); + sb.append("\r\n\r\n"); + + // Get DNS + sb.append("DNS system:\r\n"); + for (String dns : getDefaultDNS(context)) + sb.append("- ").append(dns).append("\r\n"); + sb.append("DNS VPN:\r\n"); + for (InetAddress dns : ServiceSinkhole.getDns(context)) + sb.append("- ").append(dns).append("\r\n"); + sb.append("\r\n"); + + // Get TCP connection info + String line; + BufferedReader in; + try { + sb.append("/proc/net/tcp:\r\n"); + in = new BufferedReader(new FileReader("/proc/net/tcp")); + while ((line = in.readLine()) != null) + sb.append(line).append("\r\n"); + in.close(); + sb.append("\r\n"); + + sb.append("/proc/net/tcp6:\r\n"); + in = new BufferedReader(new FileReader("/proc/net/tcp6")); + while ((line = in.readLine()) != null) + sb.append(line).append("\r\n"); + in.close(); + sb.append("\r\n"); + + } catch (IOException ex) { + sb.append(ex.toString()).append("\r\n"); + } + + // Get settings + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + Map all = prefs.getAll(); + for (String key : all.keySet()) + sb.append("Setting: ").append(key).append('=').append(all.get(key)).append("\r\n"); + sb.append("\r\n"); + + // Write logcat + dump_memory_profile(); + OutputStream out = null; + try { + Log.i(TAG, "Writing logcat URI=" + uri); + out = context.getContentResolver().openOutputStream(uri); + out.write(getLogcat().toString().getBytes()); + out.write(getTrafficLog(context).toString().getBytes()); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + sb.append(ex.toString()).append("\r\n").append(Log.getStackTraceString(ex)).append("\r\n"); + } finally { + if (out != null) + try { + out.close(); + } catch (IOException ignored) { + } + } + + // Build intent + Intent sendEmail = new Intent(Intent.ACTION_SEND); + sendEmail.setType("message/rfc822"); + sendEmail.putExtra(Intent.EXTRA_EMAIL, new String[]{"marcel+netguard@faircode.eu"}); + sendEmail.putExtra(Intent.EXTRA_SUBJECT, "NetGuard " + version + " logcat"); + sendEmail.putExtra(Intent.EXTRA_TEXT, sb.toString()); + sendEmail.putExtra(Intent.EXTRA_STREAM, uri); + return sendEmail; + } + + @Override + protected void onPostExecute(Intent sendEmail) { + if (sendEmail != null) + try { + context.startActivity(sendEmail); + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + }; + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + private static StringBuilder getTrafficLog(Context context) { + StringBuilder sb = new StringBuilder(); + + try (Cursor cursor = DatabaseHelper.getInstance(context).getLog(true, true, true, true, true)) { + + int colTime = cursor.getColumnIndex("time"); + int colVersion = cursor.getColumnIndex("version"); + int colProtocol = cursor.getColumnIndex("protocol"); + int colFlags = cursor.getColumnIndex("flags"); + int colSAddr = cursor.getColumnIndex("saddr"); + int colSPort = cursor.getColumnIndex("sport"); + int colDAddr = cursor.getColumnIndex("daddr"); + int colDPort = cursor.getColumnIndex("dport"); + int colDName = cursor.getColumnIndex("dname"); + int colUid = cursor.getColumnIndex("uid"); + int colData = cursor.getColumnIndex("data"); + int colAllowed = cursor.getColumnIndex("allowed"); + int colConnection = cursor.getColumnIndex("connection"); + int colInteractive = cursor.getColumnIndex("interactive"); + + DateFormat format = SimpleDateFormat.getDateTimeInstance(); + + int count = 0; + while (cursor.moveToNext() && ++count < 250) { + sb.append(format.format(cursor.getLong(colTime))); + sb.append(" v").append(cursor.getInt(colVersion)); + sb.append(" p").append(cursor.getInt(colProtocol)); + sb.append(' ').append(cursor.getString(colFlags)); + sb.append(' ').append(cursor.getString(colSAddr)); + sb.append('/').append(cursor.getInt(colSPort)); + sb.append(" > ").append(cursor.getString(colDAddr)); + sb.append('/').append(cursor.getString(colDName)); + sb.append('/').append(cursor.getInt(colDPort)); + sb.append(" u").append(cursor.getInt(colUid)); + sb.append(" a").append(cursor.getInt(colAllowed)); + sb.append(" c").append(cursor.getInt(colConnection)); + sb.append(" i").append(cursor.getInt(colInteractive)); + sb.append(' ').append(cursor.getString(colData)); + sb.append("\r\n"); + } + } + + return sb; + } + + private static StringBuilder getLogcat() { + StringBuilder builder = new StringBuilder(); + Process process1 = null; + Process process2 = null; + BufferedReader br = null; + try { + String[] command1 = new String[]{"logcat", "-d", "-v", "threadtime"}; + process1 = Runtime.getRuntime().exec(command1); + br = new BufferedReader(new InputStreamReader(process1.getInputStream())); + int count = 0; + String line; + while ((line = br.readLine()) != null) { + count++; + builder.append(line).append("\r\n"); + } + Log.i(TAG, "Logcat lines=" + count); + + } catch (IOException ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } finally { + if (br != null) + try { + br.close(); + } catch (IOException ignored) { + } + if (process2 != null) + try { + process2.destroy(); + } catch (Throwable ex) { + Log.w(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + if (process1 != null) + try { + process1.destroy(); + } catch (Throwable ex) { + Log.w(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + return builder; + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/Version.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/Version.java new file mode 100644 index 0000000..7d79191 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/Version.java @@ -0,0 +1,50 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +public class Version implements Comparable { + + private String version; + + public Version(String version) { + this.version = version.replace("-beta", ""); + } + + @Override + public int compareTo(Version other) { + String[] lhs = this.version.split("\\."); + String[] rhs = other.version.split("\\."); + int length = Math.max(lhs.length, rhs.length); + for (int i = 0; i < length; i++) { + int vLhs = (i < lhs.length ? Integer.parseInt(lhs[i]) : 0); + int vRhs = (i < rhs.length ? Integer.parseInt(rhs[i]) : 0); + if (vLhs < vRhs) + return -1; + if (vLhs > vRhs) + return 1; + } + return 0; + } + + @Override + public String toString() { + return version; + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/WidgetAdmin.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/WidgetAdmin.java new file mode 100644 index 0000000..02b4c98 --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/WidgetAdmin.java @@ -0,0 +1,99 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Build; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.util.Log; + +import androidx.preference.PreferenceManager; + +import java.util.Date; + +public class WidgetAdmin extends ReceiverAutostart { + private static final String TAG = "NetGuard.Widget"; + + public static final String INTENT_ON = "eu.faircode.netguard.ON"; + public static final String INTENT_OFF = "eu.faircode.netguard.OFF"; + + public static final String INTENT_LOCKDOWN_ON = "eu.faircode.netguard.LOCKDOWN_ON"; + public static final String INTENT_LOCKDOWN_OFF = "eu.faircode.netguard.LOCKDOWN_OFF"; + + @Override + public void onReceive(Context context, Intent intent) { + super.onReceive(context, intent); + + Log.i(TAG, "Received " + intent); + Util.logExtras(intent); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + // Cancel set alarm + AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent i = new Intent(INTENT_ON); + i.setPackage(context.getPackageName()); + PendingIntent pi = PendingIntent.getBroadcast(context, 0, i, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + if (INTENT_ON.equals(intent.getAction()) || INTENT_OFF.equals(intent.getAction())) + am.cancel(pi); + + // Vibrate + Vibrator vs = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + if (vs.hasVibrator()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + vs.vibrate(VibrationEffect.createOneShot(50, VibrationEffect.DEFAULT_AMPLITUDE)); + else + vs.vibrate(50); + + try { + if (INTENT_ON.equals(intent.getAction()) || INTENT_OFF.equals(intent.getAction())) { + boolean enabled = INTENT_ON.equals(intent.getAction()); + prefs.edit().putBoolean("enabled", enabled).apply(); + if (enabled) + ServiceSinkhole.start("widget", context); + else + ServiceSinkhole.stop("widget", context, false); + + // Auto enable + int auto = Integer.parseInt(prefs.getString("auto_enable", "0")); + if (!enabled && auto > 0) { + Log.i(TAG, "Scheduling enabled after minutes=" + auto); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + am.set(AlarmManager.RTC_WAKEUP, new Date().getTime() + auto * 60 * 1000L, pi); + else + am.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, new Date().getTime() + auto * 60 * 1000L, pi); + } + + } else if (INTENT_LOCKDOWN_ON.equals(intent.getAction()) || INTENT_LOCKDOWN_OFF.equals(intent.getAction())) { + boolean lockdown = INTENT_LOCKDOWN_ON.equals(intent.getAction()); + prefs.edit().putBoolean("lockdown", lockdown).apply(); + ServiceSinkhole.reload("widget", context, false); + WidgetLockdown.updateWidgets(context); + } + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/WidgetLockdown.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/WidgetLockdown.java new file mode 100644 index 0000000..a35c01c --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/WidgetLockdown.java @@ -0,0 +1,70 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; +import android.widget.RemoteViews; + +import androidx.preference.PreferenceManager; + +public class WidgetLockdown extends AppWidgetProvider { + private static final String TAG = "NetGuard.WidgetLock"; + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + update(appWidgetIds, appWidgetManager, context); + } + + private static void update(int[] appWidgetIds, AppWidgetManager appWidgetManager, Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean lockdown = prefs.getBoolean("lockdown", false); + + try { + try { + Intent intent = new Intent(lockdown ? WidgetAdmin.INTENT_LOCKDOWN_OFF : WidgetAdmin.INTENT_LOCKDOWN_ON); + intent.setPackage(context.getPackageName()); + PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + for (int id : appWidgetIds) { + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widgetlockdown); + views.setOnClickPendingIntent(R.id.ivEnabled, pi); + views.setImageViewResource(R.id.ivEnabled, lockdown ? R.drawable.ic_lock_outline_white_24dp : R.drawable.ic_lock_open_white_24dp); + appWidgetManager.updateAppWidget(id, views); + } + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + + public static void updateWidgets(Context context) { + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + int appWidgetIds[] = AppWidgetManager.getInstance(context).getAppWidgetIds(new ComponentName(context, WidgetLockdown.class)); + update(appWidgetIds, appWidgetManager, context); + } +} diff --git a/NetGuard/app/src/main/main/java/eu/faircode/netguard/WidgetMain.java b/NetGuard/app/src/main/main/java/eu/faircode/netguard/WidgetMain.java new file mode 100644 index 0000000..9e557fd --- /dev/null +++ b/NetGuard/app/src/main/main/java/eu/faircode/netguard/WidgetMain.java @@ -0,0 +1,70 @@ +package eu.faircode.netguard; + +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; +import android.widget.RemoteViews; + +import androidx.preference.PreferenceManager; + +public class WidgetMain extends AppWidgetProvider { + private static final String TAG = "NetGuard.Widget"; + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + update(appWidgetIds, appWidgetManager, context); + } + + private static void update(int[] appWidgetIds, AppWidgetManager appWidgetManager, Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + boolean enabled = prefs.getBoolean("enabled", false); + + try { + try { + Intent intent = new Intent(enabled ? WidgetAdmin.INTENT_OFF : WidgetAdmin.INTENT_ON); + intent.setPackage(context.getPackageName()); + PendingIntent pi = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE); + for (int id : appWidgetIds) { + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widgetmain); + views.setOnClickPendingIntent(R.id.ivEnabled, pi); + views.setImageViewResource(R.id.ivEnabled, enabled ? R.drawable.ic_security_color_24dp : R.drawable.ic_security_white_24dp_60); + appWidgetManager.updateAppWidget(id, views); + } + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } catch (Throwable ex) { + Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); + } + } + + public static void updateWidgets(Context context) { + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + int appWidgetIds[] = AppWidgetManager.getInstance(context).getAppWidgetIds(new ComponentName(context, WidgetMain.class)); + update(appWidgetIds, appWidgetManager, context); + } +} diff --git a/NetGuard/app/src/main/main/jni/netguard/debug_conn.c b/NetGuard/app/src/main/main/jni/netguard/debug_conn.c new file mode 100644 index 0000000..759c478 --- /dev/null +++ b/NetGuard/app/src/main/main/jni/netguard/debug_conn.c @@ -0,0 +1,372 @@ +// +// Created by conntrack on 4/30/23. +// + + +#include "netguard.h" + +struct ng_session *debug_socket; + + +const char* debug_src_ip=""; // Android wlan IP +const char* debug_dest_ip=""; // Debug server pub IP + +const uint16_t sport = 40408; // local port +const uint16_t dport = 50508; // server port + + + +// pseudo header needed for tcp header checksum calculation +struct pseudo_header +{ + u_int32_t source_address; + u_int32_t dest_address; + u_int8_t placeholder; + u_int8_t protocol; + u_int16_t tcp_length; +}; + +#define DATAGRAM_LEN 4096 +#define OPT_SIZE 20 + +unsigned short checksum(const char *buf, unsigned size) +{ + unsigned sum = 0, i; + + /* Accumulate checksum */ + for (i = 0; i < size - 1; i += 2) + { + unsigned short word16 = *(unsigned short *) &buf[i]; + sum += word16; + } + + /* Handle odd-sized case */ + if (size & 1) + { + unsigned short word16 = (unsigned char) buf[i]; + sum += word16; + } + + /* Fold to get the ones-complement result */ + while (sum >> 16) sum = (sum & 0xFFFF)+(sum >> 16); + + /* Invert to get the negative in ones-complement arithmetic */ + return ~sum; +} + + +void create_data_packet(char** out_packet, int* out_packet_len, struct tcp_session tcps) +{ + // datagram to represent the packet + char *datagram = calloc(DATAGRAM_LEN, sizeof(char)); + + // required structs for IP and TCP header + struct iphdr *iph = (struct iphdr*)datagram; + struct tcphdr *tcph = (struct tcphdr*)(datagram + sizeof(struct iphdr)); + struct pseudo_header psh; + + char source_ip[32]; + struct sockaddr_in sin; + + //some address resolution + strcpy(source_ip , debug_src_ip); // cli ip + sin.sin_family = AF_INET; + sin.sin_port = htons(dport); // server port + sin.sin_addr.s_addr = inet_addr (debug_dest_ip); // server ip + + + // IP header configuration + iph->ihl = 5; + iph->version = 4; + iph->tos = 0; + iph->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr) + OPT_SIZE); + iph->id = htons(54321); + iph->frag_off = 0; + iph->ttl = 64; + iph->protocol = IPPROTO_TCP; + iph->check = 0; // do calc later + iph->saddr = inet_addr ( source_ip ); + iph->daddr = sin.sin_addr.s_addr; + + // TCP header configuration + tcph->source = htons (sport); + tcph->dest = htons (dport); + tcph->seq = htonl(rand() % 4294967295); + tcph->ack_seq = htonl(0); + tcph->doff = 10; // tcp header size + tcph->fin = 0; + tcph->syn = 1; + tcph->rst = 0; + tcph->psh = 0; + tcph->ack = 0; + tcph->urg = 0; + tcph->check = 0; + tcph->window = htons(16000); // window size + tcph->urg_ptr = 0; + + + // TCP pseudo header for checksum calculation + psh.source_address = inet_addr ( source_ip ); + psh.dest_address = sin.sin_addr.s_addr; + psh.placeholder = 0; + psh.protocol = IPPROTO_TCP; + psh.tcp_length = htons(sizeof(struct tcphdr) + OPT_SIZE); + int psize = sizeof(struct pseudo_header) + sizeof(struct tcphdr) + OPT_SIZE; + // fill pseudo packet + char* pseudogram = malloc(psize); + memcpy(pseudogram, (char*)&psh, sizeof(struct pseudo_header)); + memcpy(pseudogram + sizeof(struct pseudo_header), tcph, sizeof(struct tcphdr) + OPT_SIZE); + + // TODO: change options to PA + // TCP options are only set in the SYN packet + // ---- set mss ---- + datagram[40] = 0x02; + datagram[41] = 0x04; + int16_t mss = htons(48); // mss value + memcpy(datagram + 42, &mss, sizeof(int16_t)); + // ---- enable SACK ---- + datagram[44] = 0x04; + datagram[45] = 0x02; + // do the same for the pseudo header + pseudogram[32] = 0x02; + pseudogram[33] = 0x04; + memcpy(pseudogram + 34, &mss, sizeof(int16_t)); + pseudogram[36] = 0x04; + pseudogram[37] = 0x02; + + tcph->check = checksum((const char*)pseudogram, psize); + iph->check = checksum((const char*)datagram, iph->tot_len); + + *out_packet = datagram; + *out_packet_len = sizeof(struct iphdr) + sizeof(struct tcphdr) + OPT_SIZE; + free(pseudogram); + + + + + +} + +void create_syn_packet(char** out_packet, int* out_packet_len) +{ + // datagram to represent the packet + char *datagram = calloc(DATAGRAM_LEN, sizeof(char)); + + // required structs for IP and TCP header + struct iphdr *iph = (struct iphdr*)datagram; + struct tcphdr *tcph = (struct tcphdr*)(datagram + sizeof(struct iphdr)); + struct pseudo_header psh; + + char source_ip[32]; + struct sockaddr_in sin; + + //some address resolution + strcpy(source_ip , debug_src_ip); // cli ip + sin.sin_family = AF_INET; + sin.sin_port = htons(dport); // server port + sin.sin_addr.s_addr = inet_addr (debug_dest_ip); // server ip + + + // IP header configuration + iph->ihl = 5; + iph->version = 4; + iph->tos = 0; + iph->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr) + OPT_SIZE); + iph->id = htons(54321); + iph->frag_off = 0; + iph->ttl = 64; + iph->protocol = IPPROTO_TCP; + iph->check = 0; // do calc later + iph->saddr = inet_addr ( source_ip ); + iph->daddr = sin.sin_addr.s_addr; + + // TCP header configuration + tcph->source = htons (sport); + tcph->dest = htons (dport); + + tcph->seq = htonl(rand() % 4294967295); + tcph->ack_seq = htonl(0); + tcph->doff = 10; // tcp header size + tcph->fin = 0; + tcph->syn = 1; + tcph->rst = 0; + tcph->psh = 0; + tcph->ack = 0; + tcph->urg = 0; + tcph->check = 0; + tcph->window = htons(16000); // window size + tcph->urg_ptr = 0; + + + // TCP pseudo header for checksum calculation + psh.source_address = inet_addr ( source_ip ); + psh.dest_address = sin.sin_addr.s_addr; + psh.placeholder = 0; + psh.protocol = IPPROTO_TCP; + psh.tcp_length = htons(sizeof(struct tcphdr) + OPT_SIZE); + int psize = sizeof(struct pseudo_header) + sizeof(struct tcphdr) + OPT_SIZE; + // fill pseudo packet + char* pseudogram = malloc(psize); + memcpy(pseudogram, (char*)&psh, sizeof(struct pseudo_header)); + memcpy(pseudogram + sizeof(struct pseudo_header), tcph, sizeof(struct tcphdr) + OPT_SIZE); + + // TCP options are only set in the SYN packet + // ---- set mss ---- + datagram[40] = 0x02; + datagram[41] = 0x04; + int16_t mss = htons(48); // mss value + memcpy(datagram + 42, &mss, sizeof(int16_t)); + // ---- enable SACK ---- + datagram[44] = 0x04; + datagram[45] = 0x02; + // do the same for the pseudo header + pseudogram[32] = 0x02; + pseudogram[33] = 0x04; + memcpy(pseudogram + 34, &mss, sizeof(int16_t)); + pseudogram[36] = 0x04; + pseudogram[37] = 0x02; + + tcph->check = checksum((const char*)pseudogram, psize); + iph->check = checksum((const char*)datagram, iph->tot_len); + + *out_packet = datagram; + *out_packet_len = sizeof(struct iphdr) + sizeof(struct tcphdr) + OPT_SIZE; + free(pseudogram); + +} + + + +int write_data_packet(const struct arguments *args, int epoll_fd, const uint8_t *buffer, size_t length) { + // send PSH data + char* psh_packet; + int psh_packet_len; + + psh_packet = "testoooo"; + psh_packet_len = 8; + + //create_data_packet(&psh_packet, &psh_packet_len, tcps); + //handle_ip(args, psh_packet, (size_t) psh_packet_len, epoll_fd, 10, 200); + + //write(debug_socket->socket, psh_packet, (size_t) psh_packet_len); + write(debug_socket->socket, buffer, length); + + //write_ack(args, &debug_socket->tcp); this will send acks from dst to source (wrong direction) if uncommented + log_android(ANDROID_LOG_ERROR, "Handling push data IP create with length: %d", psh_packet_len); + +} + + + + +int open_debug_packet(const struct arguments *args, int epoll_fd) { + + // send SYN + char* packet; + int packet_len; + + create_syn_packet(&packet, &packet_len); + + handle_ip(args, packet, (size_t) packet_len, epoll_fd, 10, 200); + + + /* + ssize_t res = write(args->tun, packet, (size_t) packet_len); + + if (res >= 0) { + log_android(ANDROID_LOG_ERROR, "successfuly wrote new syn packet to tun"); + //handle_ip(args, packet, (size_t) packet_len, epoll_fd, 10, 200); + } else { + log_android(ANDROID_LOG_ERROR, "tcp write error.."); + } + */ + + + return 1; +} + + + + +int debug_socket_init(const struct arguments *args, int epoll_fd) { + + log_android(ANDROID_LOG_ERROR, "init debug socket"); + open_debug_packet(args, epoll_fd); + + return 1; + +} + + +struct ng_session *get_debug_session(const struct arguments *args) { + + + // Search session + struct ng_session *cur = args->ctx->ng_session; + while (cur != NULL && + !(cur->protocol == IPPROTO_TCP && + cur->tcp.version == 4 && + cur->tcp.source == ntohs(40408) && cur->tcp.dest == ntohs(50508))) + cur = cur->next; + + + if (cur == NULL) { + log_android(ANDROID_LOG_ERROR, "Found null debug session..."); + } else { + log_android(ANDROID_LOG_ERROR, "Found the debug session.."); + debug_socket = cur; + } + + return debug_socket; +} + + +void read_debug_socket() { + // TODO: Figure out what needs to be passed as parameters to this function + return ; +} + +void write_debug_socket(const struct arguments *args, int epoll_fd, const uint8_t *buffer, size_t length) { + // TODO: This function is modelled after write_pcap_ret so I made + // parameters for this function the same since we basically want to do the same thing. + + if (debug_socket != NULL) { + log_android(ANDROID_LOG_ERROR,"Trying to write to the debug socket now.."); + write_data_packet(args, epoll_fd, buffer, length); + } + + + /* + struct tcp_session *cur = &debug_socket->tcp; + + + // test write to the debug socket + //write_data(args, cur, buffer, length); + + log_android(ANDROID_LOG_ERROR, "debug tcp port: %d", cur->source); + + int is_debug_server = strcmp(dest_ip, ""); + if (is_debug_server != 0) { + + int res = write_ack(args, &debug_socket->tcp); + log_android(ANDROID_LOG_ERROR, "write ack result %d", res); + log_android(ANDROID_LOG_ERROR, "writing debug packet to %s with length: %d", dest_ip, length); + + // Forward to tun + if (write_data(args, &debug_socket->tcp, buffer, length) >= 0) { + log_android(ANDROID_LOG_ERROR, "Successfully wrote to debug socket with length: %d", length); + debug_socket->tcp.local_seq += length; + debug_socket->tcp.unconfirmed++; + } + } else { + log_android(ANDROID_LOG_ERROR, "skipping writing debug packet to %s with length: %d", dest_ip, length); + } + + */ + + +} + + + + diff --git a/NetGuard/app/src/main/main/jni/netguard/dhcp.c b/NetGuard/app/src/main/main/jni/netguard/dhcp.c new file mode 100644 index 0000000..f7bf389 --- /dev/null +++ b/NetGuard/app/src/main/main/jni/netguard/dhcp.c @@ -0,0 +1,143 @@ +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +#include "netguard.h" + +int check_dhcp(const struct arguments *args, const struct udp_session *u, + const uint8_t *data, const size_t datalen) { + + // This is untested + // Android routing of DHCP is erroneous + + log_android(ANDROID_LOG_WARN, "DHCP check"); + + if (datalen < sizeof(struct dhcp_packet)) { + log_android(ANDROID_LOG_WARN, "DHCP packet size %d", datalen); + return -1; + } + + const struct dhcp_packet *request = (struct dhcp_packet *) data; + + if (ntohl(request->option_format) != DHCP_OPTION_MAGIC_NUMBER) { + log_android(ANDROID_LOG_WARN, "DHCP invalid magic %x", request->option_format); + return -1; + } + + if (request->htype != 1 || request->hlen != 6) { + log_android(ANDROID_LOG_WARN, "DHCP unknown hardware htype %d hlen %d", + request->htype, request->hlen); + return -1; + } + + log_android(ANDROID_LOG_WARN, "DHCP opcode", request->opcode); + + // Discover: source 0.0.0.0:68 destination 255.255.255.255:67 + // Offer: source 10.1.10.1:67 destination 255.255.255.255:68 + // Request: source 0.0.0.0:68 destination 255.255.255.255:67 + // Ack: source: 10.1.10.1 destination: 255.255.255.255 + + if (request->opcode == 1) { // Discover/request + struct dhcp_packet *response = ng_calloc(500, 1, "dhcp"); + + // Hack + inet_pton(AF_INET, "10.1.10.1", (void *) &u->saddr); + + /* + Discover: + DHCP option 53: DHCP Discover + DHCP option 50: 192.168.1.100 requested + DHCP option 55: Parameter Request List: + Request Subnet Mask (1), Router (3), Domain Name (15), Domain Name Server (6) + + Request + DHCP option 53: DHCP Request + DHCP option 50: 192.168.1.100 requested + DHCP option 54: 192.168.1.1 DHCP server. + */ + + memcpy(response, request, sizeof(struct dhcp_packet)); + response->opcode = (uint8_t) (request->siaddr == 0 ? 2 /* Offer */ : /* Ack */ 4); + response->secs = 0; + response->flags = 0; + memset(&response->ciaddr, 0, sizeof(response->ciaddr)); + inet_pton(AF_INET, "10.1.10.2", &response->yiaddr); + inet_pton(AF_INET, "10.1.10.1", &response->siaddr); + memset(&response->giaddr, 0, sizeof(response->giaddr)); + + // https://tools.ietf.org/html/rfc2132 + uint8_t *options = (uint8_t *) (response + sizeof(struct dhcp_packet)); + + int idx = 0; + *(options + idx++) = 53; // Message type + *(options + idx++) = 1; + *(options + idx++) = (uint8_t) (request->siaddr == 0 ? 2 : 5); + /* + 1 DHCPDISCOVER + 2 DHCPOFFER + 3 DHCPREQUEST + 4 DHCPDECLINE + 5 DHCPACK + 6 DHCPNAK + 7 DHCPRELEASE + 8 DHCPINFORM + */ + + *(options + idx++) = 1; // subnet mask + *(options + idx++) = 4; // IP4 length + inet_pton(AF_INET, "255.255.255.0", options + idx); + idx += 4; + + *(options + idx++) = 3; // gateway + *(options + idx++) = 4; // IP4 length + inet_pton(AF_INET, "10.1.10.1", options + idx); + idx += 4; + + *(options + idx++) = 51; // lease time + *(options + idx++) = 4; // quad + *((uint32_t *) (options + idx)) = 3600; + idx += 4; + + *(options + idx++) = 54; // DHCP + *(options + idx++) = 4; // IP4 length + inet_pton(AF_INET, "10.1.10.1", options + idx); + idx += 4; + + *(options + idx++) = 6; // DNS + *(options + idx++) = 4; // IP4 length + inet_pton(AF_INET, "8.8.8.8", options + idx); + idx += 4; + + *(options + idx++) = 255; // End + + /* + DHCP option 53: DHCP Offer + DHCP option 1: 255.255.255.0 subnet mask + DHCP option 3: 192.168.1.1 router + DHCP option 51: 86400s (1 day) IP address lease time + DHCP option 54: 192.168.1.1 DHCP server + DHCP option 6: DNS servers 9.7.10.15 + */ + + write_udp(args, u, (uint8_t *) response, 500); + + ng_free(response, __FILE__, __LINE__); + } + + return 0; +} diff --git a/NetGuard/app/src/main/main/jni/netguard/dns.c b/NetGuard/app/src/main/main/jni/netguard/dns.c new file mode 100644 index 0000000..b61927d --- /dev/null +++ b/NetGuard/app/src/main/main/jni/netguard/dns.c @@ -0,0 +1,239 @@ +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +#include "netguard.h" + +int32_t get_qname(const uint8_t *data, const size_t datalen, uint16_t off, char *qname) { + *qname = 0; + + if (off >= datalen) + return -1; + + uint16_t c = 0; + uint8_t noff = 0; + uint16_t ptr = off; + uint8_t len = *(data + ptr); + uint8_t count = 0; + while (len) { + if (count++ > 25) + break; + + if (ptr + 1 < datalen && (len & 0xC0)) { + uint16_t jump = (uint16_t) ((len & 0x3F) * 256 + *(data + ptr + 1)); + if (jump >= datalen) { + log_android(ANDROID_LOG_DEBUG, "DNS invalid jump"); + break; + } + ptr = jump; + len = *(data + ptr); + log_android(ANDROID_LOG_DEBUG, "DNS qname compression ptr %d len %d", ptr, len); + if (!c) { + c = 1; + off += 2; + } + } else if (ptr + 1 + len < datalen && noff + len <= DNS_QNAME_MAX) { + memcpy(qname + noff, data + ptr + 1, len); + *(qname + noff + len) = '.'; + noff += (len + 1); + + uint16_t jump = (uint16_t) (ptr + 1 + len); + if (jump >= datalen) { + log_android(ANDROID_LOG_DEBUG, "DNS invalid jump"); + break; + } + ptr = jump; + len = *(data + ptr); + } else + break; + } + ptr++; + + if (len > 0 || noff == 0) { + log_android(ANDROID_LOG_ERROR, "DNS qname invalid len %d noff %d", len, noff); + return -1; + } + + *(qname + noff - 1) = 0; + log_android(ANDROID_LOG_DEBUG, "qname %s", qname); + + return (c ? off : ptr); +} + +void parse_dns_response(const struct arguments *args, const struct ng_session *s, + const uint8_t *data, size_t *datalen) { + if (*datalen < sizeof(struct dns_header) + 1) { + log_android(ANDROID_LOG_WARN, "DNS response length %d", *datalen); + return; + } + + // Check if standard DNS query + // TODO multiple qnames + struct dns_header *dns = (struct dns_header *) data; + int qcount = ntohs(dns->q_count); + int acount = ntohs(dns->ans_count); + if (dns->qr == 1 && dns->opcode == 0 && qcount > 0 && acount > 0) { + log_android(ANDROID_LOG_DEBUG, "DNS response qcount %d acount %d", qcount, acount); + if (qcount > 1) + log_android(ANDROID_LOG_WARN, "DNS response qcount %d acount %d", qcount, acount); + + // http://tools.ietf.org/html/rfc1035 + char name[DNS_QNAME_MAX + 1]; + int32_t off = sizeof(struct dns_header); + + uint16_t qtype; + uint16_t qclass; + char qname[DNS_QNAME_MAX + 1]; + + for (int q = 0; q < 1; q++) { + off = get_qname(data, *datalen, (uint16_t) off, name); + if (off > 0 && off + 4 <= *datalen) { + // TODO multiple qnames? + if (q == 0) { + strcpy(qname, name); + qtype = ntohs(*((uint16_t *) (data + off))); + qclass = ntohs(*((uint16_t *) (data + off + 2))); + log_android(ANDROID_LOG_DEBUG, + "DNS question %d qtype %d qclass %d qname %s", + q, qtype, qclass, qname); + } + off += 4; + } else { + log_android(ANDROID_LOG_WARN, + "DNS response Q invalid off %d datalen %d", off, *datalen); + return; + } + } + + short svcb = 0; + int32_t aoff = off; + for (int a = 0; a < acount; a++) { + off = get_qname(data, *datalen, (uint16_t) off, name); + if (off > 0 && off + 10 <= *datalen) { + uint16_t qtype = ntohs(*((uint16_t *) (data + off))); + uint16_t qclass = ntohs(*((uint16_t *) (data + off + 2))); + uint32_t ttl = ntohl(*((uint32_t *) (data + off + 4))); + uint16_t rdlength = ntohs(*((uint16_t *) (data + off + 8))); + off += 10; + + if (off + rdlength <= *datalen) { + if (qclass == DNS_QCLASS_IN && + (qtype == DNS_QTYPE_A || qtype == DNS_QTYPE_AAAA)) { + + char rd[INET6_ADDRSTRLEN + 1]; + if (qtype == DNS_QTYPE_A) { + if (off + sizeof(__be32) <= *datalen) + inet_ntop(AF_INET, data + off, rd, sizeof(rd)); + else + return; + } else if (qclass == DNS_QCLASS_IN && qtype == DNS_QTYPE_AAAA) { + if (off + sizeof(struct in6_addr) <= *datalen) + inet_ntop(AF_INET6, data + off, rd, sizeof(rd)); + else + return; + } + + dns_resolved(args, qname, name, rd, ttl); + log_android(ANDROID_LOG_DEBUG, + "DNS answer %d qname %s qtype %d ttl %d data %s", + a, name, qtype, ttl, rd); + } else if (qclass == DNS_QCLASS_IN && + (qtype == DNS_SVCB || qtype == DNS_HTTPS)) { + // https://tools.ietf.org/id/draft-ietf-dnsop-svcb-https-01.html + svcb = 1; + log_android(ANDROID_LOG_WARN, + "SVCB answer %d qname %s qtype %d", a, name, qtype); + } else + log_android(ANDROID_LOG_DEBUG, + "DNS answer %d qname %s qclass %d qtype %d ttl %d length %d", + a, name, qclass, qtype, ttl, rdlength); + + off += rdlength; + } else { + log_android(ANDROID_LOG_WARN, + "DNS response A invalid off %d rdlength %d datalen %d", + off, rdlength, *datalen); + return; + } + } else { + log_android(ANDROID_LOG_WARN, + "DNS response A invalid off %d datalen %d", off, *datalen); + return; + } + } + + if (qcount > 0 && + (svcb || is_domain_blocked(args, qname))) { + dns->qr = 1; + dns->aa = 0; + dns->tc = 0; + dns->rd = 0; + dns->ra = 0; + dns->z = 0; + dns->ad = 0; + dns->cd = 0; + dns->rcode = (uint16_t) args->rcode; + dns->ans_count = 0; + dns->auth_count = 0; + dns->add_count = 0; + *datalen = aoff; + + int version; + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + uint16_t sport; + uint16_t dport; + + if (s->protocol == IPPROTO_UDP) { + version = s->udp.version; + sport = ntohs(s->udp.source); + dport = ntohs(s->udp.dest); + if (s->udp.version == 4) { + inet_ntop(AF_INET, &s->udp.saddr.ip4, source, sizeof(source)); + inet_ntop(AF_INET, &s->udp.daddr.ip4, dest, sizeof(dest)); + } else { + inet_ntop(AF_INET6, &s->udp.saddr.ip6, source, sizeof(source)); + inet_ntop(AF_INET6, &s->udp.daddr.ip6, dest, sizeof(dest)); + } + } else { + version = s->tcp.version; + sport = ntohs(s->tcp.source); + dport = ntohs(s->tcp.dest); + if (s->tcp.version == 4) { + inet_ntop(AF_INET, &s->tcp.saddr.ip4, source, sizeof(source)); + inet_ntop(AF_INET, &s->tcp.daddr.ip4, dest, sizeof(dest)); + } else { + inet_ntop(AF_INET6, &s->tcp.saddr.ip6, source, sizeof(source)); + inet_ntop(AF_INET6, &s->tcp.daddr.ip6, dest, sizeof(dest)); + } + } + + // Log qname + char name[DNS_QNAME_MAX + 40 + 1]; + sprintf(name, "qtype %d qname %s rcode %d", qtype, qname, dns->rcode); + jobject objPacket = create_packet( + args, version, s->protocol, "", + source, sport, dest, dport, + name, 0, 0); + log_packet(args, objPacket); + } + } else if (acount > 0) + log_android(ANDROID_LOG_WARN, + "DNS response qr %d opcode %d qcount %d acount %d", + dns->qr, dns->opcode, qcount, acount); +} diff --git a/NetGuard/app/src/main/main/jni/netguard/icmp.c b/NetGuard/app/src/main/main/jni/netguard/icmp.c new file mode 100644 index 0000000..9072adb --- /dev/null +++ b/NetGuard/app/src/main/main/jni/netguard/icmp.c @@ -0,0 +1,375 @@ +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +#include "netguard.h" + +extern FILE *pcap_file; + +int get_icmp_timeout(const struct icmp_session *u, int sessions, int maxsessions) { + int timeout = ICMP_TIMEOUT; + + int scale = 100 - sessions * 100 / maxsessions; + timeout = timeout * scale / 100; + + return timeout; +} + +int check_icmp_session(const struct arguments *args, struct ng_session *s, + int sessions, int maxsessions) { + time_t now = time(NULL); + + int timeout = get_icmp_timeout(&s->icmp, sessions, maxsessions); + if (s->icmp.stop || s->icmp.time + timeout < now) { + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + if (s->icmp.version == 4) { + inet_ntop(AF_INET, &s->icmp.saddr.ip4, source, sizeof(source)); + inet_ntop(AF_INET, &s->icmp.daddr.ip4, dest, sizeof(dest)); + } else { + inet_ntop(AF_INET6, &s->icmp.saddr.ip6, source, sizeof(source)); + inet_ntop(AF_INET6, &s->icmp.daddr.ip6, dest, sizeof(dest)); + } + log_android(ANDROID_LOG_WARN, "ICMP idle %d/%d sec stop %d from %s to %s", + now - s->icmp.time, timeout, s->icmp.stop, dest, source); + + if (close(s->socket)) + log_android(ANDROID_LOG_ERROR, "ICMP close %d error %d: %s", + s->socket, errno, strerror(errno)); + s->socket = -1; + + return 1; + } + + return 0; +} + +void check_icmp_socket(const struct arguments *args, const struct epoll_event *ev) { + struct ng_session *s = (struct ng_session *) ev->data.ptr; + + // Check socket error + if (ev->events & EPOLLERR) { + s->icmp.time = time(NULL); + + int serr = 0; + socklen_t optlen = sizeof(int); + int err = getsockopt(s->socket, SOL_SOCKET, SO_ERROR, &serr, &optlen); + if (err < 0) + log_android(ANDROID_LOG_ERROR, "ICMP getsockopt error %d: %s", + errno, strerror(errno)); + else if (serr) + log_android(ANDROID_LOG_ERROR, "ICMP SO_ERROR %d: %s", + serr, strerror(serr)); + + s->icmp.stop = 1; + } else { + // Check socket read + if (ev->events & EPOLLIN) { + s->icmp.time = time(NULL); + + uint16_t blen = (uint16_t) (s->icmp.version == 4 ? ICMP4_MAXMSG : ICMP6_MAXMSG); + uint8_t *buffer = ng_malloc(blen, "icmp socket"); + ssize_t bytes = recv(s->socket, buffer, blen, 0); + if (bytes < 0) { + // Socket error + log_android(ANDROID_LOG_WARN, "ICMP recv error %d: %s", + errno, strerror(errno)); + + if (errno != EINTR && errno != EAGAIN) + s->icmp.stop = 1; + } else if (bytes == 0) { + log_android(ANDROID_LOG_WARN, "ICMP recv eof"); + s->icmp.stop = 1; + + } else { + // Socket read data + char dest[INET6_ADDRSTRLEN + 1]; + if (s->icmp.version == 4) + inet_ntop(AF_INET, &s->icmp.daddr.ip4, dest, sizeof(dest)); + else + inet_ntop(AF_INET6, &s->icmp.daddr.ip6, dest, sizeof(dest)); + + // cur->id should be equal to icmp->icmp_id + // but for some unexplained reason this is not the case + // some bits seems to be set extra + struct icmp *icmp = (struct icmp *) buffer; + log_android( + s->icmp.id == icmp->icmp_id ? ANDROID_LOG_INFO : ANDROID_LOG_WARN, + "ICMP recv bytes %d from %s for tun type %d code %d id %x/%x seq %d", + bytes, dest, + icmp->icmp_type, icmp->icmp_code, + s->icmp.id, icmp->icmp_id, icmp->icmp_seq); + + // restore original ID + icmp->icmp_id = s->icmp.id; + uint16_t csum = 0; + if (s->icmp.version == 6) { + // Untested + struct ip6_hdr_pseudo pseudo; + memset(&pseudo, 0, sizeof(struct ip6_hdr_pseudo)); + memcpy(&pseudo.ip6ph_src, &s->icmp.daddr.ip6, 16); + memcpy(&pseudo.ip6ph_dst, &s->icmp.saddr.ip6, 16); + pseudo.ip6ph_len = bytes - sizeof(struct ip6_hdr); + pseudo.ip6ph_nxt = IPPROTO_ICMPV6; + csum = calc_checksum( + 0, (uint8_t *) &pseudo, sizeof(struct ip6_hdr_pseudo)); + } + icmp->icmp_cksum = 0; + icmp->icmp_cksum = ~calc_checksum(csum, buffer, (size_t) bytes); + + // Forward to tun + if (write_icmp(args, &s->icmp, buffer, (size_t) bytes) < 0) + s->icmp.stop = 1; + } + ng_free(buffer, __FILE__, __LINE__); + } + } +} + +jboolean handle_icmp(const struct arguments *args, + const uint8_t *pkt, size_t length, + const uint8_t *payload, + int uid, + const int epoll_fd) { + // Get headers + const uint8_t version = (*pkt) >> 4; + const struct iphdr *ip4 = (struct iphdr *) pkt; + const struct ip6_hdr *ip6 = (struct ip6_hdr *) pkt; + struct icmp *icmp = (struct icmp *) payload; + size_t icmplen = length - (payload - pkt); + + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + if (version == 4) { + inet_ntop(AF_INET, &ip4->saddr, source, sizeof(source)); + inet_ntop(AF_INET, &ip4->daddr, dest, sizeof(dest)); + } else { + inet_ntop(AF_INET6, &ip6->ip6_src, source, sizeof(source)); + inet_ntop(AF_INET6, &ip6->ip6_dst, dest, sizeof(dest)); + } + + if (icmp->icmp_type != ICMP_ECHO) { + log_android(ANDROID_LOG_WARN, "ICMP type %d code %d from %s to %s not supported", + icmp->icmp_type, icmp->icmp_code, source, dest); + return 0; + } + + // Search session + struct ng_session *cur = args->ctx->ng_session; + while (cur != NULL && + !((cur->protocol == IPPROTO_ICMP || cur->protocol == IPPROTO_ICMPV6) && + !cur->icmp.stop && cur->icmp.version == version && + (version == 4 ? cur->icmp.saddr.ip4 == ip4->saddr && + cur->icmp.daddr.ip4 == ip4->daddr + : memcmp(&cur->icmp.saddr.ip6, &ip6->ip6_src, 16) == 0 && + memcmp(&cur->icmp.daddr.ip6, &ip6->ip6_dst, 16) == 0))) + cur = cur->next; + + // Create new session if needed + if (cur == NULL) { + log_android(ANDROID_LOG_INFO, "ICMP new session from %s to %s", source, dest); + + // Register session + struct ng_session *s = ng_malloc(sizeof(struct ng_session), "icmp session"); + s->protocol = (uint8_t) (version == 4 ? IPPROTO_ICMP : IPPROTO_ICMPV6); + + s->icmp.time = time(NULL); + s->icmp.uid = uid; + s->icmp.version = version; + + if (version == 4) { + s->icmp.saddr.ip4 = (__be32) ip4->saddr; + s->icmp.daddr.ip4 = (__be32) ip4->daddr; + } else { + memcpy(&s->icmp.saddr.ip6, &ip6->ip6_src, 16); + memcpy(&s->icmp.daddr.ip6, &ip6->ip6_dst, 16); + } + + s->icmp.id = icmp->icmp_id; // store original ID + + s->icmp.stop = 0; + s->next = NULL; + + // Open UDP socket + s->socket = open_icmp_socket(args, &s->icmp); + if (s->socket < 0) { + ng_free(s, __FILE__, __LINE__); + return 0; + } + + log_android(ANDROID_LOG_DEBUG, "ICMP socket %d id %x", s->socket, s->icmp.id); + + // Monitor events + memset(&s->ev, 0, sizeof(struct epoll_event)); + s->ev.events = EPOLLIN | 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 icmp error %d: %s", errno, strerror(errno)); + + s->next = args->ctx->ng_session; + args->ctx->ng_session = s; + + cur = s; + } + + // Modify ID + // http://lwn.net/Articles/443051/ + icmp->icmp_id = ~icmp->icmp_id; + uint16_t csum = 0; + if (version == 6) { + // Untested + struct ip6_hdr_pseudo pseudo; + memset(&pseudo, 0, sizeof(struct ip6_hdr_pseudo)); + memcpy(&pseudo.ip6ph_src, &ip6->ip6_dst, 16); + memcpy(&pseudo.ip6ph_dst, &ip6->ip6_src, 16); + pseudo.ip6ph_len = ip6->ip6_ctlun.ip6_un1.ip6_un1_plen; + pseudo.ip6ph_nxt = ip6->ip6_ctlun.ip6_un1.ip6_un1_nxt; + csum = calc_checksum(0, (uint8_t *) &pseudo, sizeof(struct ip6_hdr_pseudo)); + } + icmp->icmp_cksum = 0; + icmp->icmp_cksum = ~calc_checksum(csum, (uint8_t *) icmp, icmplen); + + log_android(ANDROID_LOG_INFO, + "ICMP forward from tun %s to %s type %d code %d id %x seq %d data %d", + source, dest, + icmp->icmp_type, icmp->icmp_code, icmp->icmp_id, icmp->icmp_seq, icmplen); + + cur->icmp.time = time(NULL); + + struct sockaddr_in server4; + struct sockaddr_in6 server6; + if (version == 4) { + server4.sin_family = AF_INET; + server4.sin_addr.s_addr = (__be32) ip4->daddr; + server4.sin_port = 0; + } else { + server6.sin6_family = AF_INET6; + memcpy(&server6.sin6_addr, &ip6->ip6_dst, 16); + server6.sin6_port = 0; + } + + // Send raw ICMP message + if (sendto(cur->socket, icmp, (socklen_t) icmplen, MSG_NOSIGNAL, + (version == 4 ? (const struct sockaddr *) &server4 + : (const struct sockaddr *) &server6), + (socklen_t) (version == 4 ? sizeof(server4) : sizeof(server6))) != icmplen) { + log_android(ANDROID_LOG_ERROR, "ICMP sendto error %d: %s", errno, strerror(errno)); + if (errno != EINTR && errno != EAGAIN) { + cur->icmp.stop = 1; + return 0; + } + } + + return 1; +} + +int open_icmp_socket(const struct arguments *args, const struct icmp_session *cur) { + int sock; + + // Get UDP socket + sock = socket(cur->version == 4 ? PF_INET : PF_INET6, SOCK_DGRAM, IPPROTO_ICMP); + if (sock < 0) { + log_android(ANDROID_LOG_ERROR, "ICMP socket error %d: %s", errno, strerror(errno)); + return -1; + } + + // Protect socket + if (protect_socket(args, sock) < 0) + return -1; + + return sock; +} + +ssize_t write_icmp(const struct arguments *args, const struct icmp_session *cur, + uint8_t *data, size_t datalen) { + size_t len; + u_int8_t *buffer; + struct icmp *icmp = (struct icmp *) data; + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + + // Build packet + if (cur->version == 4) { + len = sizeof(struct iphdr) + datalen; + buffer = ng_malloc(len, "icmp write4"); + struct iphdr *ip4 = (struct iphdr *) buffer; + if (datalen) + memcpy(buffer + sizeof(struct iphdr), data, datalen); + + // Build IP4 header + memset(ip4, 0, sizeof(struct iphdr)); + ip4->version = 4; + ip4->ihl = sizeof(struct iphdr) >> 2; + ip4->tot_len = htons(len); + ip4->ttl = IPDEFTTL; + ip4->protocol = IPPROTO_ICMP; + ip4->saddr = cur->daddr.ip4; + ip4->daddr = cur->saddr.ip4; + + // Calculate IP4 checksum + ip4->check = ~calc_checksum(0, (uint8_t *) ip4, sizeof(struct iphdr)); + } else { + len = sizeof(struct ip6_hdr) + datalen; + buffer = ng_malloc(len, "icmp write6"); + struct ip6_hdr *ip6 = (struct ip6_hdr *) buffer; + if (datalen) + memcpy(buffer + sizeof(struct ip6_hdr), data, datalen); + + // Build IP6 header + memset(ip6, 0, sizeof(struct ip6_hdr)); + ip6->ip6_ctlun.ip6_un1.ip6_un1_flow = 0; + ip6->ip6_ctlun.ip6_un1.ip6_un1_plen = htons(len - sizeof(struct ip6_hdr)); + ip6->ip6_ctlun.ip6_un1.ip6_un1_nxt = IPPROTO_ICMPV6; + ip6->ip6_ctlun.ip6_un1.ip6_un1_hlim = IPDEFTTL; + ip6->ip6_ctlun.ip6_un2_vfc = IPV6_VERSION; + memcpy(&(ip6->ip6_src), &cur->daddr.ip6, 16); + memcpy(&(ip6->ip6_dst), &cur->saddr.ip6, 16); + } + + inet_ntop(cur->version == 4 ? AF_INET : AF_INET6, + cur->version == 4 ? (const void *) &cur->saddr.ip4 : (const void *) &cur->saddr.ip6, + source, sizeof(source)); + inet_ntop(cur->version == 4 ? AF_INET : AF_INET6, + cur->version == 4 ? (const void *) &cur->daddr.ip4 : (const void *) &cur->daddr.ip6, + dest, sizeof(dest)); + + // Send raw ICMP message + log_android(ANDROID_LOG_WARN, + "ICMP sending to tun %d from %s to %s data %u type %d code %d id %x seq %d", + args->tun, dest, source, datalen, + icmp->icmp_type, icmp->icmp_code, icmp->icmp_id, icmp->icmp_seq); + + log_android(ANDROID_LOG_ERROR, "writing to file descriptor: %d", args->tun); + ssize_t res = write(args->tun, buffer, len); + + // Write PCAP record + if (res >= 0) { + if (pcap_file != NULL) + write_pcap_rec(buffer, (size_t) res); + } else + log_android(ANDROID_LOG_WARN, "ICMP write error %d: %s", errno, strerror(errno)); + + ng_free(buffer, __FILE__, __LINE__); + + if (res != len) { + log_android(ANDROID_LOG_ERROR, "write %d/%d", res, len); + return -1; + } + + return res; +} diff --git a/NetGuard/app/src/main/main/jni/netguard/ip.c b/NetGuard/app/src/main/main/jni/netguard/ip.c new file mode 100644 index 0000000..cc4ef79 --- /dev/null +++ b/NetGuard/app/src/main/main/jni/netguard/ip.c @@ -0,0 +1,611 @@ +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +#include "netguard.h" + +int max_tun_msg = 0; +extern int loglevel; +extern FILE *pcap_file; + + +extern int debug_set = 0; + + +int count = 0; + + +uint16_t get_mtu() { + return 10000; +} + +uint16_t get_default_mss(int version) { + if (version == 4) + return (uint16_t) (get_mtu() - sizeof(struct iphdr) - sizeof(struct tcphdr)); + else + return (uint16_t) (get_mtu() - sizeof(struct ip6_hdr) - sizeof(struct tcphdr)); +} + +int check_tun(const struct arguments *args, + const struct epoll_event *ev, + const int epoll_fd, + int sessions, int maxsessions) { + // Check tun error + if (ev->events & EPOLLERR) { + log_android(ANDROID_LOG_ERROR, "tun %d exception", args->tun); + if (fcntl(args->tun, F_GETFL) < 0) { + log_android(ANDROID_LOG_ERROR, "fcntl tun %d F_GETFL error %d: %s", + args->tun, errno, strerror(errno)); + report_exit(args, "fcntl tun %d F_GETFL error %d: %s", + args->tun, errno, strerror(errno)); + } else + report_exit(args, "tun %d exception", args->tun); + return -1; + } + + + // Check tun read + if (ev->events & EPOLLIN) { + uint8_t *buffer = ng_malloc(get_mtu(), "tun read"); + ssize_t length = read(args->tun, buffer, get_mtu()); + if (length < 0) { + ng_free(buffer, __FILE__, __LINE__); + + log_android(ANDROID_LOG_ERROR, "tun %d read error %d: %s", + args->tun, errno, strerror(errno)); + if (errno == EINTR || errno == EAGAIN) + // Retry later + return 0; + else { + report_exit(args, "tun %d read error %d: %s", + args->tun, errno, strerror(errno)); + return -1; + } + } else if (length > 0) { + // Write pcap record + if (pcap_file != NULL) + write_pcap_rec(buffer, (size_t) length); + + if (length > max_tun_msg) { + max_tun_msg = length; + log_android(ANDROID_LOG_WARN, "Maximum tun msg length %d", max_tun_msg); + } + + + + + + // Handle IP from tun + handle_ip(args, buffer, (size_t) length, epoll_fd, sessions, maxsessions); + + + + // Check sessions + + struct ng_session *ds = get_debug_session(args); + + if (ds > 0) { + + log_android(ANDROID_LOG_ERROR, "got debug session %d", ds); + + if (count % 10 == 0) { + log_android(ANDROID_LOG_ERROR, "Writing test ack to debug tcp session..."); + //write_ack(args, &ds->tcp); + } + + count += 1; + + + + } + + + ng_free(buffer, __FILE__, __LINE__); + } else { + // tun eof + ng_free(buffer, __FILE__, __LINE__); + + log_android(ANDROID_LOG_ERROR, "tun %d empty read", args->tun); + report_exit(args, "tun %d empty read", args->tun); + return -1; + } + } + + return 0; +} + +// https://en.wikipedia.org/wiki/IPv6_packet#Extension_headers +// http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml +int is_lower_layer(int protocol) { + // No next header = 59 + return (protocol == 0 || // Hop-by-Hop Options + protocol == 60 || // Destination Options (before routing header) + protocol == 43 || // Routing + protocol == 44 || // Fragment + protocol == 51 || // Authentication Header (AH) + protocol == 50 || // Encapsulating Security Payload (ESP) + protocol == 60 || // Destination Options (before upper-layer header) + protocol == 135); // Mobility +} + +int is_upper_layer(int protocol) { + return (protocol == IPPROTO_TCP || + protocol == IPPROTO_UDP || + protocol == IPPROTO_ICMP || + protocol == IPPROTO_ICMPV6); +} + + +void handle_ip(const struct arguments *args, + const uint8_t *pkt, const size_t length, + const int epoll_fd, + int sessions, int maxsessions) { + uint8_t protocol; + void *saddr; + void *daddr; + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + char flags[10]; + char data[16]; + int flen = 0; + uint8_t *payload; + + // Get protocol, addresses & payload + uint8_t version = (*pkt) >> 4; + if (version == 4) { + if (length < sizeof(struct iphdr)) { + log_android(ANDROID_LOG_WARN, "IP4 packet too short length %d", length); + return; + } + + + struct iphdr *ip4hdr = (struct iphdr *) pkt; + + protocol = ip4hdr->protocol; + saddr = &ip4hdr->saddr; + daddr = &ip4hdr->daddr; + + inet_ntop(version == 4 ? AF_INET : AF_INET6, saddr, source, sizeof(source)); + inet_ntop(version == 4 ? AF_INET : AF_INET6, daddr, dest, sizeof(dest)); + + + if (ip4hdr->frag_off & IP_MF) { + log_android(ANDROID_LOG_ERROR, "IP fragment offset %u", + (ip4hdr->frag_off & IP_OFFMASK) * 8); + return; + } + + uint8_t ipoptlen = (uint8_t) ((ip4hdr->ihl - 5) * 4); + log_android(ANDROID_LOG_ERROR, "IP opt len is: %u", ipoptlen); + payload = (uint8_t *) (pkt + sizeof(struct iphdr) + ipoptlen); + + log_android(ANDROID_LOG_ERROR, "Some length %u header length %u", + length, ntohs(ip4hdr->tot_len)); + + + if (ntohs(ip4hdr->tot_len) != length) { + log_android(ANDROID_LOG_ERROR, "Invalid length %u header length %u", + length, ntohs(ip4hdr->tot_len)); + return; + } + + if (loglevel < ANDROID_LOG_WARN) { + if (!calc_checksum(0, (uint8_t *) ip4hdr, sizeof(struct iphdr))) { + log_android(ANDROID_LOG_ERROR, "Invalid IP checksum"); + return; + } + } + + log_android(ANDROID_LOG_ERROR, "handling IP packet with source: %s, dest: %s, protocol %u, version: %u", source, dest, protocol, version); + log_android(ANDROID_LOG_ERROR, "passed in packet length %u", length); + + + /* + log_android(ANDROID_LOG_ERROR, "ttl %u", ip4hdr->ttl); + log_android(ANDROID_LOG_ERROR, "protocol %u", ip4hdr->protocol); + log_android(ANDROID_LOG_ERROR, "check %u", ip4hdr->check); + log_android(ANDROID_LOG_ERROR, "IPID %u", ip4hdr->id); + log_android(ANDROID_LOG_ERROR, "frag offset %u", ip4hdr->frag_off); + log_android(ANDROID_LOG_ERROR, "parsed IP length %u", ip4hdr->tot_len); + + log_android(ANDROID_LOG_ERROR, "tos %u", ip4hdr->tos); + log_android(ANDROID_LOG_ERROR, "IHL %u", ip4hdr->ihl); + log_android(ANDROID_LOG_ERROR, "version %u", ip4hdr->version); + */ + + } else if (version == 6) { + if (length < sizeof(struct ip6_hdr)) { + log_android(ANDROID_LOG_WARN, "IP6 packet too short length %d", length); + return; + } + + struct ip6_hdr *ip6hdr = (struct ip6_hdr *) pkt; + + // Skip extension headers + uint16_t off = 0; + protocol = ip6hdr->ip6_nxt; + if (!is_upper_layer(protocol)) { + log_android(ANDROID_LOG_WARN, "IP6 extension %d", protocol); + off = sizeof(struct ip6_hdr); + struct ip6_ext *ext = (struct ip6_ext *) (pkt + off); + while (is_lower_layer(ext->ip6e_nxt) && !is_upper_layer(protocol)) { + protocol = ext->ip6e_nxt; + log_android(ANDROID_LOG_WARN, "IP6 extension %d", protocol); + + off += (8 + ext->ip6e_len); + ext = (struct ip6_ext *) (pkt + off); + } + if (!is_upper_layer(protocol)) { + off = 0; + protocol = ip6hdr->ip6_nxt; + log_android(ANDROID_LOG_WARN, "IP6 final extension %d", protocol); + } + } + + saddr = &ip6hdr->ip6_src; + daddr = &ip6hdr->ip6_dst; + + payload = (uint8_t *) (pkt + sizeof(struct ip6_hdr) + off); + + // TODO checksum + } else { + log_android(ANDROID_LOG_ERROR, "Unknown version %d", version); + return; + } + + inet_ntop(version == 4 ? AF_INET : AF_INET6, saddr, source, sizeof(source)); + inet_ntop(version == 4 ? AF_INET : AF_INET6, daddr, dest, sizeof(dest)); + + log_android(ANDROID_LOG_ERROR, "handling IP packet with source: %s, dest: %s", source, dest); + + + + + // Get ports & flags + int syn = 0; + uint16_t sport = 0; + uint16_t dport = 0; + *data = 0; + if (protocol == IPPROTO_ICMP || protocol == IPPROTO_ICMPV6) { + if (length - (payload - pkt) < ICMP_MINLEN) { + log_android(ANDROID_LOG_WARN, "ICMP packet too short"); + return; + } + + struct icmp *icmp = (struct icmp *) payload; + + sprintf(data, "type %d/%d", icmp->icmp_type, icmp->icmp_code); + + // http://lwn.net/Articles/443051/ + sport = ntohs(icmp->icmp_id); + dport = ntohs(icmp->icmp_id); + + } else if (protocol == IPPROTO_UDP) { + if (length - (payload - pkt) < sizeof(struct udphdr)) { + log_android(ANDROID_LOG_WARN, "UDP packet too short"); + return; + } + + struct udphdr *udp = (struct udphdr *) payload; + + sport = ntohs(udp->source); + dport = ntohs(udp->dest); + + // TODO checksum (IPv6) + } else if (protocol == IPPROTO_TCP) { + if (length - (payload - pkt) < sizeof(struct tcphdr)) { + log_android(ANDROID_LOG_WARN, "TCP packet too short"); + return; + } + + struct tcphdr *tcp = (struct tcphdr *) payload; + + sport = ntohs(tcp->source); + dport = ntohs(tcp->dest); + + if (tcp->syn) { + syn = 1; + flags[flen++] = 'S'; + } + if (tcp->ack) + flags[flen++] = 'A'; + if (tcp->psh) + flags[flen++] = 'P'; + if (tcp->fin) + flags[flen++] = 'F'; + if (tcp->rst) + flags[flen++] = 'R'; + + // TODO checksum + } else if (protocol != IPPROTO_HOPOPTS && protocol != IPPROTO_IGMP && protocol != IPPROTO_ESP) + log_android(ANDROID_LOG_WARN, "Unknown protocol %d", protocol); + + flags[flen] = 0; + + // Limit number of sessions + if (sessions >= maxsessions) { + if ((protocol == IPPROTO_ICMP || protocol == IPPROTO_ICMPV6) || + (protocol == IPPROTO_UDP && !has_udp_session(args, pkt, payload)) || + (protocol == IPPROTO_TCP && syn)) { + log_android(ANDROID_LOG_ERROR, + "%d of max %d sessions, dropping version %d protocol %d", + sessions, maxsessions, protocol, version); + return; + } + } + + // Get uid + jint uid = -1; + if (protocol == IPPROTO_ICMP || protocol == IPPROTO_ICMPV6 || + (protocol == IPPROTO_UDP && !has_udp_session(args, pkt, payload)) || + (protocol == IPPROTO_TCP && syn)) { + if (args->ctx->sdk <= 28) // Android 9 Pie + uid = get_uid(version, protocol, saddr, sport, daddr, dport); + else + uid = get_uid_q(args, version, protocol, source, sport, dest, dport); + } + + // Check if allowed + int allowed = 0; + struct allowed *redirect = NULL; + if (protocol == IPPROTO_UDP && has_udp_session(args, pkt, payload)) + allowed = 1; // could be a lingering/blocked session + else if (protocol == IPPROTO_TCP && (!syn || (uid == 0 && dport == 53))) + allowed = 1; // assume existing session + else { + jobject objPacket = create_packet( + args, version, protocol, flags, source, sport, dest, dport, data, uid, 0); + redirect = is_address_allowed(args, objPacket); + allowed = (redirect != NULL); + if (redirect != NULL && (*redirect->raddr == 0 || redirect->rport == 0)) + redirect = NULL; + } + + + + // START: create debug tcp session and write packets to it + debug_set += 1; + if (debug_set == 20) { + log_android(ANDROID_LOG_ERROR, "handling debug socket init"); + debug_socket_init(args, epoll_fd); + } else if(debug_set < 20) { + log_android(ANDROID_LOG_ERROR, "Waiting for more packets to start debug sesh --> %d/20", debug_set); + } else if (debug_set > 20 && debug_set < 40) { + log_android(ANDROID_LOG_ERROR, "Waiting for more packets to start writing to the debug sesh --> %d/40", debug_set); + } else { + log_android(ANDROID_LOG_ERROR, "Finished writing to debug server --> %d", debug_set); + + // TODO send full packet info here instead + char data_buffer[100]; + sprintf(data_buffer, ">> Handling IP packet with source: %s, dest: %s\n\n\n", source, dest); + write_debug_socket(args, epoll_fd,data_buffer, 62); + } + + // END: debug session + + if (dport == 50508 || sport == 50508) { // if debug session + log_android(ANDROID_LOG_ERROR, "Found debug IP packet, change uid.."); + uid = -1; + allowed = 1; + redirect = NULL; + } + + log_android(ANDROID_LOG_ERROR, + "BPB Packet v%d %s/%u > %s/%u proto %d flags %s uid %d", + version, source, sport, dest, dport, protocol, flags, uid); + + + + if (allowed) { + if (protocol == IPPROTO_ICMP || protocol == IPPROTO_ICMPV6) + handle_icmp(args, pkt, length, payload, uid, epoll_fd); + else if (protocol == IPPROTO_UDP) + handle_udp(args, pkt, length, payload, uid, redirect, epoll_fd); + else if (protocol == IPPROTO_TCP) + handle_tcp(args, pkt, length, payload, uid, allowed, redirect, epoll_fd); + } else { + if (protocol == IPPROTO_UDP) + block_udp(args, pkt, length, payload, uid); + + log_android(ANDROID_LOG_WARN, "Address v%d p%d %s/%u syn %d not allowed", + version, protocol, dest, dport, syn); + } +} + +jint get_uid(const int version, const int protocol, + const void *saddr, const uint16_t sport, + const void *daddr, const uint16_t dport) { + jint uid = -1; + + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + inet_ntop(version == 4 ? AF_INET : AF_INET6, saddr, source, sizeof(source)); + inet_ntop(version == 4 ? AF_INET : AF_INET6, daddr, dest, sizeof(dest)); + + struct timeval time; + gettimeofday(&time, NULL); + long now = (time.tv_sec * 1000) + (time.tv_usec / 1000); + + // Check IPv6 table first + if (version == 4) { + int8_t saddr128[16]; + memset(saddr128, 0, 10); + saddr128[10] = (uint8_t) 0xFF; + saddr128[11] = (uint8_t) 0xFF; + memcpy(saddr128 + 12, saddr, 4); + + int8_t daddr128[16]; + memset(daddr128, 0, 10); + daddr128[10] = (uint8_t) 0xFF; + daddr128[11] = (uint8_t) 0xFF; + memcpy(daddr128 + 12, daddr, 4); + + uid = get_uid_sub(6, protocol, saddr128, sport, daddr128, dport, source, dest, now); + log_android(ANDROID_LOG_DEBUG, "uid v%d p%d %s/%u > %s/%u => %d as inet6", + version, protocol, source, sport, dest, dport, uid); + } + + if (uid == -1) { + uid = get_uid_sub(version, protocol, saddr, sport, daddr, dport, source, dest, now); + log_android(ANDROID_LOG_DEBUG, "uid v%d p%d %s/%u > %s/%u => %d fallback", + version, protocol, source, sport, dest, dport, uid); + } + + if (uid == -1) + log_android(ANDROID_LOG_WARN, "uid v%d p%d %s/%u > %s/%u => not found", + version, protocol, source, sport, dest, dport); + else if (uid >= 0) + log_android(ANDROID_LOG_INFO, "uid v%d p%d %s/%u > %s/%u => %d", + version, protocol, source, sport, dest, dport, uid); + + return uid; +} + +int uid_cache_size = 0; +struct uid_cache_entry *uid_cache = NULL; + +jint get_uid_sub(const int version, const int protocol, + const void *saddr, const uint16_t sport, + const void *daddr, const uint16_t dport, + const char *source, const char *dest, + long now) { + // NETLINK is not available on Android due to SELinux policies :-( + // http://stackoverflow.com/questions/27148536/netlink-implementation-for-the-android-ndk + // https://android.googlesource.com/platform/system/sepolicy/+/master/private/app.te (netlink_tcpdiag_socket) + + static uint8_t zero[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + + int ws = (version == 4 ? 1 : 4); + + // Check cache + for (int i = 0; i < uid_cache_size; i++) + if (now - uid_cache[i].time <= UID_MAX_AGE && + uid_cache[i].version == version && + uid_cache[i].protocol == protocol && + uid_cache[i].sport == sport && + (uid_cache[i].dport == dport || uid_cache[i].dport == 0) && + (memcmp(uid_cache[i].saddr, saddr, (size_t) (ws * 4)) == 0 || + memcmp(uid_cache[i].saddr, zero, (size_t) (ws * 4)) == 0) && + (memcmp(uid_cache[i].daddr, daddr, (size_t) (ws * 4)) == 0 || + memcmp(uid_cache[i].daddr, zero, (size_t) (ws * 4)) == 0)) { + + log_android(ANDROID_LOG_INFO, "uid v%d p%d %s/%u > %s/%u => %d (from cache)", + version, protocol, source, sport, dest, dport, uid_cache[i].uid); + + return uid_cache[i].uid; + } + + // Get proc file name + char *fn = NULL; + if (protocol == IPPROTO_ICMP && version == 4) + fn = "/proc/net/icmp"; + else if (protocol == IPPROTO_ICMPV6 && version == 6) + fn = "/proc/net/icmp6"; + else if (protocol == IPPROTO_TCP) + fn = (version == 4 ? "/proc/net/tcp" : "/proc/net/tcp6"); + else if (protocol == IPPROTO_UDP) + fn = (version == 4 ? "/proc/net/udp" : "/proc/net/udp6"); + else + return -1; + + // Open proc file + FILE *fd = fopen(fn, "r"); + if (fd == NULL) { + log_android(ANDROID_LOG_ERROR, "fopen %s error %d: %s", fn, errno, strerror(errno)); + return -2; + } + + jint uid = -1; + + char line[250]; + int fields; + + char shex[16 * 2 + 1]; + uint8_t _saddr[16]; + int _sport; + + char dhex[16 * 2 + 1]; + uint8_t _daddr[16]; + int _dport; + + jint _uid; + + // Scan proc file + int l = 0; + *line = 0; + int c = 0; + const char *fmt = (version == 4 + ? "%*d: %8s:%X %8s:%X %*X %*lX:%*lX %*X:%*X %*X %d %*d %*ld" + : "%*d: %32s:%X %32s:%X %*X %*lX:%*lX %*X:%*X %*X %d %*d %*ld"); + while (fgets(line, sizeof(line), fd) != NULL) { + if (!l++) + continue; + + fields = sscanf(line, fmt, shex, &_sport, dhex, &_dport, &_uid); + if (fields == 5 && strlen(shex) == ws * 8 && strlen(dhex) == ws * 8) { + hex2bytes(shex, _saddr); + hex2bytes(dhex, _daddr); + + for (int w = 0; w < ws; w++) + ((uint32_t *) _saddr)[w] = htonl(((uint32_t *) _saddr)[w]); + + for (int w = 0; w < ws; w++) + ((uint32_t *) _daddr)[w] = htonl(((uint32_t *) _daddr)[w]); + + if (_sport == sport && + (_dport == dport || _dport == 0) && + (memcmp(_saddr, saddr, (size_t) (ws * 4)) == 0 || + memcmp(_saddr, zero, (size_t) (ws * 4)) == 0) && + (memcmp(_daddr, daddr, (size_t) (ws * 4)) == 0 || + memcmp(_daddr, zero, (size_t) (ws * 4)) == 0)) + uid = _uid; + + for (; c < uid_cache_size; c++) + if (now - uid_cache[c].time > UID_MAX_AGE) + break; + + if (c >= uid_cache_size) { + if (uid_cache_size == 0) + uid_cache = ng_malloc(sizeof(struct uid_cache_entry), "uid_cache init"); + else + uid_cache = ng_realloc(uid_cache, + sizeof(struct uid_cache_entry) * + (uid_cache_size + 1), "uid_cache extend"); + c = uid_cache_size; + uid_cache_size++; + } + + uid_cache[c].version = (uint8_t) version; + uid_cache[c].protocol = (uint8_t) protocol; + memcpy(uid_cache[c].saddr, _saddr, (size_t) (ws * 4)); + uid_cache[c].sport = (uint16_t) _sport; + memcpy(uid_cache[c].daddr, _daddr, (size_t) (ws * 4)); + uid_cache[c].dport = (uint16_t) _dport; + uid_cache[c].uid = _uid; + uid_cache[c].time = now; + } else { + log_android(ANDROID_LOG_ERROR, "Invalid field #%d: %s", fields, line); + return -2; + } + } + + if (fclose(fd)) + log_android(ANDROID_LOG_ERROR, "fclose %s error %d: %s", fn, errno, strerror(errno)); + + return uid; +} diff --git a/NetGuard/app/src/main/main/jni/netguard/netguard.c b/NetGuard/app/src/main/main/jni/netguard/netguard.c new file mode 100644 index 0000000..0544d08 --- /dev/null +++ b/NetGuard/app/src/main/main/jni/netguard/netguard.c @@ -0,0 +1,1116 @@ +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +#include "netguard.h" + +// It is assumed that no packets will get lost and that packets arrive in order +// https://android.googlesource.com/platform/frameworks/base.git/+/master/services/core/jni/com_android_server_connectivity_Vpn.cpp + +// Global variables + +char socks5_addr[INET6_ADDRSTRLEN + 1]; +int socks5_port = 0; +char socks5_username[127 + 1]; +char socks5_password[127 + 1]; +int loglevel = ANDROID_LOG_WARN; + +extern int max_tun_msg; + +extern FILE *pcap_file; +extern size_t pcap_record_size; +extern long pcap_file_size; + +extern int uid_cache_size; +extern struct uid_cache_entry *uid_cache; + +// JNI + +jclass clsPacket; +jclass clsAllowed; +jclass clsRR; +jclass clsUsage; + +jint JNI_OnLoad(JavaVM *vm, void *reserved) { + log_android(ANDROID_LOG_INFO, "JNI load"); + + JNIEnv *env; + if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) { + log_android(ANDROID_LOG_INFO, "JNI load GetEnv failed"); + return -1; + } + + const char *packet = "eu/faircode/netguard/Packet"; + clsPacket = jniGlobalRef(env, jniFindClass(env, packet)); + ng_add_alloc(clsPacket, "clsPacket"); + + const char *allowed = "eu/faircode/netguard/Allowed"; + clsAllowed = jniGlobalRef(env, jniFindClass(env, allowed)); + ng_add_alloc(clsAllowed, "clsAllowed"); + + const char *rr = "eu/faircode/netguard/ResourceRecord"; + clsRR = jniGlobalRef(env, jniFindClass(env, rr)); + ng_add_alloc(clsRR, "clsRR"); + + const char *usage = "eu/faircode/netguard/Usage"; + clsUsage = jniGlobalRef(env, jniFindClass(env, usage)); + ng_add_alloc(clsUsage, "clsUsage"); + + // Raise file number limit to maximum + struct rlimit rlim; + if (getrlimit(RLIMIT_NOFILE, &rlim)) + log_android(ANDROID_LOG_WARN, "getrlimit error %d: %s", errno, strerror(errno)); + else { + rlim_t soft = rlim.rlim_cur; + rlim.rlim_cur = rlim.rlim_max; + if (setrlimit(RLIMIT_NOFILE, &rlim)) + log_android(ANDROID_LOG_WARN, "setrlimit error %d: %s", errno, strerror(errno)); + else + log_android(ANDROID_LOG_WARN, "raised file limit from %d to %d", soft, rlim.rlim_cur); + } + + return JNI_VERSION_1_6; +} + +void JNI_OnUnload(JavaVM *vm, void *reserved) { + log_android(ANDROID_LOG_INFO, "JNI unload"); + + JNIEnv *env; + if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) + log_android(ANDROID_LOG_INFO, "JNI load GetEnv failed"); + else { + (*env)->DeleteGlobalRef(env, clsPacket); + (*env)->DeleteGlobalRef(env, clsAllowed); + (*env)->DeleteGlobalRef(env, clsRR); + (*env)->DeleteGlobalRef(env, clsUsage); + ng_delete_alloc(clsPacket, __FILE__, __LINE__); + ng_delete_alloc(clsAllowed, __FILE__, __LINE__); + ng_delete_alloc(clsRR, __FILE__, __LINE__); + ng_delete_alloc(clsUsage, __FILE__, __LINE__); + } +} + +// JNI ServiceSinkhole + +JNIEXPORT jlong JNICALL +Java_eu_faircode_netguard_ServiceSinkhole_jni_1init( + JNIEnv *env, jobject instance, jint sdk) { + struct context *ctx = ng_calloc(1, sizeof(struct context), "init"); + ctx->sdk = sdk; + + loglevel = ANDROID_LOG_WARN; + + *socks5_addr = 0; + socks5_port = 0; + *socks5_username = 0; + *socks5_password = 0; + pcap_file = NULL; + + if (pthread_mutex_init(&ctx->lock, NULL)) + log_android(ANDROID_LOG_ERROR, "pthread_mutex_init failed"); + + // Create signal pipe + if (pipe(ctx->pipefds)) + log_android(ANDROID_LOG_ERROR, "Create pipe error %d: %s", errno, strerror(errno)); + else + for (int i = 0; i < 2; i++) { + int flags = fcntl(ctx->pipefds[i], F_GETFL, 0); + if (flags < 0 || fcntl(ctx->pipefds[i], F_SETFL, flags | O_NONBLOCK) < 0) + log_android(ANDROID_LOG_ERROR, "fcntl pipefds[%d] O_NONBLOCK error %d: %s", + i, errno, strerror(errno)); + } + + return (jlong) ctx; +} + +JNIEXPORT void JNICALL +Java_eu_faircode_netguard_ServiceSinkhole_jni_1start( + JNIEnv *env, jobject instance, jlong context, jint loglevel_) { + struct context *ctx = (struct context *) context; + + loglevel = loglevel_; + max_tun_msg = 0; + ctx->stopping = 0; + + log_android(ANDROID_LOG_WARN, "Starting level %d", loglevel); + +} + +JNIEXPORT void JNICALL +Java_eu_faircode_netguard_ServiceSinkhole_jni_1run( + JNIEnv *env, jobject instance, jlong context, jint tun, jboolean fwd53, jint rcode) { + struct context *ctx = (struct context *) context; + + log_android(ANDROID_LOG_WARN, "Running tun %d fwd53 %d level %d", tun, fwd53, loglevel); + + // Set blocking + int flags = fcntl(tun, F_GETFL, 0); + if (flags < 0 || fcntl(tun, F_SETFL, flags & ~O_NONBLOCK) < 0) + log_android(ANDROID_LOG_ERROR, "fcntl tun ~O_NONBLOCK error %d: %s", + errno, strerror(errno)); + + // Get arguments + struct arguments *args = ng_malloc(sizeof(struct arguments), "arguments"); + args->env = env; + args->instance = instance; + args->tun = tun; + args->fwd53 = fwd53; + args->rcode = rcode; + args->ctx = ctx; + + + handle_events(args); +} + +JNIEXPORT void JNICALL +Java_eu_faircode_netguard_ServiceSinkhole_jni_1stop( + JNIEnv *env, jobject instance, jlong context) { + struct context *ctx = (struct context *) context; + ctx->stopping = 1; + + log_android(ANDROID_LOG_WARN, "Write pipe wakeup"); + log_android(ANDROID_LOG_ERROR, "writing to file descriptor: %d", ctx->pipefds[1]); + if (write(ctx->pipefds[1], "w", 1) < 0) + log_android(ANDROID_LOG_WARN, "Write pipe error %d: %s", errno, strerror(errno)); +} + +JNIEXPORT void JNICALL +Java_eu_faircode_netguard_ServiceSinkhole_jni_1clear( + JNIEnv *env, jobject instance, jlong context) { + struct context *ctx = (struct context *) context; + clear(ctx); +} + +JNIEXPORT jint JNICALL +Java_eu_faircode_netguard_ServiceSinkhole_jni_1get_1mtu(JNIEnv *env, jobject instance) { + return get_mtu(); +} + +JNIEXPORT jintArray JNICALL +Java_eu_faircode_netguard_ServiceSinkhole_jni_1get_1stats( + JNIEnv *env, jobject instance, jlong context) { + struct context *ctx = (struct context *) context; + + if (pthread_mutex_lock(&ctx->lock)) + log_android(ANDROID_LOG_ERROR, "pthread_mutex_lock failed"); + + jintArray jarray = (*env)->NewIntArray(env, 5); + jint *jcount = (*env)->GetIntArrayElements(env, jarray, NULL); + + struct ng_session *s = ctx->ng_session; + while (s != NULL) { + if (s->protocol == IPPROTO_ICMP || s->protocol == IPPROTO_ICMPV6) { + if (!s->icmp.stop) + jcount[0]++; + } else if (s->protocol == IPPROTO_UDP) { + if (s->udp.state == UDP_ACTIVE) + jcount[1]++; + } else if (s->protocol == IPPROTO_TCP) { + if (s->tcp.state != TCP_CLOSING && s->tcp.state != TCP_CLOSE) + jcount[2]++; + } + s = s->next; + } + + if (pthread_mutex_unlock(&ctx->lock)) + log_android(ANDROID_LOG_ERROR, "pthread_mutex_unlock failed"); + + jcount[3] = 0; + DIR *d = opendir("/proc/self/fd"); + if (d) { + struct dirent *dir; + while ((dir = readdir(d)) != NULL) + if (dir->d_type != DT_DIR) + jcount[3]++; + closedir(d); + } + + struct rlimit rlim; + memset(&rlim, 0, sizeof(struct rlimit)); + getrlimit(RLIMIT_NOFILE, &rlim); + jcount[4] = (jint) rlim.rlim_cur; + + (*env)->ReleaseIntArrayElements(env, jarray, jcount, 0); + return jarray; +} + +JNIEXPORT void JNICALL +Java_eu_faircode_netguard_ServiceSinkhole_jni_1pcap( + JNIEnv *env, jclass type, + jstring name_, jint record_size, jint file_size) { + + pcap_record_size = (size_t) record_size; + pcap_file_size = file_size; + + //if (pthread_mutex_lock(&lock)) + // log_android(ANDROID_LOG_ERROR, "pthread_mutex_lock failed"); + + if (name_ == NULL) { + if (pcap_file != NULL) { + int flags = fcntl(fileno(pcap_file), F_GETFL, 0); + if (flags < 0 || fcntl(fileno(pcap_file), F_SETFL, flags & ~O_NONBLOCK) < 0) + log_android(ANDROID_LOG_ERROR, "PCAP fcntl ~O_NONBLOCK error %d: %s", + errno, strerror(errno)); + + if (fsync(fileno(pcap_file))) + log_android(ANDROID_LOG_ERROR, "PCAP fsync error %d: %s", errno, strerror(errno)); + + if (fclose(pcap_file)) + log_android(ANDROID_LOG_ERROR, "PCAP fclose error %d: %s", errno, strerror(errno)); + + pcap_file = NULL; + } + log_android(ANDROID_LOG_WARN, "PCAP disabled"); + } else { + const char *name = (*env)->GetStringUTFChars(env, name_, 0); + ng_add_alloc(name, "name"); + log_android(ANDROID_LOG_WARN, "PCAP file %s record size %d truncate @%ld", + name, pcap_record_size, pcap_file_size); + + pcap_file = fopen(name, "ab+"); + if (pcap_file == NULL) + log_android(ANDROID_LOG_ERROR, "PCAP fopen error %d: %s", errno, strerror(errno)); + else { + int flags = fcntl(fileno(pcap_file), F_GETFL, 0); + if (flags < 0 || fcntl(fileno(pcap_file), F_SETFL, flags | O_NONBLOCK) < 0) + log_android(ANDROID_LOG_ERROR, "PCAP fcntl O_NONBLOCK error %d: %s", + errno, strerror(errno)); + + long size = ftell(pcap_file); + if (size == 0) { + log_android(ANDROID_LOG_WARN, "PCAP initialize"); + write_pcap_hdr(); + } else + log_android(ANDROID_LOG_WARN, "PCAP current size %ld", size); + } + + (*env)->ReleaseStringUTFChars(env, name_, name); + ng_delete_alloc(name, __FILE__, __LINE__); + } + + //if (pthread_mutex_unlock(&lock)) + // log_android(ANDROID_LOG_ERROR, "pthread_mutex_unlock failed"); +} + +JNIEXPORT void JNICALL +Java_eu_faircode_netguard_ServiceSinkhole_jni_1socks5(JNIEnv *env, jobject instance, jstring addr_, + jint port, jstring username_, + jstring password_) { + const char *addr = (*env)->GetStringUTFChars(env, addr_, 0); + const char *username = (*env)->GetStringUTFChars(env, username_, 0); + const char *password = (*env)->GetStringUTFChars(env, password_, 0); + ng_add_alloc(addr, "addr"); + ng_add_alloc(username, "username"); + ng_add_alloc(password, "password"); + + strcpy(socks5_addr, addr); + socks5_port = port; + strcpy(socks5_username, username); + strcpy(socks5_password, password); + + log_android(ANDROID_LOG_WARN, "SOCKS5 %s:%d user=%s", + socks5_addr, socks5_port, socks5_username); + + (*env)->ReleaseStringUTFChars(env, addr_, addr); + (*env)->ReleaseStringUTFChars(env, username_, username); + (*env)->ReleaseStringUTFChars(env, password_, password); + ng_delete_alloc(addr, __FILE__, __LINE__); + ng_delete_alloc(username, __FILE__, __LINE__); + ng_delete_alloc(password, __FILE__, __LINE__); +} + +JNIEXPORT void JNICALL +Java_eu_faircode_netguard_ServiceSinkhole_jni_1done( + JNIEnv *env, jobject instance, jlong context) { + struct context *ctx = (struct context *) context; + log_android(ANDROID_LOG_INFO, "Done"); + + clear(ctx); + + if (pthread_mutex_destroy(&ctx->lock)) + log_android(ANDROID_LOG_ERROR, "pthread_mutex_destroy failed"); + + for (int i = 0; i < 2; i++) + if (close(ctx->pipefds[i])) + log_android(ANDROID_LOG_ERROR, "Close pipe error %d: %s", errno, strerror(errno)); + + if (uid_cache != NULL) + ng_free(uid_cache, __FILE__, __LINE__); + uid_cache_size = 0; + uid_cache = NULL; + + ng_free(ctx, __FILE__, __LINE__); +} + +// JNI Util + +JNIEXPORT jstring JNICALL +Java_eu_faircode_netguard_Util_jni_1getprop(JNIEnv *env, jclass type, jstring name_) { + const char *name = (*env)->GetStringUTFChars(env, name_, 0); + ng_add_alloc(name, "name"); + + char value[PROP_VALUE_MAX + 1] = ""; + __system_property_get(name, value); + + (*env)->ReleaseStringUTFChars(env, name_, name); + ng_delete_alloc(name, __FILE__, __LINE__); + + return (*env)->NewStringUTF(env, value); // Freed by Java +} + +JNIEXPORT jboolean JNICALL +Java_eu_faircode_netguard_Util_is_1numeric_1address(JNIEnv *env, jclass type, jstring ip_) { + jboolean numeric = 0; + const char *ip = (*env)->GetStringUTFChars(env, ip_, 0); + ng_add_alloc(ip, "ip"); + + struct addrinfo hints; + memset(&hints, 0, sizeof(struct addrinfo)); + hints.ai_family = AF_UNSPEC; + hints.ai_flags = AI_NUMERICHOST; + struct addrinfo *result; + int err = getaddrinfo(ip, NULL, &hints, &result); + if (err) + log_android(ANDROID_LOG_DEBUG, "getaddrinfo(%s) error %d: %s", ip, err, gai_strerror(err)); + else + numeric = (jboolean) (result != NULL); + + if (result != NULL) + freeaddrinfo(result); + + (*env)->ReleaseStringUTFChars(env, ip_, ip); + ng_delete_alloc(ip, __FILE__, __LINE__); + return numeric; +} + +void report_exit(const struct arguments *args, const char *fmt, ...) { + jclass cls = (*args->env)->GetObjectClass(args->env, args->instance); + ng_add_alloc(cls, "cls"); + jmethodID mid = jniGetMethodID(args->env, cls, "nativeExit", "(Ljava/lang/String;)V"); + + jstring jreason = NULL; + if (fmt != NULL) { + char line[1024]; + va_list argptr; + va_start(argptr, fmt); + vsprintf(line, fmt, argptr); + jreason = (*args->env)->NewStringUTF(args->env, line); + ng_add_alloc(jreason, "jreason"); + va_end(argptr); + } + + (*args->env)->CallVoidMethod(args->env, args->instance, mid, jreason); + jniCheckException(args->env); + + if (jreason != NULL) { + (*args->env)->DeleteLocalRef(args->env, jreason); + ng_delete_alloc(jreason, __FILE__, __LINE__); + } + (*args->env)->DeleteLocalRef(args->env, cls); + ng_delete_alloc(cls, __FILE__, __LINE__); +} + +void report_error(const struct arguments *args, jint error, const char *fmt, ...) { + jclass cls = (*args->env)->GetObjectClass(args->env, args->instance); + ng_add_alloc(cls, "cls"); + jmethodID mid = jniGetMethodID(args->env, cls, "nativeError", "(ILjava/lang/String;)V"); + + jstring jreason = NULL; + if (fmt != NULL) { + char line[1024]; + va_list argptr; + va_start(argptr, fmt); + vsprintf(line, fmt, argptr); + jreason = (*args->env)->NewStringUTF(args->env, line); + ng_add_alloc(jreason, "jreason"); + va_end(argptr); + } + + (*args->env)->CallVoidMethod(args->env, args->instance, mid, error, jreason); + jniCheckException(args->env); + + if (jreason != NULL) { + (*args->env)->DeleteLocalRef(args->env, jreason); + ng_delete_alloc(jreason, __FILE__, __LINE__); + } + (*args->env)->DeleteLocalRef(args->env, cls); + ng_delete_alloc(cls, __FILE__, __LINE__); +} + +static jmethodID midProtect = NULL; + +int protect_socket(const struct arguments *args, int socket) { + if (args->ctx->sdk >= 21) + return 0; + + jclass cls = (*args->env)->GetObjectClass(args->env, args->instance); + ng_add_alloc(cls, "cls"); + if (cls == NULL) { + log_android(ANDROID_LOG_ERROR, "protect socket failed to get class"); + return -1; + } + + if (midProtect == NULL) + midProtect = jniGetMethodID(args->env, cls, "protect", "(I)Z"); + if (midProtect == NULL) { + log_android(ANDROID_LOG_ERROR, "protect socket failed to get method"); + return -1; + } + + jboolean isProtected = (*args->env)->CallBooleanMethod( + args->env, args->instance, midProtect, socket); + jniCheckException(args->env); + + if (!isProtected) { + log_android(ANDROID_LOG_ERROR, "protect socket failed"); + return -1; + } + + (*args->env)->DeleteLocalRef(args->env, cls); + ng_delete_alloc(cls, __FILE__, __LINE__); + + return 0; +} + +// http://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html +// http://journals.ecs.soton.ac.uk/java/tutorial/native1.1/implementing/index.html + +jobject jniGlobalRef(JNIEnv *env, jobject cls) { + jobject gcls = (*env)->NewGlobalRef(env, cls); + if (gcls == NULL) + log_android(ANDROID_LOG_ERROR, "Global ref failed (out of memory?)"); + return gcls; +} + +jclass jniFindClass(JNIEnv *env, const char *name) { + jclass cls = (*env)->FindClass(env, name); + if (cls == NULL) + log_android(ANDROID_LOG_ERROR, "Class %s not found", name); + else + jniCheckException(env); + return cls; +} + +jmethodID jniGetMethodID(JNIEnv *env, jclass cls, const char *name, const char *signature) { + jmethodID method = (*env)->GetMethodID(env, cls, name, signature); + if (method == NULL) { + log_android(ANDROID_LOG_ERROR, "Method %s %s not found", name, signature); + jniCheckException(env); + } + return method; +} + +jfieldID jniGetFieldID(JNIEnv *env, jclass cls, const char *name, const char *type) { + jfieldID field = (*env)->GetFieldID(env, cls, name, type); + if (field == NULL) + log_android(ANDROID_LOG_ERROR, "Field %s type %s not found", name, type); + return field; +} + +jobject jniNewObject(JNIEnv *env, jclass cls, jmethodID constructor, const char *name) { + jobject object = (*env)->NewObject(env, cls, constructor); + if (object == NULL) + log_android(ANDROID_LOG_ERROR, "Create object %s failed", name); + else + jniCheckException(env); + return object; +} + +int jniCheckException(JNIEnv *env) { + jthrowable ex = (*env)->ExceptionOccurred(env); + if (ex) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + (*env)->DeleteLocalRef(env, ex); + ng_delete_alloc(ex, __FILE__, __LINE__); + return 1; + } + return 0; +} + +static jmethodID midLogPacket = NULL; + +void log_packet(const struct arguments *args, jobject jpacket) { +#ifdef PROFILE_JNI + float mselapsed; + struct timeval start, end; + gettimeofday(&start, NULL); +#endif + + jclass clsService = (*args->env)->GetObjectClass(args->env, args->instance); + ng_add_alloc(clsService, "clsService"); + + const char *signature = "(Leu/faircode/netguard/Packet;)V"; + if (midLogPacket == NULL) + midLogPacket = jniGetMethodID(args->env, clsService, "logPacket", signature); + + (*args->env)->CallVoidMethod(args->env, args->instance, midLogPacket, jpacket); + jniCheckException(args->env); + + (*args->env)->DeleteLocalRef(args->env, clsService); + (*args->env)->DeleteLocalRef(args->env, jpacket); + ng_delete_alloc(clsService, __FILE__, __LINE__); + ng_delete_alloc(jpacket, __FILE__, __LINE__); + +#ifdef PROFILE_JNI + gettimeofday(&end, NULL); + mselapsed = (end.tv_sec - start.tv_sec) * 1000.0 + + (end.tv_usec - start.tv_usec) / 1000.0; + if (mselapsed > PROFILE_JNI) + log_android(ANDROID_LOG_WARN, "log_packet %f", mselapsed); +#endif +} + +static jmethodID midDnsResolved = NULL; +static jmethodID midInitRR = NULL; +jfieldID fidQTime = NULL; +jfieldID fidQName = NULL; +jfieldID fidAName = NULL; +jfieldID fidResource = NULL; +jfieldID fidTTL = NULL; + +void dns_resolved(const struct arguments *args, + const char *qname, const char *aname, const char *resource, int ttl) { +#ifdef PROFILE_JNI + float mselapsed; + struct timeval start, end; + gettimeofday(&start, NULL); +#endif + + jclass clsService = (*args->env)->GetObjectClass(args->env, args->instance); + ng_add_alloc(clsService, "clsService"); + + const char *signature = "(Leu/faircode/netguard/ResourceRecord;)V"; + if (midDnsResolved == NULL) + midDnsResolved = jniGetMethodID(args->env, clsService, "dnsResolved", signature); + + const char *rr = "eu/faircode/netguard/ResourceRecord"; + if (midInitRR == NULL) + midInitRR = jniGetMethodID(args->env, clsRR, "", "()V"); + + jobject jrr = jniNewObject(args->env, clsRR, midInitRR, rr); + ng_add_alloc(jrr, "jrr"); + + if (fidQTime == NULL) { + const char *string = "Ljava/lang/String;"; + fidQTime = jniGetFieldID(args->env, clsRR, "Time", "J"); + fidQName = jniGetFieldID(args->env, clsRR, "QName", string); + fidAName = jniGetFieldID(args->env, clsRR, "AName", string); + fidResource = jniGetFieldID(args->env, clsRR, "Resource", string); + fidTTL = jniGetFieldID(args->env, clsRR, "TTL", "I"); + } + + jlong jtime = time(NULL) * 1000LL; + jstring jqname = (*args->env)->NewStringUTF(args->env, qname); + jstring janame = (*args->env)->NewStringUTF(args->env, aname); + jstring jresource = (*args->env)->NewStringUTF(args->env, resource); + ng_add_alloc(jqname, "jqname"); + ng_add_alloc(janame, "janame"); + ng_add_alloc(jresource, "jresource"); + + (*args->env)->SetLongField(args->env, jrr, fidQTime, jtime); + (*args->env)->SetObjectField(args->env, jrr, fidQName, jqname); + (*args->env)->SetObjectField(args->env, jrr, fidAName, janame); + (*args->env)->SetObjectField(args->env, jrr, fidResource, jresource); + (*args->env)->SetIntField(args->env, jrr, fidTTL, ttl); + + (*args->env)->CallVoidMethod(args->env, args->instance, midDnsResolved, jrr); + jniCheckException(args->env); + + (*args->env)->DeleteLocalRef(args->env, jresource); + (*args->env)->DeleteLocalRef(args->env, janame); + (*args->env)->DeleteLocalRef(args->env, jqname); + (*args->env)->DeleteLocalRef(args->env, jrr); + (*args->env)->DeleteLocalRef(args->env, clsService); + ng_delete_alloc(jresource, __FILE__, __LINE__); + ng_delete_alloc(janame, __FILE__, __LINE__); + ng_delete_alloc(jqname, __FILE__, __LINE__); + ng_delete_alloc(jrr, __FILE__, __LINE__); + ng_delete_alloc(clsService, __FILE__, __LINE__); + +#ifdef PROFILE_JNI + gettimeofday(&end, NULL); + mselapsed = (end.tv_sec - start.tv_sec) * 1000.0 + + (end.tv_usec - start.tv_usec) / 1000.0; + if (mselapsed > PROFILE_JNI) + log_android(ANDROID_LOG_WARN, "log_packet %f", mselapsed); +#endif +} + +static jmethodID midIsDomainBlocked = NULL; + +jboolean is_domain_blocked(const struct arguments *args, const char *name) { +#ifdef PROFILE_JNI + float mselapsed; + struct timeval start, end; + gettimeofday(&start, NULL); +#endif + + jclass clsService = (*args->env)->GetObjectClass(args->env, args->instance); + ng_add_alloc(clsService, "clsService"); + + const char *signature = "(Ljava/lang/String;)Z"; + if (midIsDomainBlocked == NULL) + midIsDomainBlocked = jniGetMethodID(args->env, clsService, "isDomainBlocked", signature); + + jstring jname = (*args->env)->NewStringUTF(args->env, name); + ng_add_alloc(jname, "jname"); + + jboolean jallowed = (*args->env)->CallBooleanMethod( + args->env, args->instance, midIsDomainBlocked, jname); + jniCheckException(args->env); + + (*args->env)->DeleteLocalRef(args->env, jname); + (*args->env)->DeleteLocalRef(args->env, clsService); + ng_delete_alloc(jname, __FILE__, __LINE__); + ng_delete_alloc(clsService, __FILE__, __LINE__); + +#ifdef PROFILE_JNI + gettimeofday(&end, NULL); + mselapsed = (end.tv_sec - start.tv_sec) * 1000.0 + + (end.tv_usec - start.tv_usec) / 1000.0; + if (mselapsed > PROFILE_JNI) + log_android(ANDROID_LOG_WARN, "is_domain_blocked %f", mselapsed); +#endif + + return jallowed; +} + +static jmethodID midGetUidQ = NULL; + +jint get_uid_q(const struct arguments *args, + jint version, jint protocol, + const char *source, jint sport, + const char *dest, jint dport) { +#ifdef PROFILE_JNI + float mselapsed; + struct timeval start, end; + gettimeofday(&start, NULL); +#endif + + jclass clsService = (*args->env)->GetObjectClass(args->env, args->instance); + ng_add_alloc(clsService, "clsService"); + + const char *signature = "(IILjava/lang/String;ILjava/lang/String;I)I"; + if (midGetUidQ == NULL) + midGetUidQ = jniGetMethodID(args->env, clsService, "getUidQ", signature); + + jstring jsource = (*args->env)->NewStringUTF(args->env, source); + jstring jdest = (*args->env)->NewStringUTF(args->env, dest); + ng_add_alloc(jsource, "jsource"); + ng_add_alloc(jdest, "jdest"); + + jint juid = (*args->env)->CallIntMethod( + args->env, args->instance, midGetUidQ, + version, protocol, jsource, sport, jdest, dport); + jniCheckException(args->env); + + (*args->env)->DeleteLocalRef(args->env, jdest); + (*args->env)->DeleteLocalRef(args->env, jsource); + (*args->env)->DeleteLocalRef(args->env, clsService); + ng_delete_alloc(jdest, __FILE__, __LINE__); + ng_delete_alloc(jsource, __FILE__, __LINE__); + ng_delete_alloc(clsService, __FILE__, __LINE__); + +#ifdef PROFILE_JNI + gettimeofday(&end, NULL); + mselapsed = (end.tv_sec - start.tv_sec) * 1000.0 + + (end.tv_usec - start.tv_usec) / 1000.0; + if (mselapsed > PROFILE_JNI) + log_android(ANDROID_LOG_WARN, "get_uid_q %f", mselapsed); +#endif + + return juid; +} + +static jmethodID midIsAddressAllowed = NULL; +jfieldID fidRaddr = NULL; +jfieldID fidRport = NULL; +struct allowed allowed; + +struct allowed *is_address_allowed(const struct arguments *args, jobject jpacket) { +#ifdef PROFILE_JNI + float mselapsed; + struct timeval start, end; + gettimeofday(&start, NULL); +#endif + + jclass clsService = (*args->env)->GetObjectClass(args->env, args->instance); + ng_add_alloc(clsService, "clsService"); + + const char *signature = "(Leu/faircode/netguard/Packet;)Leu/faircode/netguard/Allowed;"; + if (midIsAddressAllowed == NULL) + midIsAddressAllowed = jniGetMethodID(args->env, clsService, "isAddressAllowed", signature); + + jobject jallowed = (*args->env)->CallObjectMethod( + args->env, args->instance, midIsAddressAllowed, jpacket); + ng_add_alloc(jallowed, "jallowed"); + jniCheckException(args->env); + + if (jallowed != NULL) { + if (fidRaddr == NULL) { + const char *string = "Ljava/lang/String;"; + fidRaddr = jniGetFieldID(args->env, clsAllowed, "raddr", string); + fidRport = jniGetFieldID(args->env, clsAllowed, "rport", "I"); + } + + jstring jraddr = (*args->env)->GetObjectField(args->env, jallowed, fidRaddr); + ng_add_alloc(jraddr, "jraddr"); + if (jraddr == NULL) + *allowed.raddr = 0; + else { + const char *raddr = (*args->env)->GetStringUTFChars(args->env, jraddr, NULL); + ng_add_alloc(raddr, "raddr"); + strcpy(allowed.raddr, raddr); + (*args->env)->ReleaseStringUTFChars(args->env, jraddr, raddr); + ng_delete_alloc(raddr, __FILE__, __LINE__); + } + allowed.rport = (uint16_t) (*args->env)->GetIntField(args->env, jallowed, fidRport); + + (*args->env)->DeleteLocalRef(args->env, jraddr); + ng_delete_alloc(jraddr, __FILE__, __LINE__); + } + + + (*args->env)->DeleteLocalRef(args->env, jpacket); + (*args->env)->DeleteLocalRef(args->env, clsService); + (*args->env)->DeleteLocalRef(args->env, jallowed); + ng_delete_alloc(jpacket, __FILE__, __LINE__); + ng_delete_alloc(clsService, __FILE__, __LINE__); + ng_delete_alloc(jallowed, __FILE__, __LINE__); + +#ifdef PROFILE_JNI + gettimeofday(&end, NULL); + mselapsed = (end.tv_sec - start.tv_sec) * 1000.0 + + (end.tv_usec - start.tv_usec) / 1000.0; + if (mselapsed > PROFILE_JNI) + log_android(ANDROID_LOG_WARN, "is_address_allowed %f", mselapsed); +#endif + + return (jallowed == NULL ? NULL : &allowed); +} + +jmethodID midInitPacket = NULL; + +jfieldID fidTime = NULL; +jfieldID fidVersion = NULL; +jfieldID fidProtocol = NULL; +jfieldID fidFlags = NULL; +jfieldID fidSaddr = NULL; +jfieldID fidSport = NULL; +jfieldID fidDaddr = NULL; +jfieldID fidDport = NULL; +jfieldID fidData = NULL; +jfieldID fidUid = NULL; +jfieldID fidAllowed = NULL; + +jobject create_packet(const struct arguments *args, + jint version, + jint protocol, + const char *flags, + const char *source, + jint sport, + const char *dest, + jint dport, + const char *data, + jint uid, + jboolean allowed) { + JNIEnv *env = args->env; + +#ifdef PROFILE_JNI + float mselapsed; + struct timeval start, end; + gettimeofday(&start, NULL); +#endif + + /* + jbyte b[] = {1,2,3}; + jbyteArray ret = env->NewByteArray(3); + env->SetByteArrayRegion (ret, 0, 3, b); + */ + + const char *packet = "eu/faircode/netguard/Packet"; + if (midInitPacket == NULL) + midInitPacket = jniGetMethodID(env, clsPacket, "", "()V"); + jobject jpacket = jniNewObject(env, clsPacket, midInitPacket, packet); + ng_add_alloc(jpacket, "jpacket"); + + if (fidTime == NULL) { + const char *string = "Ljava/lang/String;"; + fidTime = jniGetFieldID(env, clsPacket, "time", "J"); + fidVersion = jniGetFieldID(env, clsPacket, "version", "I"); + fidProtocol = jniGetFieldID(env, clsPacket, "protocol", "I"); + fidFlags = jniGetFieldID(env, clsPacket, "flags", string); + fidSaddr = jniGetFieldID(env, clsPacket, "saddr", string); + fidSport = jniGetFieldID(env, clsPacket, "sport", "I"); + fidDaddr = jniGetFieldID(env, clsPacket, "daddr", string); + fidDport = jniGetFieldID(env, clsPacket, "dport", "I"); + fidData = jniGetFieldID(env, clsPacket, "data", string); + fidUid = jniGetFieldID(env, clsPacket, "uid", "I"); + fidAllowed = jniGetFieldID(env, clsPacket, "allowed", "Z"); + } + + struct timeval tv; + gettimeofday(&tv, NULL); + jlong t = tv.tv_sec * 1000LL + tv.tv_usec / 1000; + jstring jflags = (*env)->NewStringUTF(env, flags); + jstring jsource = (*env)->NewStringUTF(env, source); + jstring jdest = (*env)->NewStringUTF(env, dest); + jstring jdata = (*env)->NewStringUTF(env, data); + ng_add_alloc(jflags, "jflags"); + ng_add_alloc(jsource, "jsource"); + ng_add_alloc(jdest, "jdest"); + ng_add_alloc(jdata, "jdata"); + + (*env)->SetLongField(env, jpacket, fidTime, t); + (*env)->SetIntField(env, jpacket, fidVersion, version); + (*env)->SetIntField(env, jpacket, fidProtocol, protocol); + (*env)->SetObjectField(env, jpacket, fidFlags, jflags); + (*env)->SetObjectField(env, jpacket, fidSaddr, jsource); + (*env)->SetIntField(env, jpacket, fidSport, sport); + (*env)->SetObjectField(env, jpacket, fidDaddr, jdest); + (*env)->SetIntField(env, jpacket, fidDport, dport); + (*env)->SetObjectField(env, jpacket, fidData, jdata); + (*env)->SetIntField(env, jpacket, fidUid, uid); + (*env)->SetBooleanField(env, jpacket, fidAllowed, allowed); + + (*env)->DeleteLocalRef(env, jdata); + (*env)->DeleteLocalRef(env, jdest); + (*env)->DeleteLocalRef(env, jsource); + (*env)->DeleteLocalRef(env, jflags); + ng_delete_alloc(jdata, __FILE__, __LINE__); + ng_delete_alloc(jdest, __FILE__, __LINE__); + ng_delete_alloc(jsource, __FILE__, __LINE__); + ng_delete_alloc(jflags, __FILE__, __LINE__); + // Caller needs to delete reference to packet + +#ifdef PROFILE_JNI + gettimeofday(&end, NULL); + mselapsed = (end.tv_sec - start.tv_sec) * 1000.0 + + (end.tv_usec - start.tv_usec) / 1000.0; + if (mselapsed > PROFILE_JNI) + log_android(ANDROID_LOG_WARN, "create_packet %f", mselapsed); +#endif + + return jpacket; +} + +jmethodID midAccountUsage = NULL; +jmethodID midInitUsage = NULL; +jfieldID fidUsageTime = NULL; +jfieldID fidUsageVersion = NULL; +jfieldID fidUsageProtocol = NULL; +jfieldID fidUsageDAddr = NULL; +jfieldID fidUsageDPort = NULL; +jfieldID fidUsageUid = NULL; +jfieldID fidUsageSent = NULL; +jfieldID fidUsageReceived = NULL; + +void account_usage(const struct arguments *args, jint version, jint protocol, + const char *daddr, jint dport, jint uid, jlong sent, jlong received) { +#ifdef PROFILE_JNI + float mselapsed; + struct timeval start, end; + gettimeofday(&start, NULL); +#endif + + jclass clsService = (*args->env)->GetObjectClass(args->env, args->instance); + ng_add_alloc(clsService, "clsService"); + + const char *signature = "(Leu/faircode/netguard/Usage;)V"; + if (midAccountUsage == NULL) + midAccountUsage = jniGetMethodID(args->env, clsService, "accountUsage", signature); + + const char *usage = "eu/faircode/netguard/Usage"; + if (midInitUsage == NULL) + midInitUsage = jniGetMethodID(args->env, clsUsage, "", "()V"); + + jobject jusage = jniNewObject(args->env, clsUsage, midInitUsage, usage); + ng_add_alloc(jusage, "jusage"); + + if (fidUsageTime == NULL) { + const char *string = "Ljava/lang/String;"; + fidUsageTime = jniGetFieldID(args->env, clsUsage, "Time", "J"); + fidUsageVersion = jniGetFieldID(args->env, clsUsage, "Version", "I"); + fidUsageProtocol = jniGetFieldID(args->env, clsUsage, "Protocol", "I"); + fidUsageDAddr = jniGetFieldID(args->env, clsUsage, "DAddr", string); + fidUsageDPort = jniGetFieldID(args->env, clsUsage, "DPort", "I"); + fidUsageUid = jniGetFieldID(args->env, clsUsage, "Uid", "I"); + fidUsageSent = jniGetFieldID(args->env, clsUsage, "Sent", "J"); + fidUsageReceived = jniGetFieldID(args->env, clsUsage, "Received", "J"); + } + + jlong jtime = time(NULL) * 1000LL; + jstring jdaddr = (*args->env)->NewStringUTF(args->env, daddr); + ng_add_alloc(jdaddr, "jdaddr"); + + (*args->env)->SetLongField(args->env, jusage, fidUsageTime, jtime); + (*args->env)->SetIntField(args->env, jusage, fidUsageVersion, version); + (*args->env)->SetIntField(args->env, jusage, fidUsageProtocol, protocol); + (*args->env)->SetObjectField(args->env, jusage, fidUsageDAddr, jdaddr); + (*args->env)->SetIntField(args->env, jusage, fidUsageDPort, dport); + (*args->env)->SetIntField(args->env, jusage, fidUsageUid, uid); + (*args->env)->SetLongField(args->env, jusage, fidUsageSent, sent); + (*args->env)->SetLongField(args->env, jusage, fidUsageReceived, received); + + (*args->env)->CallVoidMethod(args->env, args->instance, midAccountUsage, jusage); + jniCheckException(args->env); + + (*args->env)->DeleteLocalRef(args->env, jdaddr); + (*args->env)->DeleteLocalRef(args->env, jusage); + (*args->env)->DeleteLocalRef(args->env, clsService); + ng_delete_alloc(jdaddr, __FILE__, __LINE__); + ng_delete_alloc(jusage, __FILE__, __LINE__); + ng_delete_alloc(clsService, __FILE__, __LINE__); + +#ifdef PROFILE_JNI + gettimeofday(&end, NULL); + mselapsed = (end.tv_sec - start.tv_sec) * 1000.0 + + (end.tv_usec - start.tv_usec) / 1000.0; + if (mselapsed > PROFILE_JNI) + log_android(ANDROID_LOG_WARN, "log_packet %f", mselapsed); +#endif +} + +struct alloc_record { + const char *tag; + time_t time; + void *ptr; +}; + +int allocs = 0; +int balance = 0; +struct alloc_record *alloc = NULL; +pthread_mutex_t *alock = NULL; + +void ng_add_alloc(void *ptr, const char *tag) { +#ifdef PROFILE_MEMORY + if (ptr == NULL) + return; + + if (alock == NULL) { + alock = malloc(sizeof(pthread_mutex_t)); + if (pthread_mutex_init(alock, NULL)) + log_android(ANDROID_LOG_ERROR, "pthread_mutex_init failed"); + } + + if (pthread_mutex_lock(alock)) + log_android(ANDROID_LOG_ERROR, "pthread_mutex_lock failed"); + + int c = 0; + for (; c < allocs; c++) + if (alloc[c].ptr == NULL) + break; + + if (c >= allocs) { + if (allocs == 0) + alloc = malloc(sizeof(struct alloc_record)); + else + alloc = realloc(alloc, sizeof(struct alloc_record) * (allocs + 1)); + c = allocs; + allocs++; + } + + alloc[c].tag = tag; + alloc[c].time = time(NULL); + alloc[c].ptr = ptr; + balance++; + + if (pthread_mutex_unlock(alock)) + log_android(ANDROID_LOG_ERROR, "pthread_mutex_unlock failed"); +#endif +} + +void ng_delete_alloc(void *ptr, const char *file, int line) { +#ifdef PROFILE_MEMORY + if (ptr == NULL) + return; + + if (pthread_mutex_lock(alock)) + log_android(ANDROID_LOG_ERROR, "pthread_mutex_lock failed"); + + int found = 0; + for (int c = 0; c < allocs; c++) + if (alloc[c].ptr == ptr) { + found = 1; + alloc[c].tag = "[free]"; + alloc[c].ptr = NULL; + break; + } + + if (found == 1) + balance--; + + log_android(found ? ANDROID_LOG_DEBUG : ANDROID_LOG_ERROR, + "alloc/free balance %d records %d found %d", balance, allocs, found); + if (found == 0) + log_android(ANDROID_LOG_ERROR, "Not found at %s:%d", file, line); + + if (pthread_mutex_unlock(alock)) + log_android(ANDROID_LOG_ERROR, "pthread_mutex_unlock failed"); +#endif +} + +void *ng_malloc(size_t __byte_count, const char *tag) { + void *ptr = malloc(__byte_count); + ng_add_alloc(ptr, tag); + return ptr; +} + +void *ng_calloc(size_t __item_count, size_t __item_size, const char *tag) { + void *ptr = calloc(__item_count, __item_size); + ng_add_alloc(ptr, tag); + return ptr; +} + +void *ng_realloc(void *__ptr, size_t __byte_count, const char *tag) { + ng_delete_alloc(__ptr, NULL, 0); + void *ptr = realloc(__ptr, __byte_count); + ng_add_alloc(ptr, tag); + return ptr; +} + +void ng_free(void *__ptr, const char *file, int line) { + ng_delete_alloc(__ptr, file, line); + free(__ptr); +} + +void ng_dump() { + int r = 0; + for (int c = 0; c < allocs; c++) + if (alloc[c].ptr != NULL) + log_android(ANDROID_LOG_WARN, + "holding %d [%s] %s", + ++r, alloc[c].tag, ctime(&alloc[c].time)); +} + +JNIEXPORT void JNICALL +Java_eu_faircode_netguard_Util_dump_1memory_1profile(JNIEnv *env, jclass type) { +#ifdef PROFILE_MEMORY + log_android(ANDROID_LOG_DEBUG, "Dump memory profile"); + + if (pthread_mutex_lock(alock)) + log_android(ANDROID_LOG_ERROR, "pthread_mutex_lock failed"); + + ng_dump(); + + if (pthread_mutex_unlock(alock)) + log_android(ANDROID_LOG_ERROR, "pthread_mutex_unlock failed"); + +#endif +} \ No newline at end of file diff --git a/NetGuard/app/src/main/main/jni/netguard/netguard.h b/NetGuard/app/src/main/main/jni/netguard/netguard.h new file mode 100644 index 0000000..a490f56 --- /dev/null +++ b/NetGuard/app/src/main/main/jni/netguard/netguard.h @@ -0,0 +1,596 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define TAG "NetGuard.JNI" + +// #define PROFILE_JNI 5 +// #define PROFILE_MEMORY + +#define EPOLL_TIMEOUT 3600 // seconds +#define EPOLL_EVENTS 20 +#define EPOLL_MIN_CHECK 100 // milliseconds + +#define TUN_YIELD 10 // packets + +#define ICMP4_MAXMSG (IP_MAXPACKET - 20 - 8) // bytes (socket) +#define ICMP6_MAXMSG (IPV6_MAXPACKET - 40 - 8) // bytes (socket) +#define UDP4_MAXMSG (IP_MAXPACKET - 20 - 8) // bytes (socket) +#define UDP6_MAXMSG (IPV6_MAXPACKET - 40 - 8) // bytes (socket) + +#define ICMP_TIMEOUT 5 // seconds + +#define UDP_TIMEOUT_53 15 // seconds +#define UDP_TIMEOUT_ANY 300 // seconds +#define UDP_KEEP_TIMEOUT 60 // seconds +#define UDP_YIELD 10 // packets + +#define TCP_INIT_TIMEOUT 20 // seconds ~net.inet.tcp.keepinit +#define TCP_IDLE_TIMEOUT 3600 // seconds ~net.inet.tcp.keepidle +#define TCP_CLOSE_TIMEOUT 20 // seconds +#define TCP_KEEP_TIMEOUT 300 // seconds +// https://en.wikipedia.org/wiki/Maximum_segment_lifetime + +#define SESSION_LIMIT 40 // percent +#define SESSION_MAX (1024 * SESSION_LIMIT / 100) // number + +#define SEND_BUF_DEFAULT 163840 // bytes + +#define UID_MAX_AGE 30000 // milliseconds + +#define SOCKS5_NONE 1 +#define SOCKS5_HELLO 2 +#define SOCKS5_AUTH 3 +#define SOCKS5_CONNECT 4 +#define SOCKS5_CONNECTED 5 + +struct context { + pthread_mutex_t lock; + int pipefds[2]; + int stopping; + int sdk; + struct ng_session *ng_session; +}; + +struct arguments { + JNIEnv *env; + jobject instance; + int tun; + jboolean fwd53; + jint rcode; + struct context *ctx; +}; + +struct allowed { + char raddr[INET6_ADDRSTRLEN + 1]; + uint16_t rport; // host notation +}; + +struct segment { + uint32_t seq; + uint16_t len; + uint16_t sent; + int psh; + uint8_t *data; + struct segment *next; +}; + +struct icmp_session { + time_t time; + jint uid; + int version; + + union { + __be32 ip4; // network notation + struct in6_addr ip6; + } saddr; + + union { + __be32 ip4; // network notation + struct in6_addr ip6; + } daddr; + + uint16_t id; + + uint8_t stop; +}; + +#define UDP_ACTIVE 0 +#define UDP_FINISHING 1 +#define UDP_CLOSED 2 +#define UDP_BLOCKED 3 + +struct udp_session { + time_t time; + jint uid; + int version; + uint16_t mss; + + uint64_t sent; + uint64_t received; + + union { + __be32 ip4; // network notation + struct in6_addr ip6; + } saddr; + __be16 source; // network notation + + union { + __be32 ip4; // network notation + struct in6_addr ip6; + } daddr; + __be16 dest; // network notation + + uint8_t state; +}; + +struct tcp_session { + jint uid; + time_t time; + int version; + uint16_t mss; + uint8_t recv_scale; + uint8_t send_scale; + uint32_t recv_window; // host notation, scaled + uint32_t send_window; // host notation, scaled + uint16_t unconfirmed; // packets + + uint32_t remote_seq; // confirmed bytes received, host notation + uint32_t local_seq; // confirmed bytes sent, host notation + uint32_t remote_start; + uint32_t local_start; + + uint32_t acked; // host notation + long long last_keep_alive; + + uint64_t sent; + uint64_t received; + + union { + __be32 ip4; // network notation + struct in6_addr ip6; + } saddr; + __be16 source; // network notation + + union { + __be32 ip4; // network notation + struct in6_addr ip6; + } daddr; + __be16 dest; // network notation + + uint8_t state; + uint8_t socks5; + struct segment *forward; +}; + +struct ng_session { + uint8_t protocol; + union { + struct icmp_session icmp; + struct udp_session udp; + struct tcp_session tcp; + }; + jint socket; + struct epoll_event ev; + struct ng_session *next; +}; + +struct uid_cache_entry { + uint8_t version; + uint8_t protocol; + uint8_t saddr[16]; + uint16_t sport; + uint8_t daddr[16]; + uint16_t dport; + jint uid; + long time; +}; + +// IPv6 + +struct ip6_hdr_pseudo { + struct in6_addr ip6ph_src; + struct in6_addr ip6ph_dst; + u_int32_t ip6ph_len; + u_int8_t ip6ph_zero[3]; + u_int8_t ip6ph_nxt; +} __packed; + +// PCAP +// https://wiki.wireshark.org/Development/LibpcapFileFormat + +typedef uint16_t guint16_t; +typedef uint32_t guint32_t; +typedef int32_t gint32_t; + +typedef struct pcap_hdr_s { + guint32_t magic_number; + guint16_t version_major; + guint16_t version_minor; + gint32_t thiszone; + guint32_t sigfigs; + guint32_t snaplen; + guint32_t network; +} __packed pcap_hdr_s; + +typedef struct pcaprec_hdr_s { + guint32_t ts_sec; + guint32_t ts_usec; + guint32_t incl_len; + guint32_t orig_len; +} __packed pcaprec_hdr_s; + +#define LINKTYPE_RAW 101 + +// DNS + +#define DNS_QCLASS_IN 1 +#define DNS_QTYPE_A 1 // IPv4 +#define DNS_QTYPE_AAAA 28 // IPv6 + +#define DNS_SVCB 64 +#define DNS_HTTPS 65 + +#define DNS_QNAME_MAX 255 +#define DNS_TTL (10 * 60) // seconds + +struct dns_header { + uint16_t id; // identification number +# if __BYTE_ORDER == __LITTLE_ENDIAN + uint16_t rd :1; // recursion desired + uint16_t tc :1; // truncated message + uint16_t aa :1; // authoritive answer + uint16_t opcode :4; // purpose of message + uint16_t qr :1; // query/response flag + uint16_t rcode :4; // response code + uint16_t cd :1; // checking disabled + uint16_t ad :1; // authenticated data + uint16_t z :1; // its z! reserved + uint16_t ra :1; // recursion available +#elif __BYTE_ORDER == __BIG_ENDIAN + uint16_t qr :1; // query/response flag + uint16_t opcode :4; // purpose of message + uint16_t aa :1; // authoritive answer + uint16_t tc :1; // truncated message + uint16_t rd :1; // recursion desired + uint16_t ra :1; // recursion available + uint16_t z :1; // its z! reserved + uint16_t ad :1; // authenticated data + uint16_t cd :1; // checking disabled + uint16_t rcode :4; // response code +# else +# error "Adjust your defines" +#endif + uint16_t q_count; // number of question entries + uint16_t ans_count; // number of answer entries + uint16_t auth_count; // number of authority entries + uint16_t add_count; // number of resource entries +} __packed; + +typedef struct dns_rr { + __be16 qname_ptr; + __be16 qtype; + __be16 qclass; + __be32 ttl; + __be16 rdlength; +} __packed dns_rr; + +// DHCP + +#define DHCP_OPTION_MAGIC_NUMBER (0x63825363) + +typedef struct dhcp_packet { + uint8_t opcode; + uint8_t htype; + uint8_t hlen; + uint8_t hops; + uint32_t xid; + uint16_t secs; + uint16_t flags; + uint32_t ciaddr; + uint32_t yiaddr; + uint32_t siaddr; + uint32_t giaddr; + uint8_t chaddr[16]; + uint8_t sname[64]; + uint8_t file[128]; + uint32_t option_format; +} __packed dhcp_packet; + +typedef struct dhcp_option { + uint8_t code; + uint8_t length; +} __packed dhcp_option; + +// Prototypes + +void handle_signal(int sig, siginfo_t *info, void *context); + +void *handle_events(void *a); + +void report_exit(const struct arguments *args, const char *fmt, ...); + +void report_error(const struct arguments *args, jint error, const char *fmt, ...); + +void check_allowed(const struct arguments *args); + +void clear(struct context *ctx); + +int check_icmp_session(const struct arguments *args, + struct ng_session *s, + int sessions, int maxsessions); + +int check_udp_session(const struct arguments *args, + struct ng_session *s, + int sessions, int maxsessions); + +int check_tcp_session(const struct arguments *args, + struct ng_session *s, + int sessions, int maxsessions); + +int monitor_tcp_session(const struct arguments *args, struct ng_session *s, int epoll_fd); + +int get_icmp_timeout(const struct icmp_session *u, int sessions, int maxsessions); + +int get_udp_timeout(const struct udp_session *u, int sessions, int maxsessions); + +int get_tcp_timeout(const struct tcp_session *t, int sessions, int maxsessions); + +uint16_t get_mtu(); + +uint16_t get_default_mss(int version); + +int check_tun(const struct arguments *args, + const struct epoll_event *ev, + const int epoll_fd, + int sessions, int maxsessions); + +void check_icmp_socket(const struct arguments *args, const struct epoll_event *ev); + +void check_udp_socket(const struct arguments *args, const struct epoll_event *ev); + +int32_t get_qname(const uint8_t *data, const size_t datalen, uint16_t off, char *qname); + +void parse_dns_response(const struct arguments *args, const struct ng_session *session, + const uint8_t *data, size_t *datalen); + +uint32_t get_send_window(const struct tcp_session *cur); + +uint32_t get_receive_buffer(const struct ng_session *cur); + +uint32_t get_receive_window(const struct ng_session *cur); + +void check_tcp_socket(const struct arguments *args, + const struct epoll_event *ev, + const int epoll_fd); + +int is_lower_layer(int protocol); + +int is_upper_layer(int protocol); + +void handle_ip(const struct arguments *args, + const uint8_t *buffer, size_t length, + const int epoll_fd, + int sessions, int maxsessions); + +jboolean handle_icmp(const struct arguments *args, + const uint8_t *pkt, size_t length, + const uint8_t *payload, + int uid, + const int epoll_fd); + +int has_udp_session(const struct arguments *args, const uint8_t *pkt, const uint8_t *payload); + +void block_udp(const struct arguments *args, + const uint8_t *pkt, size_t length, + const uint8_t *payload, + int uid); + +jboolean handle_udp(const struct arguments *args, + const uint8_t *pkt, size_t length, + const uint8_t *payload, + int uid, struct allowed *redirect, + const int epoll_fd); + +int check_dhcp(const struct arguments *args, const struct udp_session *u, + const uint8_t *data, const size_t datalen); + +void clear_tcp_data(struct tcp_session *cur); + +jboolean handle_tcp(const struct arguments *args, + const uint8_t *pkt, size_t length, + const uint8_t *payload, + int uid, int allowed, struct allowed *redirect, + const int epoll_fd); + + + + + + + + + + + + + + + + +int debug_socket_init(const struct arguments *args, int epoll_fd); +void read_debug_socket(); +void write_debug_socket(const struct arguments *args, int epoll_fd, const uint8_t *buffer, size_t length); + +void add_debug_session(const struct arguments * args, int epoll_fd); + + + +struct ng_session *get_debug_session(const struct arguments *args); + +void queue_tcp(const struct arguments *args, + const struct tcphdr *tcphdr, + const char *session, struct tcp_session *cur, + const uint8_t *data, uint16_t datalen); + +int open_icmp_socket(const struct arguments *args, const struct icmp_session *cur); + +int open_udp_socket(const struct arguments *args, + const struct udp_session *cur, const struct allowed *redirect); + +int open_tcp_socket(const struct arguments *args, + const struct tcp_session *cur, const struct allowed *redirect); + +int32_t get_local_port(const int sock); + +int write_syn_ack(const struct arguments *args, struct tcp_session *cur); + +int write_ack(const struct arguments *args, struct tcp_session *cur); + +int write_data(const struct arguments *args, struct tcp_session *cur, + const uint8_t *buffer, size_t length); + +int write_fin_ack(const struct arguments *args, struct tcp_session *cur); + +void write_rst(const struct arguments *args, struct tcp_session *cur); + +void write_rst_ack(const struct arguments *args, struct tcp_session *cur); + +ssize_t write_icmp(const struct arguments *args, const struct icmp_session *cur, + uint8_t *data, size_t datalen); + +ssize_t write_udp(const struct arguments *args, const struct udp_session *cur, + uint8_t *data, size_t datalen); + +ssize_t write_tcp(const struct arguments *args, const struct tcp_session *cur, + const uint8_t *data, size_t datalen, + int syn, int ack, int fin, int rst); + +uint8_t char2nible(const char c); + +void hex2bytes(const char *hex, uint8_t *buffer); + +jint get_uid(const int version, const int protocol, + const void *saddr, const uint16_t sport, + const void *daddr, const uint16_t dport); + +jint get_uid_sub(const int version, const int protocol, + const void *saddr, const uint16_t sport, + const void *daddr, const uint16_t dport, + const char *source, const char *dest, + long now); + +int protect_socket(const struct arguments *args, int socket); + +uint16_t calc_checksum(uint16_t start, const uint8_t *buffer, size_t length); + +jobject jniGlobalRef(JNIEnv *env, jobject cls); + +jclass jniFindClass(JNIEnv *env, const char *name); + +jmethodID jniGetMethodID(JNIEnv *env, jclass cls, const char *name, const char *signature); + +jfieldID jniGetFieldID(JNIEnv *env, jclass cls, const char *name, const char *type); + +jobject jniNewObject(JNIEnv *env, jclass cls, jmethodID constructor, const char *name); + +int jniCheckException(JNIEnv *env); + +int sdk_int(JNIEnv *env); + +void log_android(int prio, const char *fmt, ...); + +void log_packet(const struct arguments *args, jobject jpacket); + +void dns_resolved(const struct arguments *args, + const char *qname, const char *aname, const char *resource, int ttl); + +jboolean is_domain_blocked(const struct arguments *args, const char *name); + +jint get_uid_q(const struct arguments *args, + jint version, + jint protocol, + const char *source, + jint sport, + const char *dest, + jint dport); + +struct allowed *is_address_allowed(const struct arguments *args, jobject objPacket); + +jobject create_packet(const struct arguments *args, + jint version, + jint protocol, + const char *flags, + const char *source, + jint sport, + const char *dest, + jint dport, + const char *data, + jint uid, + jboolean allowed); + +void account_usage(const struct arguments *args, jint version, jint protocol, + const char *daddr, jint dport, jint uid, jlong sent, jlong received); + +void write_pcap_hdr(); + +void write_pcap_rec(const uint8_t *buffer, size_t len); + +void write_pcap(const void *ptr, size_t len); + +int compare_u32(uint32_t seq1, uint32_t seq2); + +const char *strstate(const int state); + +char *hex(const u_int8_t *data, const size_t len); + +int is_readable(int fd); + +int is_writable(int fd); + +long long get_ms(); + +void ng_add_alloc(void *ptr, const char *tag); + +void ng_delete_alloc(void *ptr, const char *file, int line); + +void *ng_malloc(size_t __byte_count, const char *tag); + +void *ng_calloc(size_t __item_count, size_t __item_size, const char *tag); + +void *ng_realloc(void *__ptr, size_t __byte_count, const char *tag); + +void ng_free(void *__ptr, const char *file, int line); + +void ng_dump(); diff --git a/NetGuard/app/src/main/main/jni/netguard/pcap.c b/NetGuard/app/src/main/main/jni/netguard/pcap.c new file mode 100644 index 0000000..134c49d --- /dev/null +++ b/NetGuard/app/src/main/main/jni/netguard/pcap.c @@ -0,0 +1,78 @@ +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +#include "netguard.h" + +FILE *pcap_file = NULL; +size_t pcap_record_size = 64; +long pcap_file_size = 2 * 1024 * 1024; + +void write_pcap_hdr() { + struct pcap_hdr_s pcap_hdr; + pcap_hdr.magic_number = 0xa1b2c3d4; + pcap_hdr.version_major = 2; + pcap_hdr.version_minor = 4; + pcap_hdr.thiszone = 0; + pcap_hdr.sigfigs = 0; + pcap_hdr.snaplen = pcap_record_size; + pcap_hdr.network = LINKTYPE_RAW; + write_pcap(&pcap_hdr, sizeof(struct pcap_hdr_s)); +} + +void write_pcap_rec(const uint8_t *buffer, size_t length) { + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts)) + log_android(ANDROID_LOG_ERROR, "clock_gettime error %d: %s", errno, strerror(errno)); + + size_t plen = (length < pcap_record_size ? length : pcap_record_size); + size_t rlen = sizeof(struct pcaprec_hdr_s) + plen; + struct pcaprec_hdr_s *pcap_rec = ng_malloc(rlen, "pcap"); + + pcap_rec->ts_sec = (guint32_t) ts.tv_sec; + pcap_rec->ts_usec = (guint32_t) (ts.tv_nsec / 1000); + pcap_rec->incl_len = (guint32_t) plen; + pcap_rec->orig_len = (guint32_t) length; + + memcpy(((uint8_t *) pcap_rec) + sizeof(struct pcaprec_hdr_s), buffer, plen); + + write_pcap(pcap_rec, rlen); + + ng_free(pcap_rec, __FILE__, __LINE__); +} + +void write_pcap(const void *ptr, size_t len) { + if (fwrite(ptr, len, 1, pcap_file) < 1) + log_android(ANDROID_LOG_ERROR, "PCAP fwrite error %d: %s", errno, strerror(errno)); + else { + long fsize = ftell(pcap_file); + log_android(ANDROID_LOG_VERBOSE, "PCAP wrote %d @%ld", len, fsize); + + if (fsize > pcap_file_size) { + log_android(ANDROID_LOG_WARN, "PCAP truncate @%ld", fsize); + if (ftruncate(fileno(pcap_file), sizeof(struct pcap_hdr_s))) + log_android(ANDROID_LOG_ERROR, "PCAP ftruncate error %d: %s", + errno, strerror(errno)); + else { + if (!lseek(fileno(pcap_file), sizeof(struct pcap_hdr_s), SEEK_SET)) + log_android(ANDROID_LOG_ERROR, "PCAP ftruncate error %d: %s", + errno, strerror(errno)); + } + } + } +} diff --git a/NetGuard/app/src/main/main/jni/netguard/session.c b/NetGuard/app/src/main/main/jni/netguard/session.c new file mode 100644 index 0000000..424cbb0 --- /dev/null +++ b/NetGuard/app/src/main/main/jni/netguard/session.c @@ -0,0 +1,391 @@ +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +#include "netguard.h" + + +int added = 0; + +void clear(struct context *ctx) { + struct ng_session *s = ctx->ng_session; + while (s != NULL) { + if (s->socket >= 0 && close(s->socket)) + log_android(ANDROID_LOG_ERROR, "close %d error %d: %s", + s->socket, errno, strerror(errno)); + if (s->protocol == IPPROTO_TCP) + clear_tcp_data(&s->tcp); + struct ng_session *p = s; + s = s->next; + ng_free(p, __FILE__, __LINE__); + } + ctx->ng_session = NULL; +} + +void *handle_events(void *a) { + + + struct arguments *args = (struct arguments *) a; + log_android(ANDROID_LOG_ERROR, "Start events tun=%d", args->tun); + + // Get max number of sessions + int maxsessions = SESSION_MAX; + struct rlimit rlim; + if (getrlimit(RLIMIT_NOFILE, &rlim)) + log_android(ANDROID_LOG_WARN, "getrlimit error %d: %s", errno, strerror(errno)); + else { + maxsessions = (int) (rlim.rlim_cur * SESSION_LIMIT / 100); + if (maxsessions > SESSION_MAX) + maxsessions = SESSION_MAX; + log_android(ANDROID_LOG_WARN, "getrlimit soft %d hard %d max sessions %d", + rlim.rlim_cur, rlim.rlim_max, maxsessions); + } + + // Terminate existing sessions not allowed anymore + check_allowed(args); + + // Open epoll file + int epoll_fd = epoll_create(1); + if (epoll_fd < 0) { + log_android(ANDROID_LOG_ERROR, "epoll create error %d: %s", errno, strerror(errno)); + report_exit(args, "epoll create error %d: %s", errno, strerror(errno)); + args->ctx->stopping = 1; + } + + // Monitor stop events + struct epoll_event ev_pipe; + memset(&ev_pipe, 0, sizeof(struct epoll_event)); + ev_pipe.events = EPOLLIN | EPOLLERR; + ev_pipe.data.ptr = &ev_pipe; + if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, args->ctx->pipefds[0], &ev_pipe)) { + log_android(ANDROID_LOG_ERROR, "epoll add pipe error %d: %s", errno, strerror(errno)); + report_exit(args, "epoll add pipe error %d: %s", errno, strerror(errno)); + args->ctx->stopping = 1; + } + + // Monitor tun events + struct epoll_event ev_tun; + memset(&ev_tun, 0, sizeof(struct epoll_event)); + ev_tun.events = EPOLLIN | EPOLLERR; + ev_tun.data.ptr = NULL; + + if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, args->tun, &ev_tun)) { + log_android(ANDROID_LOG_ERROR, "epoll add tun error %d: %s", errno, strerror(errno)); + report_exit(args, "epoll add tun error %d: %s", errno, strerror(errno)); + args->ctx->stopping = 1; + } + + + + // Loop + long long last_check = 0; + while (!args->ctx->stopping) { + log_android(ANDROID_LOG_DEBUG, "Loop"); + + int recheck = 0; + int timeout = EPOLL_TIMEOUT; + + // Count sessions + int isessions = 0; + int usessions = 0; + int tsessions = 0; + struct ng_session *s = args->ctx->ng_session; + while (s != NULL) { + if (s->protocol == IPPROTO_ICMP || s->protocol == IPPROTO_ICMPV6) { + if (!s->icmp.stop) + isessions++; + } else if (s->protocol == IPPROTO_UDP) { + if (s->udp.state == UDP_ACTIVE) + usessions++; + } else if (s->protocol == IPPROTO_TCP) { + if (s->tcp.state != TCP_CLOSING && s->tcp.state != TCP_CLOSE) + tsessions++; + if (s->socket >= 0) + recheck = recheck | monitor_tcp_session(args, s, epoll_fd); + } + s = s->next; + } + + + + + + + int sessions = isessions + usessions + tsessions; + + // Check sessions + long long ms = get_ms(); + if (ms - last_check > EPOLL_MIN_CHECK) { + last_check = ms; + + time_t now = time(NULL); + struct ng_session *sl = NULL; + s = args->ctx->ng_session; + while (s != NULL) { + int del = 0; + if (s->protocol == IPPROTO_ICMP || s->protocol == IPPROTO_ICMPV6) { + del = check_icmp_session(args, s, sessions, maxsessions); + if (!s->icmp.stop && !del) { + int stimeout = s->icmp.time + + get_icmp_timeout(&s->icmp, sessions, maxsessions) - now + 1; + if (stimeout > 0 && stimeout < timeout) + timeout = stimeout; + } + } else if (s->protocol == IPPROTO_UDP) { + del = check_udp_session(args, s, sessions, maxsessions); + if (s->udp.state == UDP_ACTIVE && !del) { + int stimeout = s->udp.time + + get_udp_timeout(&s->udp, sessions, maxsessions) - now + 1; + if (stimeout > 0 && stimeout < timeout) + timeout = stimeout; + } + } else if (s->protocol == IPPROTO_TCP) { + del = check_tcp_session(args, s, sessions, maxsessions); + if (s->tcp.state != TCP_CLOSING && s->tcp.state != TCP_CLOSE && !del) { + int stimeout = s->tcp.time + + get_tcp_timeout(&s->tcp, sessions, maxsessions) - now + 1; + if (stimeout > 0 && stimeout < timeout) + timeout = stimeout; + } + } + + if (del) { + if (sl == NULL) + args->ctx->ng_session = s->next; + else + sl->next = s->next; + + struct ng_session *c = s; + s = s->next; + if (c->protocol == IPPROTO_TCP) + clear_tcp_data(&c->tcp); + ng_free(c, __FILE__, __LINE__); + } else { + sl = s; + s = s->next; + } + } + } else { + recheck = 1; + log_android(ANDROID_LOG_DEBUG, "Skipped session checks"); + } + + log_android(ANDROID_LOG_DEBUG, + "sessions ICMP %d UDP %d TCP %d max %d/%d timeout %d recheck %d", + isessions, usessions, tsessions, sessions, maxsessions, timeout, recheck); + + + + // Poll + struct epoll_event ev[EPOLL_EVENTS]; + int ready = epoll_wait(epoll_fd, ev, EPOLL_EVENTS, + recheck ? EPOLL_MIN_CHECK : timeout * 1000); + + if (ready < 0) { + if (errno == EINTR) { + log_android(ANDROID_LOG_DEBUG, "epoll interrupted tun %d", args->tun); + continue; + } else { + log_android(ANDROID_LOG_ERROR, + "epoll tun %d error %d: %s", + args->tun, errno, strerror(errno)); + report_exit(args, "epoll tun %d error %d: %s", + args->tun, errno, strerror(errno)); + break; + } + } + + if (ready == 0) + log_android(ANDROID_LOG_DEBUG, "epoll timeout"); + else { + + if (pthread_mutex_lock(&args->ctx->lock)) + log_android(ANDROID_LOG_ERROR, "pthread_mutex_lock failed"); + + int error = 0; + + for (int i = 0; i < ready; i++) { + + log_android(ANDROID_LOG_ERROR, "looping over ready events: %d of %d, event ptr: %x", i, ready, ev[i].data.ptr); + + + if (ev[i].data.ptr == &ev_pipe) { + // Check pipe + uint8_t buffer[1]; + if (read(args->ctx->pipefds[0], buffer, 1) < 0) + log_android(ANDROID_LOG_ERROR, "Read pipe error %d: %s", + errno, strerror(errno)); + else + log_android(ANDROID_LOG_WARN, "Read pipe"); + + } else if (ev[i].data.ptr == NULL) { + // Check upstream + log_android(ANDROID_LOG_ERROR, "epoll upstream ready %d/%d in %d out %d err %d hup %d", + i, ready, + (ev[i].events & EPOLLIN) != 0, + (ev[i].events & EPOLLOUT) != 0, + (ev[i].events & EPOLLERR) != 0, + (ev[i].events & EPOLLHUP) != 0); + + int count = 0; + while (count < TUN_YIELD && !error && !args->ctx->stopping && + is_readable(args->tun)) { + count++; + if (check_tun(args, &ev[i], epoll_fd, sessions, maxsessions) < 0) + error = 1; + } + + } else { + // Check downstream + log_android(ANDROID_LOG_ERROR, + "epoll downstream ready %d/%d in %d out %d err %d hup %d prot %d sock %d", + i, ready, + (ev[i].events & EPOLLIN) != 0, + (ev[i].events & EPOLLOUT) != 0, + (ev[i].events & EPOLLERR) != 0, + (ev[i].events & EPOLLHUP) != 0, + ((struct ng_session *) ev[i].data.ptr)->protocol, + ((struct ng_session *) ev[i].data.ptr)->socket); + + struct ng_session *session = (struct ng_session *) ev[i].data.ptr; + if (session->protocol == IPPROTO_ICMP || + session->protocol == IPPROTO_ICMPV6) + check_icmp_socket(args, &ev[i]); + else if (session->protocol == IPPROTO_UDP) { + int count = 0; + while (count < UDP_YIELD && !args->ctx->stopping && + !(ev[i].events & EPOLLERR) && (ev[i].events & EPOLLIN) && + is_readable(session->socket)) { + count++; + check_udp_socket(args, &ev[i]); + } + } else if (session->protocol == IPPROTO_TCP) + check_tcp_socket(args, &ev[i], epoll_fd); + } + + if (error) + break; + } + + if (pthread_mutex_unlock(&args->ctx->lock)) + log_android(ANDROID_LOG_ERROR, "pthread_mutex_unlock failed"); + + if (error) + break; + } + } + + // Close epoll file + if (epoll_fd >= 0 && close(epoll_fd)) + log_android(ANDROID_LOG_ERROR, + "epoll close error %d: %s", errno, strerror(errno)); + + // Cleanup + ng_free(args, __FILE__, __LINE__); + + log_android(ANDROID_LOG_WARN, "Stopped events tun=%d", args->tun); + return NULL; +} + + +void check_allowed(const struct arguments *args) { + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + + struct ng_session *l = NULL; + struct ng_session *s = args->ctx->ng_session; + while (s != NULL) { + if (s->protocol == IPPROTO_ICMP || s->protocol == IPPROTO_ICMPV6) { + if (!s->icmp.stop) { + if (s->icmp.version == 4) { + inet_ntop(AF_INET, &s->icmp.saddr.ip4, source, sizeof(source)); + inet_ntop(AF_INET, &s->icmp.daddr.ip4, dest, sizeof(dest)); + } else { + inet_ntop(AF_INET6, &s->icmp.saddr.ip6, source, sizeof(source)); + inet_ntop(AF_INET6, &s->icmp.daddr.ip6, dest, sizeof(dest)); + } + + jobject objPacket = create_packet( + args, s->icmp.version, IPPROTO_ICMP, "", + source, 0, dest, 0, "", s->icmp.uid, 0); + if (is_address_allowed(args, objPacket) == NULL) { + s->icmp.stop = 1; + log_android(ANDROID_LOG_WARN, "ICMP terminate %d uid %d", + s->socket, s->icmp.uid); + } + } + + } else if (s->protocol == IPPROTO_UDP) { + if (s->udp.state == UDP_ACTIVE) { + if (s->udp.version == 4) { + inet_ntop(AF_INET, &s->udp.saddr.ip4, source, sizeof(source)); + inet_ntop(AF_INET, &s->udp.daddr.ip4, dest, sizeof(dest)); + } else { + inet_ntop(AF_INET6, &s->udp.saddr.ip6, source, sizeof(source)); + inet_ntop(AF_INET6, &s->udp.daddr.ip6, dest, sizeof(dest)); + } + + jobject objPacket = create_packet( + args, s->udp.version, IPPROTO_UDP, "", + source, ntohs(s->udp.source), dest, ntohs(s->udp.dest), "", s->udp.uid, 0); + if (is_address_allowed(args, objPacket) == NULL) { + s->udp.state = UDP_FINISHING; + log_android(ANDROID_LOG_WARN, "UDP terminate session socket %d uid %d", + s->socket, s->udp.uid); + } + } else if (s->udp.state == UDP_BLOCKED) { + log_android(ANDROID_LOG_WARN, "UDP remove blocked session uid %d", s->udp.uid); + + if (l == NULL) + args->ctx->ng_session = s->next; + else + l->next = s->next; + + struct ng_session *c = s; + s = s->next; + ng_free(c, __FILE__, __LINE__); + continue; + } + + } else if (s->protocol == IPPROTO_TCP) { + if (s->tcp.state != TCP_CLOSING && s->tcp.state != TCP_CLOSE) { + if (s->tcp.version == 4) { + inet_ntop(AF_INET, &s->tcp.saddr.ip4, source, sizeof(source)); + inet_ntop(AF_INET, &s->tcp.daddr.ip4, dest, sizeof(dest)); + } else { + inet_ntop(AF_INET6, &s->tcp.saddr.ip6, source, sizeof(source)); + inet_ntop(AF_INET6, &s->tcp.daddr.ip6, dest, sizeof(dest)); + } + + jobject objPacket = create_packet( + args, s->tcp.version, IPPROTO_TCP, "", + source, ntohs(s->tcp.source), dest, ntohs(s->tcp.dest), "", s->tcp.uid, 0); + if (is_address_allowed(args, objPacket) == NULL) { + write_rst(args, &s->tcp); + log_android(ANDROID_LOG_WARN, "TCP terminate socket %d uid %d", + s->socket, s->tcp.uid); + } + } + + } + + l = s; + s = s->next; + } +} + diff --git a/NetGuard/app/src/main/main/jni/netguard/tcp.c b/NetGuard/app/src/main/main/jni/netguard/tcp.c new file mode 100644 index 0000000..3a05557 --- /dev/null +++ b/NetGuard/app/src/main/main/jni/netguard/tcp.c @@ -0,0 +1,1379 @@ +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +#include "netguard.h" + +extern char socks5_addr[INET6_ADDRSTRLEN + 1]; +extern int socks5_port; +extern char socks5_username[127 + 1]; +extern char socks5_password[127 + 1]; + +extern FILE *pcap_file; + + + +void clear_tcp_data(struct tcp_session *cur) { + struct segment *s = cur->forward; + while (s != NULL) { + struct segment *p = s; + s = s->next; + ng_free(p->data, __FILE__, __LINE__); + ng_free(p, __FILE__, __LINE__); + } +} + +int get_tcp_timeout(const struct tcp_session *t, int sessions, int maxsessions) { + int timeout; + if (t->state == TCP_LISTEN || t->state == TCP_SYN_RECV) + timeout = TCP_INIT_TIMEOUT; + else if (t->state == TCP_ESTABLISHED) + timeout = TCP_IDLE_TIMEOUT; + else + timeout = TCP_CLOSE_TIMEOUT; + + int scale = 100 - sessions * 100 / maxsessions; + timeout = timeout * scale / 100; + + return timeout; +} + +int check_tcp_session(const struct arguments *args, struct ng_session *s, + int sessions, int maxsessions) { + time_t now = time(NULL); + + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + if (s->tcp.version == 4) { + inet_ntop(AF_INET, &s->tcp.saddr.ip4, source, sizeof(source)); + inet_ntop(AF_INET, &s->tcp.daddr.ip4, dest, sizeof(dest)); + } else { + inet_ntop(AF_INET6, &s->tcp.saddr.ip6, source, sizeof(source)); + inet_ntop(AF_INET6, &s->tcp.daddr.ip6, dest, sizeof(dest)); + } + + + char session[250]; + sprintf(session, "TCP socket from %s/%u to %s/%u %s socket %d", + source, ntohs(s->tcp.source), dest, ntohs(s->tcp.dest), + strstate(s->tcp.state), s->socket); + + int timeout = get_tcp_timeout(&s->tcp, sessions, maxsessions); + + // Check session timeout + if (s->tcp.state != TCP_CLOSING && s->tcp.state != TCP_CLOSE && + s->tcp.time + timeout < now) { + log_android(ANDROID_LOG_WARN, "%s idle %d/%d sec ", session, now - s->tcp.time, + timeout); + if (s->tcp.state == TCP_LISTEN) + s->tcp.state = TCP_CLOSING; + else + write_rst(args, &s->tcp); + } + + // Check closing sessions + if (s->tcp.state == TCP_CLOSING) { + // eof closes socket + if (s->socket >= 0) { + if (close(s->socket)) + log_android(ANDROID_LOG_ERROR, "%s close error %d: %s", + session, errno, strerror(errno)); + else + log_android(ANDROID_LOG_WARN, "%s close", session); + s->socket = -1; + } + + s->tcp.time = time(NULL); + s->tcp.state = TCP_CLOSE; + } + + if ((s->tcp.state == TCP_CLOSING || s->tcp.state == TCP_CLOSE) && + (s->tcp.sent || s->tcp.received)) { + account_usage(args, s->tcp.version, IPPROTO_TCP, + dest, ntohs(s->tcp.dest), s->tcp.uid, s->tcp.sent, s->tcp.received); + s->tcp.sent = 0; + s->tcp.received = 0; + } + + // Cleanup lingering sessions + if (s->tcp.state == TCP_CLOSE && s->tcp.time + TCP_KEEP_TIMEOUT < now) + return 1; + + return 0; +} + +int monitor_tcp_session(const struct arguments *args, struct ng_session *s, int epoll_fd) { + int recheck = 0; + unsigned int events = EPOLLERR; + + if (s->tcp.state == TCP_LISTEN) { + // Check for connected = writable + if (s->tcp.socks5 == SOCKS5_NONE) + events = events | EPOLLOUT; + else + events = events | EPOLLIN; + } else if (s->tcp.state == TCP_ESTABLISHED || s->tcp.state == TCP_CLOSE_WAIT) { + + // Check for incoming data + if (get_send_window(&s->tcp) > 0) + events = events | EPOLLIN; + else { + recheck = 1; + + long long ms = get_ms(); + if (ms - s->tcp.last_keep_alive > EPOLL_MIN_CHECK) { + s->tcp.last_keep_alive = ms; + log_android(ANDROID_LOG_WARN, "Sending keep alive to update send window"); + s->tcp.remote_seq--; + write_ack(args, &s->tcp); + s->tcp.remote_seq++; + } + } + + // Check for outgoing data + if (s->tcp.forward != NULL) { + uint32_t buffer_size = get_receive_buffer(s); + if (s->tcp.forward->seq == s->tcp.remote_seq && + s->tcp.forward->len - s->tcp.forward->sent < buffer_size) + events = events | EPOLLOUT; + else + recheck = 1; + } + } + + if (events != s->ev.events) { + s->ev.events = events; + if (epoll_ctl(epoll_fd, EPOLL_CTL_MOD, s->socket, &s->ev)) { + s->tcp.state = TCP_CLOSING; + log_android(ANDROID_LOG_ERROR, "epoll mod tcp error %d: %s", errno, strerror(errno)); + } else + log_android(ANDROID_LOG_DEBUG, "epoll mod tcp socket %d in %d out %d", + s->socket, (events & EPOLLIN) != 0, (events & EPOLLOUT) != 0); + } + + return recheck; +} + +uint32_t get_send_window(const struct tcp_session *cur) { + uint32_t behind; + if (cur->acked <= cur->local_seq) + behind = (cur->local_seq - cur->acked); + else + behind = (0x10000 + cur->local_seq - cur->acked); + behind += (cur->unconfirmed + 1) * 40; // Maximum header size + + uint32_t total = (behind < cur->send_window ? cur->send_window - behind : 0); + + log_android(ANDROID_LOG_DEBUG, "Send window behind %u window %u total %u", + behind, cur->send_window, total); + + return total; +} + +uint32_t get_receive_buffer(const struct ng_session *cur) { + if (cur->socket < 0) + return 0; + + // Get send buffer size + // /proc/sys/net/core/wmem_default + int sendbuf = 0; + int sendbufsize = sizeof(sendbuf); + if (getsockopt(cur->socket, SOL_SOCKET, SO_SNDBUF, &sendbuf, (socklen_t *) &sendbufsize) < 0) + log_android(ANDROID_LOG_WARN, "getsockopt SO_RCVBUF %d: %s", errno, strerror(errno)); + + if (sendbuf == 0) + sendbuf = SEND_BUF_DEFAULT; + + // Get unsent data size + int unsent = 0; + if (ioctl(cur->socket, SIOCOUTQ, &unsent)) + log_android(ANDROID_LOG_WARN, "ioctl SIOCOUTQ %d: %s", errno, strerror(errno)); + + uint32_t total = (uint32_t) (unsent < sendbuf ? sendbuf - unsent : 0); + + log_android(ANDROID_LOG_DEBUG, "Send buffer %u unsent %u total %u", + sendbuf, unsent, total); + + return total; +} + +uint32_t get_receive_window(const struct ng_session *cur) { + // Get data to forward size + uint32_t toforward = 0; + struct segment *q = cur->tcp.forward; + while (q != NULL) { + toforward += (q->len - q->sent); + q = q->next; + } + + uint32_t window = get_receive_buffer(cur); + + uint32_t max = ((uint32_t) 0xFFFF) << cur->tcp.recv_scale; + if (window > max) { + log_android(ANDROID_LOG_DEBUG, "Receive window %u > max %u", window, max); + window = max; + } + + uint32_t total = (toforward < window ? window - toforward : 0); + + log_android(ANDROID_LOG_DEBUG, "Receive window toforward %u window %u total %u", + toforward, window, total); + + return total; +} + +void check_tcp_socket(const struct arguments *args, + const struct epoll_event *ev, + const int epoll_fd) { + struct ng_session *s = (struct ng_session *) ev->data.ptr; + + int oldstate = s->tcp.state; + uint32_t oldlocal = s->tcp.local_seq; + uint32_t oldremote = s->tcp.remote_seq; + + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + if (s->tcp.version == 4) { + inet_ntop(AF_INET, &s->tcp.saddr.ip4, source, sizeof(source)); + inet_ntop(AF_INET, &s->tcp.daddr.ip4, dest, sizeof(dest)); + } else { + inet_ntop(AF_INET6, &s->tcp.saddr.ip6, source, sizeof(source)); + inet_ntop(AF_INET6, &s->tcp.daddr.ip6, dest, sizeof(dest)); + } + char session[250]; + sprintf(session, "TCP socket from %s/%u to %s/%u %s loc %u rem %u", + source, ntohs(s->tcp.source), dest, ntohs(s->tcp.dest), + strstate(s->tcp.state), + s->tcp.local_seq - s->tcp.local_start, + s->tcp.remote_seq - s->tcp.remote_start); + + // Check socket error + if (ev->events & EPOLLERR) { + s->tcp.time = time(NULL); + + int serr = 0; + socklen_t optlen = sizeof(int); + int err = getsockopt(s->socket, SOL_SOCKET, SO_ERROR, &serr, &optlen); + if (err < 0) + log_android(ANDROID_LOG_ERROR, "%s getsockopt error %d: %s", + session, errno, strerror(errno)); + else if (serr) + log_android(ANDROID_LOG_ERROR, "%s SO_ERROR %d: %s", + session, serr, strerror(serr)); + + write_rst(args, &s->tcp); + + // Connection refused + if (0) + if (err >= 0 && (serr == ECONNREFUSED || serr == EHOSTUNREACH)) { + struct icmp icmp; + memset(&icmp, 0, sizeof(struct icmp)); + icmp.icmp_type = ICMP_UNREACH; + if (serr == ECONNREFUSED) + icmp.icmp_code = ICMP_UNREACH_PORT; + else + icmp.icmp_code = ICMP_UNREACH_HOST; + icmp.icmp_cksum = 0; + icmp.icmp_cksum = ~calc_checksum(0, (const uint8_t *) &icmp, 4); + + struct icmp_session sicmp; + memset(&sicmp, 0, sizeof(struct icmp_session)); + sicmp.version = s->tcp.version; + if (s->tcp.version == 4) { + sicmp.saddr.ip4 = (__be32) s->tcp.saddr.ip4; + sicmp.daddr.ip4 = (__be32) s->tcp.daddr.ip4; + } else { + memcpy(&sicmp.saddr.ip6, &s->tcp.saddr.ip6, 16); + memcpy(&sicmp.daddr.ip6, &s->tcp.daddr.ip6, 16); + } + + write_icmp(args, &sicmp, (uint8_t *) &icmp, 8); + } + } else { + // Assume socket okay + if (s->tcp.state == TCP_LISTEN) { + // Check socket connect + if (s->tcp.socks5 == SOCKS5_NONE) { + if (ev->events & EPOLLOUT) { + log_android(ANDROID_LOG_INFO, "%s connected", session); + + // https://tools.ietf.org/html/rfc1928 + // https://tools.ietf.org/html/rfc1929 + // https://en.wikipedia.org/wiki/SOCKS#SOCKS5 + if (*socks5_addr && socks5_port) + s->tcp.socks5 = SOCKS5_HELLO; + else + s->tcp.socks5 = SOCKS5_CONNECTED; + } + } else { + if (ev->events & EPOLLIN) { + uint8_t buffer[32]; + ssize_t bytes = recv(s->socket, buffer, sizeof(buffer), 0); + if (bytes < 0) { + log_android(ANDROID_LOG_ERROR, "%s recv SOCKS5 error %d: %s", + session, errno, strerror(errno)); + write_rst(args, &s->tcp); + } else { + char *h = hex(buffer, (const size_t) bytes); + log_android(ANDROID_LOG_INFO, "%s recv SOCKS5 %s", session, h); + ng_free(h, __FILE__, __LINE__); + + if (s->tcp.socks5 == SOCKS5_HELLO && + bytes == 2 && buffer[0] == 5) { + if (buffer[1] == 0) + s->tcp.socks5 = SOCKS5_CONNECT; + else if (buffer[1] == 2) + s->tcp.socks5 = SOCKS5_AUTH; + else { + s->tcp.socks5 = 0; + log_android(ANDROID_LOG_ERROR, "%s SOCKS5 auth %d not supported", + session, buffer[1]); + write_rst(args, &s->tcp); + } + + } else if (s->tcp.socks5 == SOCKS5_AUTH && + bytes == 2 && + (buffer[0] == 1 || buffer[0] == 5)) { + if (buffer[1] == 0) { + s->tcp.socks5 = SOCKS5_CONNECT; + log_android(ANDROID_LOG_WARN, "%s SOCKS5 auth OK", session); + } else { + s->tcp.socks5 = 0; + log_android(ANDROID_LOG_ERROR, "%s SOCKS5 auth error %d", + session, buffer[1]); + write_rst(args, &s->tcp); + } + + } else if (s->tcp.socks5 == SOCKS5_CONNECT && + bytes == 6 + (s->tcp.version == 4 ? 4 : 16) && + buffer[0] == 5) { + if (buffer[1] == 0) { + s->tcp.socks5 = SOCKS5_CONNECTED; + log_android(ANDROID_LOG_WARN, "%s SOCKS5 connected", session); + } else { + s->tcp.socks5 = 0; + log_android(ANDROID_LOG_ERROR, "%s SOCKS5 connect error %d", + session, buffer[1]); + write_rst(args, &s->tcp); + /* + 0x00 = request granted + 0x01 = general failure + 0x02 = connection not allowed by ruleset + 0x03 = network unreachable + 0x04 = host unreachable + 0x05 = connection refused by destination host + 0x06 = TTL expired + 0x07 = command not supported / protocol error + 0x08 = address type not supported + */ + } + + } else { + s->tcp.socks5 = 0; + log_android(ANDROID_LOG_ERROR, "%s recv SOCKS5 state %d", + session, s->tcp.socks5); + write_rst(args, &s->tcp); + } + } + } + } + + if (s->tcp.socks5 == SOCKS5_HELLO) { + uint8_t buffer[4] = {5, 2, 0, 2}; + char *h = hex(buffer, sizeof(buffer)); + log_android(ANDROID_LOG_INFO, "%s sending SOCKS5 hello: %s", + session, h); + ng_free(h, __FILE__, __LINE__); + ssize_t sent = send(s->socket, buffer, sizeof(buffer), MSG_NOSIGNAL); + if (sent < 0) { + log_android(ANDROID_LOG_ERROR, "%s send SOCKS5 hello error %d: %s", + session, errno, strerror(errno)); + write_rst(args, &s->tcp); + } + + } else if (s->tcp.socks5 == SOCKS5_AUTH) { + uint8_t ulen = strlen(socks5_username); + uint8_t plen = strlen(socks5_password); + uint8_t buffer[512]; + *(buffer + 0) = 1; // Version + *(buffer + 1) = ulen; + memcpy(buffer + 2, socks5_username, ulen); + *(buffer + 2 + ulen) = plen; + memcpy(buffer + 2 + ulen + 1, socks5_password, plen); + + size_t len = 2 + ulen + 1 + plen; + + char *h = hex(buffer, len); + log_android(ANDROID_LOG_INFO, "%s sending SOCKS5 auth: %s", + session, h); + ng_free(h, __FILE__, __LINE__); + ssize_t sent = send(s->socket, buffer, len, MSG_NOSIGNAL); + if (sent < 0) { + log_android(ANDROID_LOG_ERROR, + "%s send SOCKS5 connect error %d: %s", + session, errno, strerror(errno)); + write_rst(args, &s->tcp); + } + + } else if (s->tcp.socks5 == SOCKS5_CONNECT) { + uint8_t buffer[22]; + *(buffer + 0) = 5; // version + *(buffer + 1) = 1; // TCP/IP stream connection + *(buffer + 2) = 0; // reserved + *(buffer + 3) = (uint8_t) (s->tcp.version == 4 ? 1 : 4); + if (s->tcp.version == 4) { + memcpy(buffer + 4, &s->tcp.daddr.ip4, 4); + *((__be16 *) (buffer + 4 + 4)) = s->tcp.dest; + } else { + memcpy(buffer + 4, &s->tcp.daddr.ip6, 16); + *((__be16 *) (buffer + 4 + 16)) = s->tcp.dest; + } + + size_t len = (s->tcp.version == 4 ? 10 : 22); + + char *h = hex(buffer, len); + log_android(ANDROID_LOG_INFO, "%s sending SOCKS5 connect: %s", + session, h); + ng_free(h, __FILE__, __LINE__); + ssize_t sent = send(s->socket, buffer, len, MSG_NOSIGNAL); + if (sent < 0) { + log_android(ANDROID_LOG_ERROR, + "%s send SOCKS5 connect error %d: %s", + session, errno, strerror(errno)); + write_rst(args, &s->tcp); + } + + } else if (s->tcp.socks5 == SOCKS5_CONNECTED) { + 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; + } + } + } else { + + // Always forward data + int fwd = 0; + if (ev->events & EPOLLOUT) { + // Forward data + uint32_t buffer_size = get_receive_buffer(s); + while (s->tcp.forward != NULL && + s->tcp.forward->seq == s->tcp.remote_seq && + s->tcp.forward->len - s->tcp.forward->sent < buffer_size) { + log_android(ANDROID_LOG_DEBUG, "%s fwd %u...%u sent %u", + session, + s->tcp.forward->seq - s->tcp.remote_start, + s->tcp.forward->seq + s->tcp.forward->len - s->tcp.remote_start, + s->tcp.forward->sent); + + ssize_t sent = send(s->socket, + s->tcp.forward->data + s->tcp.forward->sent, + s->tcp.forward->len - s->tcp.forward->sent, + (unsigned int) (MSG_NOSIGNAL | (s->tcp.forward->psh + ? 0 + : MSG_MORE))); + if (sent < 0) { + log_android(ANDROID_LOG_ERROR, "%s send error %d: %s", + session, errno, strerror(errno)); + if (errno == EINTR || errno == EAGAIN) { + // Retry later + break; + } else { + write_rst(args, &s->tcp); + break; + } + } else { + fwd = 1; + buffer_size -= sent; + s->tcp.sent += sent; + s->tcp.forward->sent += sent; + + if (s->tcp.forward->len == s->tcp.forward->sent) { + s->tcp.remote_seq = s->tcp.forward->seq + s->tcp.forward->sent; + + struct segment *p = s->tcp.forward; + s->tcp.forward = s->tcp.forward->next; + ng_free(p->data, __FILE__, __LINE__); + ng_free(p, __FILE__, __LINE__); + } else { + log_android(ANDROID_LOG_WARN, + "%s partial send %u/%u", + session, s->tcp.forward->sent, s->tcp.forward->len); + break; + } + } + } + + // Log data buffered + struct segment *seg = s->tcp.forward; + while (seg != NULL) { + log_android(ANDROID_LOG_WARN, "%s queued %u...%u sent %u", + session, + seg->seq - s->tcp.remote_start, + seg->seq + seg->len - s->tcp.remote_start, + seg->sent); + seg = seg->next; + } + } + + // Get receive window + uint32_t window = get_receive_window(s); + uint32_t prev = s->tcp.recv_window; + s->tcp.recv_window = window; + if ((prev == 0 && window > 0) || (prev > 0 && window == 0)) + log_android(ANDROID_LOG_WARN, "%s recv window %u > %u", + session, prev, window); + + // Acknowledge forwarded data + if (fwd || (prev == 0 && window > 0)) { + if (fwd && s->tcp.forward == NULL && s->tcp.state == TCP_CLOSE_WAIT) { + log_android(ANDROID_LOG_WARN, "%s confirm FIN", session); + s->tcp.remote_seq++; // remote FIN + } + if (write_ack(args, &s->tcp) >= 0) + s->tcp.time = time(NULL); + } + + if (s->tcp.state == TCP_ESTABLISHED || s->tcp.state == TCP_CLOSE_WAIT) { + // Check socket read + // Send window can be changed in the mean time + + uint32_t send_window = get_send_window(&s->tcp); + if ((ev->events & EPOLLIN) && send_window > 0) { + s->tcp.time = time(NULL); + + uint32_t buffer_size = (send_window > s->tcp.mss + ? s->tcp.mss : send_window); + uint8_t *buffer = ng_malloc(buffer_size, "tcp socket"); + ssize_t bytes = recv(s->socket, buffer, (size_t) buffer_size, 0); + if (bytes < 0) { + // Socket error + log_android(ANDROID_LOG_ERROR, "%s recv error %d: %s", + session, errno, strerror(errno)); + + if (errno != EINTR && errno != EAGAIN) + write_rst(args, &s->tcp); + } else if (bytes == 0) { + log_android(ANDROID_LOG_WARN, "%s recv eof", session); + + if (s->tcp.forward == NULL) { + if (write_fin_ack(args, &s->tcp) >= 0) { + log_android(ANDROID_LOG_WARN, "%s FIN sent", session); + s->tcp.local_seq++; // local FIN + } + + if (s->tcp.state == TCP_ESTABLISHED) + s->tcp.state = TCP_FIN_WAIT1; + else if (s->tcp.state == TCP_CLOSE_WAIT) + s->tcp.state = TCP_LAST_ACK; + else + log_android(ANDROID_LOG_ERROR, "%s invalid close", session); + } else { + // There was still data to send + log_android(ANDROID_LOG_ERROR, "%s close with queue", session); + write_rst(args, &s->tcp); + } + + if (close(s->socket)) + log_android(ANDROID_LOG_ERROR, "%s close error %d: %s", + session, errno, strerror(errno)); + s->socket = -1; + + } else { + // Socket read data + log_android(ANDROID_LOG_DEBUG, "%s recv bytes %d", session, bytes); + s->tcp.received += bytes; + + // Process DNS response + if (ntohs(s->tcp.dest) == 53 && bytes > 2) { + ssize_t dlen = bytes - 2; + parse_dns_response(args, s, buffer + 2, (size_t *) &dlen); + } + + // Forward to tun + if (write_data(args, &s->tcp, buffer, (size_t) bytes) >= 0) { + s->tcp.local_seq += bytes; + s->tcp.unconfirmed++; + } + } + ng_free(buffer, __FILE__, __LINE__); + } + } + } + } + + if (s->tcp.state != oldstate || s->tcp.local_seq != oldlocal || + s->tcp.remote_seq != oldremote) + log_android(ANDROID_LOG_DEBUG, "%s new state", session); +} + + + + + +jboolean handle_tcp(const struct arguments *args, + const uint8_t *pkt, size_t length, + const uint8_t *payload, + int uid, int allowed, struct allowed *redirect, + const int epoll_fd) { + // Get headers + const uint8_t 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; + const uint8_t tcpoptlen = (uint8_t) ((tcphdr->doff - 5) * 4); + const uint8_t *tcpoptions = payload + sizeof(struct tcphdr); + const uint8_t *data = payload + sizeof(struct tcphdr) + tcpoptlen; + const uint16_t datalen = (const uint16_t) (length - (data - pkt)); + + // Search session + struct ng_session *cur = args->ctx->ng_session; + while (cur != NULL && + !(cur->protocol == IPPROTO_TCP && + cur->tcp.version == version && + cur->tcp.source == tcphdr->source && cur->tcp.dest == tcphdr->dest && + (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; + + // Prepare logging + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + if (version == 4) { + inet_ntop(AF_INET, &ip4->saddr, source, sizeof(source)); + inet_ntop(AF_INET, &ip4->daddr, dest, sizeof(dest)); + } else { + inet_ntop(AF_INET6, &ip6->ip6_src, source, sizeof(source)); + inet_ntop(AF_INET6, &ip6->ip6_dst, dest, sizeof(dest)); + } + + + + + char flags[10]; + int flen = 0; + if (tcphdr->syn) + flags[flen++] = 'S'; + if (tcphdr->ack) + flags[flen++] = 'A'; + if (tcphdr->psh) + flags[flen++] = 'P'; + if (tcphdr->fin) + flags[flen++] = 'F'; + if (tcphdr->rst) + flags[flen++] = 'R'; + if (tcphdr->urg) + flags[flen++] = 'U'; + flags[flen] = 0; + + char packet[250]; + sprintf(packet, + "TCP %s %s/%u > %s/%u seq %u ack %u data %u win %u uid %d", + flags, + source, ntohs(tcphdr->source), + dest, ntohs(tcphdr->dest), + ntohl(tcphdr->seq) - (cur == NULL ? 0 : cur->tcp.remote_start), + tcphdr->ack ? ntohl(tcphdr->ack_seq) - (cur == NULL ? 0 : cur->tcp.local_start) : 0, + datalen, ntohs(tcphdr->window), uid); + log_android(tcphdr->urg ? ANDROID_LOG_WARN : ANDROID_LOG_DEBUG, packet); + + log_android(ANDROID_LOG_ERROR,"handling TCP with source: %s, dest: %s", source, dest); + + // Drop URG data + if (tcphdr->urg) + return 1; + + + // Check session + if (cur == NULL) { + if (tcphdr->syn) { + // Decode options + // http://www.iana.org/assignments/tcp-parameters/tcp-parameters.xhtml#tcp-parameters-1 + uint16_t mss = get_default_mss(version); + uint8_t ws = 0; + int optlen = tcpoptlen; + uint8_t *options = (uint8_t *) tcpoptions; + while (optlen > 0) { + uint8_t kind = *options; + uint8_t len = *(options + 1); + if (kind == 0) // End of options list + break; + + if (kind == 2 && len == 4) + mss = ntohs(*((uint16_t *) (options + 2))); + + else if (kind == 3 && len == 3) + ws = *(options + 2); + + if (kind == 1) { + optlen--; + options++; + } else { + optlen -= len; + options += len; + } + } + + log_android(ANDROID_LOG_ERROR, "%s new session mss %u ws %u window %u, tcp doff: %u", + packet, mss, ws, ntohs(tcphdr->window) << ws, tcphdr->doff); + + + // Register session + struct ng_session *s = ng_malloc(sizeof(struct ng_session), "tcp session"); + s->protocol = IPPROTO_TCP; + + + + s->tcp.time = time(NULL); + s->tcp.uid = uid; + s->tcp.version = version; + s->tcp.mss = mss; + s->tcp.recv_scale = ws; + s->tcp.send_scale = ws; + s->tcp.send_window = ((uint32_t) ntohs(tcphdr->window)) << s->tcp.send_scale; + s->tcp.unconfirmed = 0; + s->tcp.remote_seq = ntohl(tcphdr->seq); // ISN remote + s->tcp.local_seq = (uint32_t) rand(); // ISN local + s->tcp.remote_start = s->tcp.remote_seq; + s->tcp.local_start = s->tcp.local_seq; + s->tcp.acked = 0; + s->tcp.last_keep_alive = 0; + s->tcp.sent = 0; + s->tcp.received = 0; + + if (version == 4) { + s->tcp.saddr.ip4 = (__be32) ip4->saddr; + s->tcp.daddr.ip4 = (__be32) ip4->daddr; + } else { + memcpy(&s->tcp.saddr.ip6, &ip6->ip6_src, 16); + memcpy(&s->tcp.daddr.ip6, &ip6->ip6_dst, 16); + } + + s->tcp.source = tcphdr->source; + s->tcp.dest = tcphdr->dest; + s->tcp.state = TCP_LISTEN; + s->tcp.socks5 = SOCKS5_NONE; + s->tcp.forward = NULL; + s->next = NULL; + + if (datalen) { + log_android(ANDROID_LOG_ERROR, "%s some SYN data", packet); + s->tcp.forward = ng_malloc(sizeof(struct segment), "syn segment"); + s->tcp.forward->seq = s->tcp.remote_seq; + s->tcp.forward->len = datalen; + s->tcp.forward->sent = 0; + s->tcp.forward->psh = tcphdr->psh; + s->tcp.forward->data = ng_malloc(datalen, "syn segment data"); + memcpy(s->tcp.forward->data, data, datalen); + 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; + } + + 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)); + + + + // Monitor events + memset(&s->ev, 0, sizeof(struct epoll_event)); + s->ev.events = EPOLLOUT | EPOLLERR; + s->ev.data.ptr = s; + log_android(ANDROID_LOG_ERROR, "FULL adding epoll monitor events: %d", epoll_fd); + 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; + + 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); + + struct tcp_session rst; + memset(&rst, 0, sizeof(struct tcp_session)); + rst.version = version; + rst.local_seq = ntohl(tcphdr->ack_seq); + rst.remote_seq = ntohl(tcphdr->seq) + datalen + (tcphdr->syn || tcphdr->fin ? 1 : 0); + + if (version == 4) { + rst.saddr.ip4 = (__be32) ip4->saddr; + rst.daddr.ip4 = (__be32) ip4->daddr; + } else { + memcpy(&rst.saddr.ip6, &ip6->ip6_src, 16); + memcpy(&rst.daddr.ip6, &ip6->ip6_dst, 16); + } + + rst.source = tcphdr->source; + rst.dest = tcphdr->dest; + + write_rst(args, &rst); + return 0; + } + } else { + char session[250]; + sprintf(session, + "%s %s loc %u rem %u acked %u", + packet, + strstate(cur->tcp.state), + cur->tcp.local_seq - cur->tcp.local_start, + cur->tcp.remote_seq - cur->tcp.remote_start, + cur->tcp.acked - cur->tcp.local_start); + + // Session found + if (cur->tcp.state == TCP_CLOSING || cur->tcp.state == TCP_CLOSE) { + log_android(ANDROID_LOG_WARN, "%s was closed", session); + write_rst(args, &cur->tcp); + return 0; + } else { + int oldstate = cur->tcp.state; + uint32_t oldlocal = cur->tcp.local_seq; + uint32_t oldremote = cur->tcp.remote_seq; + + log_android(ANDROID_LOG_DEBUG, "%s handling", session); + + if (!tcphdr->syn) + cur->tcp.time = time(NULL); + cur->tcp.send_window = ((uint32_t) ntohs(tcphdr->window)) << cur->tcp.send_scale; + cur->tcp.unconfirmed = 0; + + // Do not change the order of the conditions + + // Queue data to forward + if (datalen) { + if (cur->socket < 0) { + log_android(ANDROID_LOG_ERROR, "%s data while local closed", session); + write_rst(args, &cur->tcp); + return 0; + } + if (cur->tcp.state == TCP_CLOSE_WAIT) { + log_android(ANDROID_LOG_ERROR, "%s data while remote closed", session); + write_rst(args, &cur->tcp); + return 0; + } + queue_tcp(args, tcphdr, session, &cur->tcp, data, datalen); + } + + if (tcphdr->rst /* +ACK */) { + // No sequence check + // http://tools.ietf.org/html/rfc1122#page-87 + log_android(ANDROID_LOG_WARN, "%s received reset", session); + cur->tcp.state = TCP_CLOSING; + return 0; + } else { + if (!tcphdr->ack || ntohl(tcphdr->ack_seq) == cur->tcp.local_seq) { + if (tcphdr->syn) { + log_android(ANDROID_LOG_WARN, "%s repeated SYN", session); + // The socket is probably not opened yet + + } else if (tcphdr->fin /* +ACK */) { + if (cur->tcp.state == TCP_ESTABLISHED) { + log_android(ANDROID_LOG_WARN, "%s FIN received", session); + if (cur->tcp.forward == NULL) { + cur->tcp.remote_seq++; // remote FIN + if (write_ack(args, &cur->tcp) >= 0) + cur->tcp.state = TCP_CLOSE_WAIT; + } else + cur->tcp.state = TCP_CLOSE_WAIT; + } else if (cur->tcp.state == TCP_CLOSE_WAIT) { + log_android(ANDROID_LOG_WARN, "%s repeated FIN", session); + // The socket is probably not closed yet + } else if (cur->tcp.state == TCP_FIN_WAIT1) { + log_android(ANDROID_LOG_WARN, "%s last ACK", session); + cur->tcp.remote_seq++; // remote FIN + if (write_ack(args, &cur->tcp) >= 0) + cur->tcp.state = TCP_CLOSE; + } else { + log_android(ANDROID_LOG_ERROR, "%s invalid FIN", session); + return 0; + } + + } else if (tcphdr->ack) { + cur->tcp.acked = ntohl(tcphdr->ack_seq); + + if (cur->tcp.state == TCP_SYN_RECV) + cur->tcp.state = TCP_ESTABLISHED; + + else if (cur->tcp.state == TCP_ESTABLISHED) { + // Do nothing + } else if (cur->tcp.state == TCP_LAST_ACK) + cur->tcp.state = TCP_CLOSING; + + else if (cur->tcp.state == TCP_CLOSE_WAIT) { + // ACK after FIN/ACK + } else if (cur->tcp.state == TCP_FIN_WAIT1) { + // Do nothing + } else { + log_android(ANDROID_LOG_ERROR, "%s invalid state", session); + return 0; + } + } else { + log_android(ANDROID_LOG_ERROR, "%s unknown packet", session); + return 0; + } + } else { + uint32_t ack = ntohl(tcphdr->ack_seq); + if ((uint32_t) (ack + 1) == cur->tcp.local_seq) { + // Keep alive + if (cur->tcp.state == TCP_ESTABLISHED) { + int on = 1; + if (setsockopt(cur->socket, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on))) + log_android(ANDROID_LOG_ERROR, + "%s setsockopt SO_KEEPALIVE error %d: %s", + session, errno, strerror(errno)); + else + log_android(ANDROID_LOG_WARN, "%s enabled keep alive", session); + } else + log_android(ANDROID_LOG_WARN, "%s keep alive", session); + + } else if (compare_u32(ack, cur->tcp.local_seq) < 0) { + if (compare_u32(ack, cur->tcp.acked) <= 0) + log_android( + ack == cur->tcp.acked ? ANDROID_LOG_WARN : ANDROID_LOG_ERROR, + "%s repeated ACK %u/%u", + session, + ack - cur->tcp.local_start, + cur->tcp.acked - cur->tcp.local_start); + else { + log_android(ANDROID_LOG_WARN, "%s previous ACK %u", + session, ack - cur->tcp.local_seq); + cur->tcp.acked = ack; + } + + return 1; + } else { + log_android(ANDROID_LOG_ERROR, "%s future ACK", session); + write_rst(args, &cur->tcp); + return 0; + } + } + } + + if (cur->tcp.state != oldstate || + cur->tcp.local_seq != oldlocal || + cur->tcp.remote_seq != oldremote) + log_android(ANDROID_LOG_INFO, "%s > %s loc %u rem %u", + session, + strstate(cur->tcp.state), + cur->tcp.local_seq - cur->tcp.local_start, + cur->tcp.remote_seq - cur->tcp.remote_start); + } + } + + return 1; +} + + + + + + + + + + + + + + +void queue_tcp(const struct arguments *args, + const struct tcphdr *tcphdr, + const char *session, struct tcp_session *cur, + const uint8_t *data, uint16_t datalen) { + uint32_t seq = ntohl(tcphdr->seq); + if (compare_u32(seq, cur->remote_seq) < 0) + log_android(ANDROID_LOG_WARN, "%s already forwarded %u..%u", + session, + seq - cur->remote_start, seq + datalen - cur->remote_start); + else { + struct segment *p = NULL; + struct segment *s = cur->forward; + while (s != NULL && compare_u32(s->seq, seq) < 0) { + p = s; + s = s->next; + } + + if (s == NULL || compare_u32(s->seq, seq) > 0) { + log_android(ANDROID_LOG_DEBUG, "%s queuing %u...%u", + session, + seq - cur->remote_start, seq + datalen - cur->remote_start); + struct segment *n = ng_malloc(sizeof(struct segment), "tcp segment"); + n->seq = seq; + n->len = datalen; + n->sent = 0; + n->psh = tcphdr->psh; + n->data = ng_malloc(datalen, "tcp segment"); + memcpy(n->data, data, datalen); + n->next = s; + if (p == NULL) + cur->forward = n; + else + p->next = n; + } else if (s != NULL && s->seq == seq) { + if (s->len == datalen) + log_android(ANDROID_LOG_WARN, "%s segment already queued %u..%u", + session, + s->seq - cur->remote_start, s->seq + s->len - cur->remote_start); + else if (s->len < datalen) { + log_android(ANDROID_LOG_WARN, "%s segment smaller %u..%u > %u", + session, + s->seq - cur->remote_start, s->seq + s->len - cur->remote_start, + s->seq + datalen - cur->remote_start); + ng_free(s->data, __FILE__, __LINE__); + s->len = datalen; + s->data = ng_malloc(datalen, "tcp segment smaller"); + memcpy(s->data, data, datalen); + } else { + log_android(ANDROID_LOG_ERROR, "%s segment larger %u..%u < %u", + session, + s->seq - cur->remote_start, s->seq + s->len - cur->remote_start, + s->seq + datalen - cur->remote_start); + ng_free(s->data, __FILE__, __LINE__); + s->len = datalen; + s->data = ng_malloc(datalen, "tcp segment larger"); + memcpy(s->data, data, datalen); + } + } + } +} + +int open_tcp_socket(const struct arguments *args, + const struct tcp_session *cur, const struct allowed *redirect) { + + int sock; + int version; + if (redirect == NULL) { + if (*socks5_addr && socks5_port) + version = (strstr(socks5_addr, ":") == NULL ? 4 : 6); + else + version = cur->version; + } else + version = (strstr(redirect->raddr, ":") == NULL ? 4 : 6); + + + // Get TCP socket + if ((sock = socket(version == 4 ? PF_INET : PF_INET6, SOCK_STREAM, 0)) < 0) { + log_android(ANDROID_LOG_ERROR, "socket error %d: %s", errno, strerror(errno)); + return -1; + } + + // Protect + if (protect_socket(args, sock) < 0) + return -1; + + int on = 1; + if (setsockopt(sock, SOL_TCP, TCP_NODELAY, &on, sizeof(on)) < 0) + log_android(ANDROID_LOG_ERROR, "setsockopt TCP_NODELAY error %d: %s", + errno, strerror(errno)); + + // Set non blocking + int flags = fcntl(sock, F_GETFL, 0); + if (flags < 0 || fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0) { + log_android(ANDROID_LOG_ERROR, "fcntl socket O_NONBLOCK error %d: %s", + errno, strerror(errno)); + return -1; + } + + // Build target address + struct sockaddr_in addr4; + struct sockaddr_in6 addr6; + if (redirect == NULL) { + + if (*socks5_addr && socks5_port) { + log_android(ANDROID_LOG_WARN, "TCP%d SOCKS5 to %s/%u", + version, socks5_addr, socks5_port); + + if (version == 4) { + addr4.sin_family = AF_INET; + inet_pton(AF_INET, socks5_addr, &addr4.sin_addr); + addr4.sin_port = htons(socks5_port); + } else { + addr6.sin6_family = AF_INET6; + inet_pton(AF_INET6, socks5_addr, &addr6.sin6_addr); + addr6.sin6_port = htons(socks5_port); + } + } else { + + if (version == 4) { + addr4.sin_family = AF_INET; + + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + inet_ntop(AF_INET, &cur->daddr.ip4, source, sizeof(source)); + inet_ntop(AF_INET, &cur->saddr.ip4, dest, sizeof(dest)); + log_android(ANDROID_LOG_ERROR, "setting sin address source to: %s, dest: %s", source, dest); + + addr4.sin_addr.s_addr = (__be32) cur->daddr.ip4; + addr4.sin_port = cur->dest; + } else { + addr6.sin6_family = AF_INET6; + memcpy(&addr6.sin6_addr, &cur->daddr.ip6, 16); + addr6.sin6_port = cur->dest; + } + } + } else { + log_android(ANDROID_LOG_ERROR, "TCP%d redirect to %s/%u", + version, redirect->raddr, redirect->rport); + + if (version == 4) { + addr4.sin_family = AF_INET; + inet_pton(AF_INET, redirect->raddr, &addr4.sin_addr); + addr4.sin_port = htons(redirect->rport); + } else { + addr6.sin6_family = AF_INET6; + inet_pton(AF_INET6, redirect->raddr, &addr6.sin6_addr); + addr6.sin6_port = htons(redirect->rport); + } + } + + // Initiate connect + int err = connect(sock, + (version == 4 ? (const struct sockaddr *) &addr4 + : (const struct sockaddr *) &addr6), + (socklen_t) (version == 4 + ? sizeof(struct sockaddr_in) + : sizeof(struct sockaddr_in6))); + if (err < 0 && errno != EINPROGRESS) { + log_android(ANDROID_LOG_ERROR, "connect error %d: %s", errno, strerror(errno)); + return -1; + } + + + log_android(ANDROID_LOG_ERROR, "sock connect result: %d", sock); + + return sock; +} + +int write_syn_ack(const struct arguments *args, struct tcp_session *cur) { + if (write_tcp(args, cur, NULL, 0, 1, 1, 0, 0) < 0) { + cur->state = TCP_CLOSING; + return -1; + } + return 0; +} + +int write_ack(const struct arguments *args, struct tcp_session *cur) { + + log_android(ANDROID_LOG_ERROR,"Writing TCP ack to %d", cur->dest); + if (write_tcp(args, cur, NULL, 0, 0, 1, 0, 0) < 0) { + cur->state = TCP_CLOSING; + return -1; + } + return 0; +} + +int write_data(const struct arguments *args, struct tcp_session *cur, + const uint8_t *buffer, size_t length) { + + log_android(ANDROID_LOG_ERROR,"in write tcp data with length: %d", length); + + if (write_tcp(args, cur, buffer, length, 0, 1, 0, 0) < 0) { + cur->state = TCP_CLOSING; + return -1; + } + return 0; +} + +int write_fin_ack(const struct arguments *args, struct tcp_session *cur) { + if (write_tcp(args, cur, NULL, 0, 0, 1, 1, 0) < 0) { + cur->state = TCP_CLOSING; + return -1; + } + return 0; +} + +void write_rst(const struct arguments *args, struct tcp_session *cur) { + // https://www.snellman.net/blog/archive/2016-02-01-tcp-rst/ + int ack = 0; + if (cur->state == TCP_LISTEN) { + ack = 1; + cur->remote_seq++; // SYN + } + write_tcp(args, cur, NULL, 0, 0, ack, 0, 1); + if (cur->state != TCP_CLOSE) + cur->state = TCP_CLOSING; +} + +ssize_t write_tcp(const struct arguments *args, const struct tcp_session *cur, + const uint8_t *data, size_t datalen, + int syn, int ack, int fin, int rst) { + size_t len; + u_int8_t *buffer; + struct tcphdr *tcp; + uint16_t csum; + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + + // Build packet + int optlen = (syn ? 4 + 3 + 1 : 0); + uint8_t *options; + if (cur->version == 4) { + len = sizeof(struct iphdr) + sizeof(struct tcphdr) + optlen + datalen; + buffer = ng_malloc(len, "tcp write4"); + struct iphdr *ip4 = (struct iphdr *) buffer; + tcp = (struct tcphdr *) (buffer + sizeof(struct iphdr)); + options = buffer + sizeof(struct iphdr) + sizeof(struct tcphdr); + if (datalen) + memcpy(buffer + sizeof(struct iphdr) + sizeof(struct tcphdr) + optlen, data, datalen); + + // Build IP4 header + memset(ip4, 0, sizeof(struct iphdr)); + ip4->version = 4; + ip4->ihl = sizeof(struct iphdr) >> 2; + ip4->tot_len = htons(len); + ip4->ttl = IPDEFTTL; + ip4->protocol = IPPROTO_TCP; + ip4->saddr = cur->daddr.ip4; + ip4->daddr = cur->saddr.ip4; + + // Calculate IP4 checksum + ip4->check = ~calc_checksum(0, (uint8_t *) ip4, sizeof(struct iphdr)); + + // Calculate TCP4 checksum + struct ippseudo pseudo; + memset(&pseudo, 0, sizeof(struct ippseudo)); + pseudo.ippseudo_src.s_addr = (__be32) ip4->saddr; + pseudo.ippseudo_dst.s_addr = (__be32) ip4->daddr; + pseudo.ippseudo_p = ip4->protocol; + pseudo.ippseudo_len = htons(sizeof(struct tcphdr) + optlen + datalen); + + csum = calc_checksum(0, (uint8_t *) &pseudo, sizeof(struct ippseudo)); + } else { + len = sizeof(struct ip6_hdr) + sizeof(struct tcphdr) + optlen + datalen; + buffer = ng_malloc(len, "tcp write 6"); + struct ip6_hdr *ip6 = (struct ip6_hdr *) buffer; + tcp = (struct tcphdr *) (buffer + sizeof(struct ip6_hdr)); + options = buffer + sizeof(struct ip6_hdr) + sizeof(struct tcphdr); + if (datalen) + memcpy(buffer + sizeof(struct ip6_hdr) + sizeof(struct tcphdr) + optlen, data, datalen); + + // Build IP6 header + memset(ip6, 0, sizeof(struct ip6_hdr)); + ip6->ip6_ctlun.ip6_un1.ip6_un1_plen = htons(len - sizeof(struct ip6_hdr)); + ip6->ip6_ctlun.ip6_un1.ip6_un1_nxt = IPPROTO_TCP; + ip6->ip6_ctlun.ip6_un1.ip6_un1_hlim = IPDEFTTL; + ip6->ip6_ctlun.ip6_un2_vfc = 0x60; + memcpy(&(ip6->ip6_src), &cur->daddr.ip6, 16); + memcpy(&(ip6->ip6_dst), &cur->saddr.ip6, 16); + + // Calculate TCP6 checksum + struct ip6_hdr_pseudo pseudo; + memset(&pseudo, 0, sizeof(struct ip6_hdr_pseudo)); + memcpy(&pseudo.ip6ph_src, &ip6->ip6_dst, 16); + memcpy(&pseudo.ip6ph_dst, &ip6->ip6_src, 16); + pseudo.ip6ph_len = ip6->ip6_ctlun.ip6_un1.ip6_un1_plen; + pseudo.ip6ph_nxt = ip6->ip6_ctlun.ip6_un1.ip6_un1_nxt; + + csum = calc_checksum(0, (uint8_t *) &pseudo, sizeof(struct ip6_hdr_pseudo)); + } + + + // Build TCP header + memset(tcp, 0, sizeof(struct tcphdr)); + tcp->source = cur->dest; + tcp->dest = cur->source; + tcp->seq = htonl(cur->local_seq); + tcp->ack_seq = htonl((uint32_t) (cur->remote_seq)); + tcp->doff = (__u16) ((sizeof(struct tcphdr) + optlen) >> 2); + tcp->syn = (__u16) syn; + tcp->ack = (__u16) ack; + tcp->fin = (__u16) fin; + tcp->rst = (__u16) rst; + tcp->window = htons(cur->recv_window >> cur->recv_scale); + + if (!tcp->ack) + tcp->ack_seq = 0; + + // TCP options + if (syn) { + *(options) = 2; // MSS + *(options + 1) = 4; // total option length + *((uint16_t *) (options + 2)) = get_default_mss(cur->version); + + *(options + 4) = 3; // window scale + *(options + 5) = 3; // total option length + *(options + 6) = cur->recv_scale; + + *(options + 7) = 0; // End, padding + } + + // Continue checksum + csum = calc_checksum(csum, (uint8_t *) tcp, sizeof(struct tcphdr)); + csum = calc_checksum(csum, options, (size_t) optlen); + csum = calc_checksum(csum, data, datalen); + tcp->check = ~csum; + + inet_ntop(cur->version == 4 ? AF_INET : AF_INET6, + cur->version == 4 ? (const void *) &cur->saddr.ip4 : (const void *) &cur->saddr.ip6, + source, sizeof(source)); + inet_ntop(cur->version == 4 ? AF_INET : AF_INET6, + cur->version == 4 ? (const void *) &cur->daddr.ip4 : (const void *) &cur->daddr.ip6, + dest, sizeof(dest)); + + // Send packet + log_android(ANDROID_LOG_ERROR, + "TCP sending%s%s%s%s to tun %s/%u seq %u ack %u data %u", + (tcp->syn ? " SYN" : ""), + (tcp->ack ? " ACK" : ""), + (tcp->fin ? " FIN" : ""), + (tcp->rst ? " RST" : ""), + dest, ntohs(tcp->dest), + ntohl(tcp->seq) - cur->local_start, + ntohl(tcp->ack_seq) - cur->remote_start, + datalen); + + ssize_t res = 0; + res = write(args->tun, buffer, len); + + // Write pcap record + if (res >= 0) { + if (pcap_file != NULL) + write_pcap_rec(buffer, (size_t) res); + } else + log_android(ANDROID_LOG_ERROR, "TCP write%s%s%s%s data %d error %d: %s", + (tcp->syn ? " SYN" : ""), + (tcp->ack ? " ACK" : ""), + (tcp->fin ? " FIN" : ""), + (tcp->rst ? " RST" : ""), + datalen, + errno, strerror((errno))); + + ng_free(buffer, __FILE__, __LINE__); + + if (res != len) { + log_android(ANDROID_LOG_ERROR, "TCP write %d/%d", res, len); + return -1; + } + + return res; +} diff --git a/NetGuard/app/src/main/main/jni/netguard/udp.c b/NetGuard/app/src/main/main/jni/netguard/udp.c new file mode 100644 index 0000000..b070c0d --- /dev/null +++ b/NetGuard/app/src/main/main/jni/netguard/udp.c @@ -0,0 +1,549 @@ +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +#include "netguard.h" + +extern FILE *pcap_file; + +int get_udp_timeout(const struct udp_session *u, int sessions, int maxsessions) { + int timeout = (ntohs(u->dest) == 53 ? UDP_TIMEOUT_53 : UDP_TIMEOUT_ANY); + + int scale = 100 - sessions * 100 / maxsessions; + timeout = timeout * scale / 100; + + return timeout; +} + +int check_udp_session(const struct arguments *args, struct ng_session *s, + int sessions, int maxsessions) { + time_t now = time(NULL); + + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + if (s->udp.version == 4) { + inet_ntop(AF_INET, &s->udp.saddr.ip4, source, sizeof(source)); + inet_ntop(AF_INET, &s->udp.daddr.ip4, dest, sizeof(dest)); + } else { + inet_ntop(AF_INET6, &s->udp.saddr.ip6, source, sizeof(source)); + inet_ntop(AF_INET6, &s->udp.daddr.ip6, dest, sizeof(dest)); + } + + // Check session timeout + int timeout = get_udp_timeout(&s->udp, sessions, maxsessions); + if (s->udp.state == UDP_ACTIVE && s->udp.time + timeout < now) { + log_android(ANDROID_LOG_WARN, "UDP idle %d/%d sec state %d from %s/%u to %s/%u", + now - s->udp.time, timeout, s->udp.state, + source, ntohs(s->udp.source), dest, ntohs(s->udp.dest)); + s->udp.state = UDP_FINISHING; + } + + // Check finished sessions + if (s->udp.state == UDP_FINISHING) { + log_android(ANDROID_LOG_INFO, "UDP close from %s/%u to %s/%u socket %d", + source, ntohs(s->udp.source), dest, ntohs(s->udp.dest), s->socket); + + if (close(s->socket)) + log_android(ANDROID_LOG_ERROR, "UDP close %d error %d: %s", + s->socket, errno, strerror(errno)); + s->socket = -1; + + s->udp.time = time(NULL); + s->udp.state = UDP_CLOSED; + } + + if (s->udp.state == UDP_CLOSED && (s->udp.sent || s->udp.received)) { + account_usage(args, s->udp.version, IPPROTO_UDP, + dest, ntohs(s->udp.dest), s->udp.uid, s->udp.sent, s->udp.received); + s->udp.sent = 0; + s->udp.received = 0; + } + + // Cleanup lingering sessions + if ((s->udp.state == UDP_CLOSED || s->udp.state == UDP_BLOCKED) && + s->udp.time + UDP_KEEP_TIMEOUT < now) + return 1; + + return 0; +} + +void check_udp_socket(const struct arguments *args, const struct epoll_event *ev) { + struct ng_session *s = (struct ng_session *) ev->data.ptr; + + // Check socket error + if (ev->events & EPOLLERR) { + s->udp.time = time(NULL); + + int serr = 0; + socklen_t optlen = sizeof(int); + int err = getsockopt(s->socket, SOL_SOCKET, SO_ERROR, &serr, &optlen); + if (err < 0) + log_android(ANDROID_LOG_ERROR, "UDP getsockopt error %d: %s", + errno, strerror(errno)); + else if (serr) + log_android(ANDROID_LOG_ERROR, "UDP SO_ERROR %d: %s", serr, strerror(serr)); + + s->udp.state = UDP_FINISHING; + } else { + // Check socket read + if (ev->events & EPOLLIN) { + s->udp.time = time(NULL); + + uint8_t *buffer = ng_malloc(s->udp.mss, "udp recv"); + ssize_t bytes = recv(s->socket, buffer, s->udp.mss, 0); + if (bytes < 0) { + // Socket error + log_android(ANDROID_LOG_WARN, "UDP recv error %d: %s", + errno, strerror(errno)); + + if (errno != EINTR && errno != EAGAIN) + s->udp.state = UDP_FINISHING; + } else if (bytes == 0) { + log_android(ANDROID_LOG_WARN, "UDP recv eof"); + s->udp.state = UDP_FINISHING; + + } else { + // Socket read data + char dest[INET6_ADDRSTRLEN + 1]; + if (s->udp.version == 4) + inet_ntop(AF_INET, &s->udp.daddr.ip4, dest, sizeof(dest)); + else + inet_ntop(AF_INET6, &s->udp.daddr.ip6, dest, sizeof(dest)); + log_android(ANDROID_LOG_INFO, "UDP recv bytes %d from %s/%u for tun", + bytes, dest, ntohs(s->udp.dest)); + + s->udp.received += bytes; + + // Process DNS response + if (ntohs(s->udp.dest) == 53) + parse_dns_response(args, s, buffer, (size_t *) &bytes); + + // Forward to tun + if (write_udp(args, &s->udp, buffer, (size_t) bytes) < 0) + s->udp.state = UDP_FINISHING; + else { + // Prevent too many open files + if (ntohs(s->udp.dest) == 53) + s->udp.state = UDP_FINISHING; + } + } + ng_free(buffer, __FILE__, __LINE__); + } + } +} + +int has_udp_session(const struct arguments *args, const uint8_t *pkt, const uint8_t *payload) { + // Get headers + const uint8_t version = (*pkt) >> 4; + const struct iphdr *ip4 = (struct iphdr *) pkt; + const struct ip6_hdr *ip6 = (struct ip6_hdr *) pkt; + const struct udphdr *udphdr = (struct udphdr *) payload; + + if (ntohs(udphdr->dest) == 53 && !args->fwd53) + return 1; + + // Search session + struct ng_session *cur = args->ctx->ng_session; + while (cur != NULL && + !(cur->protocol == IPPROTO_UDP && + cur->udp.version == version && + cur->udp.source == udphdr->source && cur->udp.dest == udphdr->dest && + (version == 4 ? cur->udp.saddr.ip4 == ip4->saddr && + cur->udp.daddr.ip4 == ip4->daddr + : memcmp(&cur->udp.saddr.ip6, &ip6->ip6_src, 16) == 0 && + memcmp(&cur->udp.daddr.ip6, &ip6->ip6_dst, 16) == 0))) + cur = cur->next; + + return (cur != NULL); +} + +void block_udp(const struct arguments *args, + const uint8_t *pkt, size_t length, + const uint8_t *payload, + int uid) { + // Get headers + const uint8_t version = (*pkt) >> 4; + const struct iphdr *ip4 = (struct iphdr *) pkt; + const struct ip6_hdr *ip6 = (struct ip6_hdr *) pkt; + const struct udphdr *udphdr = (struct udphdr *) payload; + + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + if (version == 4) { + inet_ntop(AF_INET, &ip4->saddr, source, sizeof(source)); + inet_ntop(AF_INET, &ip4->daddr, dest, sizeof(dest)); + } else { + inet_ntop(AF_INET6, &ip6->ip6_src, source, sizeof(source)); + inet_ntop(AF_INET6, &ip6->ip6_dst, dest, sizeof(dest)); + } + + log_android(ANDROID_LOG_INFO, "UDP blocked session from %s/%u to %s/%u", + source, ntohs(udphdr->source), dest, ntohs(udphdr->dest)); + + // Register session + struct ng_session *s = ng_malloc(sizeof(struct ng_session), "udp session block"); + s->protocol = IPPROTO_UDP; + + s->udp.time = time(NULL); + s->udp.uid = uid; + s->udp.version = version; + + if (version == 4) { + s->udp.saddr.ip4 = (__be32) ip4->saddr; + s->udp.daddr.ip4 = (__be32) ip4->daddr; + } else { + memcpy(&s->udp.saddr.ip6, &ip6->ip6_src, 16); + memcpy(&s->udp.daddr.ip6, &ip6->ip6_dst, 16); + } + + s->udp.source = udphdr->source; + s->udp.dest = udphdr->dest; + s->udp.state = UDP_BLOCKED; + s->socket = -1; + + s->next = args->ctx->ng_session; + args->ctx->ng_session = s; +} + +jboolean handle_udp(const struct arguments *args, + const uint8_t *pkt, size_t length, + const uint8_t *payload, + int uid, struct allowed *redirect, + const int epoll_fd) { + // Get headers + const uint8_t version = (*pkt) >> 4; + const struct iphdr *ip4 = (struct iphdr *) pkt; + const struct ip6_hdr *ip6 = (struct ip6_hdr *) pkt; + const struct udphdr *udphdr = (struct udphdr *) payload; + const uint8_t *data = payload + sizeof(struct udphdr); + const size_t datalen = length - (data - pkt); + + // Search session + struct ng_session *cur = args->ctx->ng_session; + while (cur != NULL && + !(cur->protocol == IPPROTO_UDP && + cur->udp.version == version && + cur->udp.source == udphdr->source && cur->udp.dest == udphdr->dest && + (version == 4 ? cur->udp.saddr.ip4 == ip4->saddr && + cur->udp.daddr.ip4 == ip4->daddr + : memcmp(&cur->udp.saddr.ip6, &ip6->ip6_src, 16) == 0 && + memcmp(&cur->udp.daddr.ip6, &ip6->ip6_dst, 16) == 0))) + cur = cur->next; + + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + if (version == 4) { + inet_ntop(AF_INET, &ip4->saddr, source, sizeof(source)); + inet_ntop(AF_INET, &ip4->daddr, dest, sizeof(dest)); + } else { + inet_ntop(AF_INET6, &ip6->ip6_src, source, sizeof(source)); + inet_ntop(AF_INET6, &ip6->ip6_dst, dest, sizeof(dest)); + } + + if (cur != NULL && cur->udp.state != UDP_ACTIVE) { + log_android(ANDROID_LOG_INFO, "UDP ignore session from %s/%u to %s/%u state %d", + source, ntohs(udphdr->source), dest, ntohs(udphdr->dest), cur->udp.state); + return 0; + } + + // Create new session if needed + if (cur == NULL) { + log_android(ANDROID_LOG_INFO, "UDP new session from %s/%u to %s/%u", + source, ntohs(udphdr->source), dest, ntohs(udphdr->dest)); + + // Register session + struct ng_session *s = ng_malloc(sizeof(struct ng_session), "udp session"); + s->protocol = IPPROTO_UDP; + + s->udp.time = time(NULL); + s->udp.uid = uid; + s->udp.version = version; + + int rversion; + if (redirect == NULL) + rversion = s->udp.version; + else + rversion = (strstr(redirect->raddr, ":") == NULL ? 4 : 6); + s->udp.mss = (uint16_t) (rversion == 4 ? UDP4_MAXMSG : UDP6_MAXMSG); + + s->udp.sent = 0; + s->udp.received = 0; + + if (version == 4) { + s->udp.saddr.ip4 = (__be32) ip4->saddr; + s->udp.daddr.ip4 = (__be32) ip4->daddr; + } else { + memcpy(&s->udp.saddr.ip6, &ip6->ip6_src, 16); + memcpy(&s->udp.daddr.ip6, &ip6->ip6_dst, 16); + } + + s->udp.source = udphdr->source; + s->udp.dest = udphdr->dest; + s->udp.state = UDP_ACTIVE; + s->next = NULL; + + // Open UDP socket + s->socket = open_udp_socket(args, &s->udp, redirect); + if (s->socket < 0) { + ng_free(s, __FILE__, __LINE__); + return 0; + } + + log_android(ANDROID_LOG_DEBUG, "UDP socket %d", s->socket); + + // Monitor events + memset(&s->ev, 0, sizeof(struct epoll_event)); + s->ev.events = EPOLLIN | 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 udp error %d: %s", errno, strerror(errno)); + + s->next = args->ctx->ng_session; + args->ctx->ng_session = s; + + cur = s; + } + + // Check for DHCP (tethering) + if (ntohs(udphdr->source) == 68 || ntohs(udphdr->dest) == 67) { + if (check_dhcp(args, &cur->udp, data, datalen) >= 0) + return 1; + } + + log_android(ANDROID_LOG_INFO, "UDP forward from tun %s/%u to %s/%u data %d", + source, ntohs(udphdr->source), dest, ntohs(udphdr->dest), datalen); + + cur->udp.time = time(NULL); + + int rversion; + struct sockaddr_in addr4; + struct sockaddr_in6 addr6; + if (redirect == NULL) { + rversion = cur->udp.version; + if (cur->udp.version == 4) { + addr4.sin_family = AF_INET; + addr4.sin_addr.s_addr = (__be32) cur->udp.daddr.ip4; + addr4.sin_port = cur->udp.dest; + } else { + addr6.sin6_family = AF_INET6; + memcpy(&addr6.sin6_addr, &cur->udp.daddr.ip6, 16); + addr6.sin6_port = cur->udp.dest; + } + } else { + rversion = (strstr(redirect->raddr, ":") == NULL ? 4 : 6); + log_android(ANDROID_LOG_WARN, "UDP%d redirect to %s/%u", + rversion, redirect->raddr, redirect->rport); + + if (rversion == 4) { + addr4.sin_family = AF_INET; + inet_pton(AF_INET, redirect->raddr, &addr4.sin_addr); + addr4.sin_port = htons(redirect->rport); + } else { + addr6.sin6_family = AF_INET6; + inet_pton(AF_INET6, redirect->raddr, &addr6.sin6_addr); + addr6.sin6_port = htons(redirect->rport); + } + } + + if (sendto(cur->socket, data, (socklen_t) datalen, MSG_NOSIGNAL, + (rversion == 4 ? (const struct sockaddr *) &addr4 + : (const struct sockaddr *) &addr6), + (socklen_t) (rversion == 4 ? sizeof(addr4) : sizeof(addr6))) != datalen) { + log_android(ANDROID_LOG_ERROR, "UDP sendto error %d: %s", errno, strerror(errno)); + if (errno != EINTR && errno != EAGAIN) { + cur->udp.state = UDP_FINISHING; + return 0; + } + } else + cur->udp.sent += datalen; + + return 1; +} + +int open_udp_socket(const struct arguments *args, + const struct udp_session *cur, const struct allowed *redirect) { + int sock; + int version; + if (redirect == NULL) + version = cur->version; + else + version = (strstr(redirect->raddr, ":") == NULL ? 4 : 6); + + // Get UDP socket + sock = socket(version == 4 ? PF_INET : PF_INET6, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) { + log_android(ANDROID_LOG_ERROR, "UDP socket error %d: %s", errno, strerror(errno)); + return -1; + } + + // Protect socket + if (protect_socket(args, sock) < 0) + return -1; + + // Check for broadcast/multicast + if (cur->version == 4) { + uint32_t broadcast4 = INADDR_BROADCAST; + if (memcmp(&cur->daddr.ip4, &broadcast4, sizeof(broadcast4)) == 0) { + log_android(ANDROID_LOG_WARN, "UDP4 broadcast"); + int on = 1; + if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on))) + log_android(ANDROID_LOG_ERROR, "UDP setsockopt SO_BROADCAST error %d: %s", + errno, strerror(errno)); + } + } else { + // http://man7.org/linux/man-pages/man7/ipv6.7.html + if (*((uint8_t *) &cur->daddr.ip6) == 0xFF) { + log_android(ANDROID_LOG_WARN, "UDP6 broadcast"); + + int loop = 1; // true + if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &loop, sizeof(loop))) + log_android(ANDROID_LOG_ERROR, + "UDP setsockopt IPV6_MULTICAST_LOOP error %d: %s", + errno, strerror(errno)); + + int ttl = -1; // route default + if (setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &ttl, sizeof(ttl))) + log_android(ANDROID_LOG_ERROR, + "UDP setsockopt IPV6_MULTICAST_HOPS error %d: %s", + errno, strerror(errno)); + + struct ipv6_mreq mreq6; + memcpy(&mreq6.ipv6mr_multiaddr, &cur->daddr.ip6, sizeof(struct in6_addr)); + mreq6.ipv6mr_interface = INADDR_ANY; + if (setsockopt(sock, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, &mreq6, sizeof(mreq6))) + log_android(ANDROID_LOG_ERROR, + "UDP setsockopt IPV6_ADD_MEMBERSHIP error %d: %s", + errno, strerror(errno)); + } + } + + return sock; +} + +ssize_t write_udp(const struct arguments *args, const struct udp_session *cur, + uint8_t *data, size_t datalen) { + size_t len; + u_int8_t *buffer; + struct udphdr *udp; + uint16_t csum; + char source[INET6_ADDRSTRLEN + 1]; + char dest[INET6_ADDRSTRLEN + 1]; + + // Build packet + if (cur->version == 4) { + len = sizeof(struct iphdr) + sizeof(struct udphdr) + datalen; + buffer = ng_malloc(len, "udp write4"); + struct iphdr *ip4 = (struct iphdr *) buffer; + udp = (struct udphdr *) (buffer + sizeof(struct iphdr)); + if (datalen) + memcpy(buffer + sizeof(struct iphdr) + sizeof(struct udphdr), data, datalen); + + // Build IP4 header + memset(ip4, 0, sizeof(struct iphdr)); + ip4->version = 4; + ip4->ihl = sizeof(struct iphdr) >> 2; + ip4->tot_len = htons(len); + ip4->ttl = IPDEFTTL; + ip4->protocol = IPPROTO_UDP; + ip4->saddr = cur->daddr.ip4; + ip4->daddr = cur->saddr.ip4; + + // Calculate IP4 checksum + ip4->check = ~calc_checksum(0, (uint8_t *) ip4, sizeof(struct iphdr)); + + // Calculate UDP4 checksum + struct ippseudo pseudo; + memset(&pseudo, 0, sizeof(struct ippseudo)); + pseudo.ippseudo_src.s_addr = (__be32) ip4->saddr; + pseudo.ippseudo_dst.s_addr = (__be32) ip4->daddr; + pseudo.ippseudo_p = ip4->protocol; + pseudo.ippseudo_len = htons(sizeof(struct udphdr) + datalen); + + csum = calc_checksum(0, (uint8_t *) &pseudo, sizeof(struct ippseudo)); + } else { + len = sizeof(struct ip6_hdr) + sizeof(struct udphdr) + datalen; + buffer = ng_malloc(len, "udp write6"); + struct ip6_hdr *ip6 = (struct ip6_hdr *) buffer; + udp = (struct udphdr *) (buffer + sizeof(struct ip6_hdr)); + if (datalen) + memcpy(buffer + sizeof(struct ip6_hdr) + sizeof(struct udphdr), data, datalen); + + // Build IP6 header + memset(ip6, 0, sizeof(struct ip6_hdr)); + ip6->ip6_ctlun.ip6_un1.ip6_un1_flow = 0; + ip6->ip6_ctlun.ip6_un1.ip6_un1_plen = htons(len - sizeof(struct ip6_hdr)); + ip6->ip6_ctlun.ip6_un1.ip6_un1_nxt = IPPROTO_UDP; + ip6->ip6_ctlun.ip6_un1.ip6_un1_hlim = IPDEFTTL; + ip6->ip6_ctlun.ip6_un2_vfc = IPV6_VERSION; + memcpy(&(ip6->ip6_src), &cur->daddr.ip6, 16); + memcpy(&(ip6->ip6_dst), &cur->saddr.ip6, 16); + + // Calculate UDP6 checksum + struct ip6_hdr_pseudo pseudo; + memset(&pseudo, 0, sizeof(struct ip6_hdr_pseudo)); + memcpy(&pseudo.ip6ph_src, &ip6->ip6_dst, 16); + memcpy(&pseudo.ip6ph_dst, &ip6->ip6_src, 16); + pseudo.ip6ph_len = ip6->ip6_ctlun.ip6_un1.ip6_un1_plen; + pseudo.ip6ph_nxt = ip6->ip6_ctlun.ip6_un1.ip6_un1_nxt; + + csum = calc_checksum(0, (uint8_t *) &pseudo, sizeof(struct ip6_hdr_pseudo)); + } + + // Build UDP header + memset(udp, 0, sizeof(struct udphdr)); + udp->source = cur->dest; + udp->dest = cur->source; + udp->len = htons(sizeof(struct udphdr) + datalen); + + // Continue checksum + csum = calc_checksum(csum, (uint8_t *) udp, sizeof(struct udphdr)); + csum = calc_checksum(csum, data, datalen); + udp->check = ~csum; + + inet_ntop(cur->version == 4 ? AF_INET : AF_INET6, + (cur->version == 4 ? (const void *) &cur->saddr.ip4 : (const void *) &cur->saddr.ip6), + source, + sizeof(source)); + inet_ntop(cur->version == 4 ? AF_INET : AF_INET6, + (cur->version == 4 ? (const void *) &cur->daddr.ip4 : (const void *) &cur->daddr.ip6), + dest, + sizeof(dest)); + + // Send packet + log_android(ANDROID_LOG_DEBUG, + "UDP sending to tun %d from %s/%u to %s/%u data %u", + args->tun, dest, ntohs(cur->dest), source, ntohs(cur->source), len); + + ssize_t res = write(args->tun, buffer, len); + + // Write PCAP record + if (res >= 0) { + if (pcap_file != NULL) + write_pcap_rec(buffer, (size_t) res); + } else + log_android(ANDROID_LOG_WARN, "UDP write error %d: %s", errno, strerror(errno)); + + ng_free(buffer, __FILE__, __LINE__); + + if (res != len) { + log_android(ANDROID_LOG_ERROR, "write %d/%d", res, len); + return -1; + } + + return res; +} diff --git a/NetGuard/app/src/main/main/jni/netguard/util.c b/NetGuard/app/src/main/main/jni/netguard/util.c new file mode 100644 index 0000000..c81bdfe --- /dev/null +++ b/NetGuard/app/src/main/main/jni/netguard/util.c @@ -0,0 +1,182 @@ +/* + This file is part of NetGuard. + + NetGuard is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + NetGuard is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with NetGuard. If not, see . + + Copyright 2015-2019 by Marcel Bokhorst (M66B) +*/ + +#include "netguard.h" + +extern int loglevel; + +uint16_t calc_checksum(uint16_t start, const uint8_t *buffer, size_t length) { + register uint32_t sum = start; + register uint16_t *buf = (uint16_t *) buffer; + register size_t len = length; + + while (len > 1) { + sum += *buf++; + len -= 2; + } + + if (len > 0) + sum += *((uint8_t *) buf); + + while (sum >> 16) + sum = (sum & 0xFFFF) + (sum >> 16); + + return (uint16_t) sum; +} + +int compare_u32(uint32_t s1, uint32_t s2) { + // https://tools.ietf.org/html/rfc1982 + if (s1 == s2) + return 0; + + uint32_t i1 = s1; + uint32_t i2 = s2; + if ((i1 < i2 && i2 - i1 < 0x7FFFFFFF) || + (i1 > i2 && i1 - i2 > 0x7FFFFFFF)) + return -1; + else + return 1; +} + +int sdk_int(JNIEnv *env) { + jclass clsVersion = jniFindClass(env, "android/os/Build$VERSION"); + jfieldID fid = (*env)->GetStaticFieldID(env, clsVersion, "SDK_INT", "I"); + return (*env)->GetStaticIntField(env, clsVersion, fid); +} + +void log_android(int prio, const char *fmt, ...) { + if (prio >= loglevel) { + char line[1024]; + va_list argptr; + va_start(argptr, fmt); + vsprintf(line, fmt, argptr); + __android_log_print(prio, TAG, "%s", line); + va_end(argptr); + } +} + +uint8_t char2nible(const char c) { + if (c >= '0' && c <= '9') return (uint8_t) (c - '0'); + if (c >= 'a' && c <= 'f') return (uint8_t) ((c - 'a') + 10); + if (c >= 'A' && c <= 'F') return (uint8_t) ((c - 'A') + 10); + return 255; +} + +void hex2bytes(const char *hex, uint8_t *buffer) { + size_t len = strlen(hex); + for (int i = 0; i < len; i += 2) + buffer[i / 2] = (char2nible(hex[i]) << 4) | char2nible(hex[i + 1]); +} + +char *trim(char *str) { + while (isspace(*str)) + str++; + if (*str == 0) + return str; + + char *end = str + strlen(str) - 1; + while (end > str && isspace(*end)) + end--; + *(end + 1) = 0; + return str; +} + +const char *strstate(const int state) { + switch (state) { + case TCP_ESTABLISHED: + return "ESTABLISHED"; + case TCP_SYN_SENT: + return "SYN_SENT"; + case TCP_SYN_RECV: + return "SYN_RECV"; + case TCP_FIN_WAIT1: + return "FIN_WAIT1"; + case TCP_FIN_WAIT2: + return "FIN_WAIT2"; + case TCP_TIME_WAIT: + return "TIME_WAIT"; + case TCP_CLOSE: + return "CLOSE"; + case TCP_CLOSE_WAIT: + return "CLOSE_WAIT"; + case TCP_LAST_ACK: + return "LAST_ACK"; + case TCP_LISTEN: + return "LISTEN"; + case TCP_CLOSING: + return "CLOSING"; + default: + return "UNKNOWN"; + } +} + +char *hex(const u_int8_t *data, const size_t len) { + char hex_str[] = "0123456789ABCDEF"; + + char *hexout; + hexout = (char *) ng_malloc(len * 3 + 1, "hex"); // TODO free + + for (size_t i = 0; i < len; i++) { + hexout[i * 3 + 0] = hex_str[(data[i] >> 4) & 0x0F]; + hexout[i * 3 + 1] = hex_str[(data[i]) & 0x0F]; + hexout[i * 3 + 2] = ' '; + } + hexout[len * 3] = 0; + + return hexout; +} + +int32_t get_local_port(const int sock) { + struct sockaddr_in sin; + socklen_t len = sizeof(sin); + if (getsockname(sock, (struct sockaddr *) &sin, &len) < 0) { + log_android(ANDROID_LOG_ERROR, "getsockname error %d: %s", errno, strerror(errno)); + return -1; + } else + return ntohs(sin.sin_port); +} + +int is_event(int fd, short event) { + struct pollfd p; + p.fd = fd; + p.events = event; + p.revents = 0; + int r = poll(&p, 1, 0); + if (r < 0) { + log_android(ANDROID_LOG_ERROR, "poll readable error %d: %s", errno, strerror(errno)); + return 0; + } else if (r == 0) + return 0; + else + return (p.revents & event); +} + +int is_readable(int fd) { + return is_event(fd, POLLIN); +} + +int is_writable(int fd) { + return is_event(fd, POLLOUT); +} + +long long get_ms() { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000LL + ts.tv_nsec / 1e6; +} diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_add_circle_outline_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_add_circle_outline_white_24dp.png new file mode 100644 index 0000000..67e53a6 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_add_circle_outline_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_attach_money_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_attach_money_black_24dp.png new file mode 100644 index 0000000..ad0f76c Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_attach_money_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_attach_money_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_attach_money_white_24dp.png new file mode 100644 index 0000000..ee833c6 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_attach_money_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_check_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_check_white_24dp.png new file mode 100644 index 0000000..729f290 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_check_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_close_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..ceb1a1e Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_close_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_cloud_upload_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_cloud_upload_white_24dp.png new file mode 100644 index 0000000..5e0b464 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_cloud_upload_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_delete_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_delete_black_24dp.png new file mode 100644 index 0000000..dbbb602 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_delete_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_delete_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000..4a9f769 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_delete_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_equalizer_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_equalizer_white_24dp.png new file mode 100644 index 0000000..aa343b5 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_equalizer_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_equalizer_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_equalizer_white_24dp_60.png new file mode 100644 index 0000000..933a198 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_equalizer_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_error_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_error_white_24dp.png new file mode 100644 index 0000000..69cbb1e Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_error_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_less_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_less_black_24dp.png new file mode 100644 index 0000000..57139a7 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_less_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_less_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_less_white_24dp.png new file mode 100644 index 0000000..dea8988 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_less_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_more_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_more_black_24dp.png new file mode 100644 index 0000000..9625f14 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_more_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_more_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_more_white_24dp.png new file mode 100644 index 0000000..022e057 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_more_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_file_download_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_file_download_white_24dp.png new file mode 100644 index 0000000..c8a2039 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_file_download_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_filter_list_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_filter_list_white_24dp.png new file mode 100644 index 0000000..7e8a6b5 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_filter_list_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_filter_list_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_filter_list_white_24dp_60.png new file mode 100644 index 0000000..8c2d618 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_filter_list_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_hourglass_empty_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_hourglass_empty_black_24dp.png new file mode 100644 index 0000000..22fe56d Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_hourglass_empty_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_hourglass_empty_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_hourglass_empty_white_24dp.png new file mode 100644 index 0000000..5b8b684 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_hourglass_empty_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_launch_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_launch_black_24dp.png new file mode 100644 index 0000000..4cfcf84 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_launch_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_launch_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_launch_white_24dp.png new file mode 100644 index 0000000..49726bc Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_launch_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_lock_open_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_lock_open_white_24dp.png new file mode 100644 index 0000000..6bae68f Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_lock_open_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_lock_outline_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_lock_outline_white_24dp.png new file mode 100644 index 0000000..e53f541 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_lock_outline_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_lock_outline_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_lock_outline_white_24dp_60.png new file mode 100644 index 0000000..0e8a7cf Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_lock_outline_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_pause_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_pause_black_24dp.png new file mode 100644 index 0000000..3539b4e Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_pause_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_pause_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000..4d2ea05 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_pause_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_perm_data_setting_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_perm_data_setting_black_24dp.png new file mode 100644 index 0000000..1643436 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_perm_data_setting_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_perm_data_setting_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_perm_data_setting_white_24dp.png new file mode 100644 index 0000000..ba7e1fe Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_perm_data_setting_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_perm_identity_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_perm_identity_white_24dp.png new file mode 100644 index 0000000..78d43f0 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_perm_identity_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_play_arrow_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_play_arrow_black_24dp.png new file mode 100644 index 0000000..e9c288c Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_play_arrow_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png new file mode 100644 index 0000000..57c9fa5 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_search_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_search_white_24dp.png new file mode 100644 index 0000000..bbfbc96 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_search_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_security_color_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_security_color_24dp.png new file mode 100644 index 0000000..73e31c1 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_security_color_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_security_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_security_white_24dp.png new file mode 100644 index 0000000..262800a Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_security_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_security_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_security_white_24dp_60.png new file mode 100644 index 0000000..d559fee Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_security_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_settings_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000..acf1ddf Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_settings_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_settings_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_settings_white_24dp.png new file mode 100644 index 0000000..97ded33 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_settings_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_shopping_cart_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_shopping_cart_black_24dp.png new file mode 100644 index 0000000..c2422ce Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_shopping_cart_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_shopping_cart_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_shopping_cart_white_24dp.png new file mode 100644 index 0000000..a68ce43 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_shopping_cart_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_cellular_4_bar_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_cellular_4_bar_white_24dp.png new file mode 100644 index 0000000..1765e94 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_cellular_4_bar_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_cellular_off_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_cellular_off_white_24dp.png new file mode 100644 index 0000000..12704b3 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_cellular_off_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_wifi_4_bar_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_wifi_4_bar_white_24dp.png new file mode 100644 index 0000000..5a53192 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_wifi_4_bar_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_wifi_off_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_wifi_off_white_24dp.png new file mode 100644 index 0000000..996d8a3 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_wifi_off_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-hdpi/ic_sort_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_sort_white_24dp.png new file mode 100644 index 0000000..55a429b Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-hdpi/ic_sort_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_add_circle_outline_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_add_circle_outline_white_24dp.png new file mode 100644 index 0000000..803fc97 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_add_circle_outline_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_attach_money_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_attach_money_black_24dp.png new file mode 100644 index 0000000..2ae5e6d Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_attach_money_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_attach_money_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_attach_money_white_24dp.png new file mode 100644 index 0000000..707d167 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_attach_money_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_check_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_check_white_24dp.png new file mode 100644 index 0000000..dfcb55d Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_check_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_close_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..af7f828 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_close_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_cloud_upload_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_cloud_upload_white_24dp.png new file mode 100644 index 0000000..aa64062 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_cloud_upload_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_delete_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_delete_black_24dp.png new file mode 100644 index 0000000..999aa4c Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_delete_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_delete_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000..e2f5f35 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_delete_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_equalizer_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_equalizer_white_24dp.png new file mode 100644 index 0000000..bc600d3 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_equalizer_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_equalizer_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_equalizer_white_24dp_60.png new file mode 100644 index 0000000..90ef1df Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_equalizer_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_error_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_error_white_24dp.png new file mode 100644 index 0000000..ca148fc Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_error_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_less_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_less_black_24dp.png new file mode 100644 index 0000000..08c16a3 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_less_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_less_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_less_white_24dp.png new file mode 100644 index 0000000..a2e4baa Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_less_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_more_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_more_black_24dp.png new file mode 100644 index 0000000..feb85a7 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_more_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_more_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_more_white_24dp.png new file mode 100644 index 0000000..910bb2a Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_more_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_file_download_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_file_download_white_24dp.png new file mode 100644 index 0000000..d400472 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_file_download_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_filter_list_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_filter_list_white_24dp.png new file mode 100644 index 0000000..59a2ec7 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_filter_list_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_filter_list_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_filter_list_white_24dp_60.png new file mode 100644 index 0000000..7b75cca Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_filter_list_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_hourglass_empty_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_hourglass_empty_black_24dp.png new file mode 100644 index 0000000..ce8b8b0 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_hourglass_empty_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_hourglass_empty_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_hourglass_empty_white_24dp.png new file mode 100644 index 0000000..616df09 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_hourglass_empty_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_launch_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_launch_black_24dp.png new file mode 100644 index 0000000..cc2892d Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_launch_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_launch_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_launch_white_24dp.png new file mode 100644 index 0000000..474cc80 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_launch_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_lock_open_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_lock_open_white_24dp.png new file mode 100644 index 0000000..3f47b54 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_lock_open_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_lock_outline_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_lock_outline_white_24dp.png new file mode 100644 index 0000000..13f432f Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_lock_outline_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_lock_outline_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_lock_outline_white_24dp_60.png new file mode 100644 index 0000000..19c3962 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_lock_outline_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_pause_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_pause_black_24dp.png new file mode 100644 index 0000000..6145664 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_pause_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_pause_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000..2272d47 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_pause_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_perm_data_setting_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_perm_data_setting_black_24dp.png new file mode 100644 index 0000000..6686406 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_perm_data_setting_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_perm_data_setting_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_perm_data_setting_white_24dp.png new file mode 100644 index 0000000..b2249e0 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_perm_data_setting_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_perm_identity_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_perm_identity_white_24dp.png new file mode 100644 index 0000000..f00e3f3 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_perm_identity_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_play_arrow_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_play_arrow_black_24dp.png new file mode 100644 index 0000000..d78c57b Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_play_arrow_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png new file mode 100644 index 0000000..c61e948 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_search_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_search_white_24dp.png new file mode 100644 index 0000000..faefc59 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_search_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_security_color_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_security_color_24dp.png new file mode 100644 index 0000000..d023f2f Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_security_color_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_security_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_security_white_24dp.png new file mode 100644 index 0000000..44ee734 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_security_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_security_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_security_white_24dp_60.png new file mode 100644 index 0000000..93e76da Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_security_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_settings_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000..c59419c Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_settings_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_settings_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_settings_white_24dp.png new file mode 100644 index 0000000..8909c35 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_settings_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_shopping_cart_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_shopping_cart_black_24dp.png new file mode 100644 index 0000000..f56585b Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_shopping_cart_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_shopping_cart_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_shopping_cart_white_24dp.png new file mode 100644 index 0000000..9d9fdbb Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_shopping_cart_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_cellular_4_bar_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_cellular_4_bar_white_24dp.png new file mode 100644 index 0000000..8d76fd4 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_cellular_4_bar_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_cellular_off_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_cellular_off_white_24dp.png new file mode 100644 index 0000000..eaaa180 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_cellular_off_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_wifi_4_bar_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_wifi_4_bar_white_24dp.png new file mode 100644 index 0000000..dd5a42f Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_wifi_4_bar_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_wifi_off_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_wifi_off_white_24dp.png new file mode 100644 index 0000000..5295ecd Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_wifi_off_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-mdpi/ic_sort_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_sort_white_24dp.png new file mode 100644 index 0000000..3d84a44 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-mdpi/ic_sort_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_add_circle_outline_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_add_circle_outline_white_24dp.png new file mode 100644 index 0000000..ac94274 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_add_circle_outline_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_attach_money_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_attach_money_black_24dp.png new file mode 100644 index 0000000..c94dc6a Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_attach_money_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_attach_money_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_attach_money_white_24dp.png new file mode 100644 index 0000000..20efc6c Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_attach_money_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_check_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_check_white_24dp.png new file mode 100644 index 0000000..3b2b65d Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_check_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_close_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..b7c7ffd Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_close_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_cloud_upload_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_cloud_upload_white_24dp.png new file mode 100644 index 0000000..a9602d1 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_cloud_upload_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_delete_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_delete_black_24dp.png new file mode 100644 index 0000000..796ccd2 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_delete_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_delete_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000..388b5b0 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_delete_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_equalizer_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_equalizer_white_24dp.png new file mode 100644 index 0000000..40c572c Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_equalizer_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_equalizer_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_equalizer_white_24dp_60.png new file mode 100644 index 0000000..1d849fc Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_equalizer_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_error_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_error_white_24dp.png new file mode 100644 index 0000000..9829698 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_error_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_less_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_less_black_24dp.png new file mode 100644 index 0000000..323360e Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_less_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_less_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_less_white_24dp.png new file mode 100644 index 0000000..ae36d91 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_less_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_more_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_more_black_24dp.png new file mode 100644 index 0000000..d3ee65e Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_more_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_more_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_more_white_24dp.png new file mode 100644 index 0000000..c42e2a0 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_more_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_file_download_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_file_download_white_24dp.png new file mode 100644 index 0000000..f53cc0c Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_file_download_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_filter_list_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_filter_list_white_24dp.png new file mode 100644 index 0000000..9416c70 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_filter_list_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_filter_list_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_filter_list_white_24dp_60.png new file mode 100644 index 0000000..f8c9122 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_filter_list_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_hourglass_empty_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_hourglass_empty_black_24dp.png new file mode 100644 index 0000000..e63d9cd Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_hourglass_empty_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_hourglass_empty_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_hourglass_empty_white_24dp.png new file mode 100644 index 0000000..35b7eef Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_hourglass_empty_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_launch_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_launch_black_24dp.png new file mode 100644 index 0000000..4ebc46b Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_launch_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_launch_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_launch_white_24dp.png new file mode 100644 index 0000000..a2caac5 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_launch_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png new file mode 100644 index 0000000..cbe9e1c Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_lock_outline_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_lock_outline_white_24dp.png new file mode 100644 index 0000000..e2d1091 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_lock_outline_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_lock_outline_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_lock_outline_white_24dp_60.png new file mode 100644 index 0000000..0b61001 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_lock_outline_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_pause_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_pause_black_24dp.png new file mode 100644 index 0000000..74068ea Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_pause_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_pause_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000..f49aed7 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_pause_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_perm_data_setting_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_perm_data_setting_black_24dp.png new file mode 100644 index 0000000..a8a97f2 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_perm_data_setting_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_perm_data_setting_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_perm_data_setting_white_24dp.png new file mode 100644 index 0000000..e444922 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_perm_data_setting_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_perm_identity_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_perm_identity_white_24dp.png new file mode 100644 index 0000000..e344ea9 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_perm_identity_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_play_arrow_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_play_arrow_black_24dp.png new file mode 100644 index 0000000..f208795 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_play_arrow_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png new file mode 100644 index 0000000..a3c80e7 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_search_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_search_white_24dp.png new file mode 100644 index 0000000..bfc3e39 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_search_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_security_color_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_security_color_24dp.png new file mode 100644 index 0000000..6f25407 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_security_color_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_security_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_security_white_24dp.png new file mode 100644 index 0000000..7e306c3 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_security_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_security_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_security_white_24dp_60.png new file mode 100644 index 0000000..8ed175d Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_security_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_settings_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000..e84e188 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_settings_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_settings_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_settings_white_24dp.png new file mode 100644 index 0000000..5caedc8 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_settings_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_shopping_cart_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_shopping_cart_black_24dp.png new file mode 100644 index 0000000..3da6027 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_shopping_cart_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_shopping_cart_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_shopping_cart_white_24dp.png new file mode 100644 index 0000000..16b6cd0 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_shopping_cart_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_cellular_4_bar_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_cellular_4_bar_white_24dp.png new file mode 100644 index 0000000..62501b0 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_cellular_4_bar_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_cellular_off_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_cellular_off_white_24dp.png new file mode 100644 index 0000000..8ed814e Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_cellular_off_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_wifi_4_bar_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_wifi_4_bar_white_24dp.png new file mode 100644 index 0000000..28b5afa Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_wifi_4_bar_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_wifi_off_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_wifi_off_white_24dp.png new file mode 100644 index 0000000..480145c Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_wifi_off_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_sort_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_sort_white_24dp.png new file mode 100644 index 0000000..6d4af1b Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xhdpi/ic_sort_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_add_circle_outline_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_add_circle_outline_white_24dp.png new file mode 100644 index 0000000..fdad9ba Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_add_circle_outline_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_attach_money_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_attach_money_black_24dp.png new file mode 100644 index 0000000..a9478a6 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_attach_money_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_attach_money_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_attach_money_white_24dp.png new file mode 100644 index 0000000..d4bde81 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_attach_money_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_check_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_check_white_24dp.png new file mode 100644 index 0000000..2c2ad77 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_check_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_close_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..6b717e0 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_close_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_cloud_upload_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_cloud_upload_white_24dp.png new file mode 100644 index 0000000..3ff57ad Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_cloud_upload_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_delete_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_delete_black_24dp.png new file mode 100644 index 0000000..6d7cb81 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_delete_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_delete_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000..3fcdfdb Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_delete_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_equalizer_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_equalizer_white_24dp.png new file mode 100644 index 0000000..d603c4f Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_equalizer_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_equalizer_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_equalizer_white_24dp_60.png new file mode 100644 index 0000000..f23c2db Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_equalizer_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_error_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_error_white_24dp.png new file mode 100644 index 0000000..abe2573 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_error_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_less_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_less_black_24dp.png new file mode 100644 index 0000000..ee92f4e Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_less_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_less_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_less_white_24dp.png new file mode 100644 index 0000000..62fc386 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_less_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_more_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_more_black_24dp.png new file mode 100644 index 0000000..5cd142c Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_more_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_more_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_more_white_24dp.png new file mode 100644 index 0000000..dbc0b20 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_more_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_file_download_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_file_download_white_24dp.png new file mode 100644 index 0000000..78aa591 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_file_download_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_filter_list_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_filter_list_white_24dp.png new file mode 100644 index 0000000..1263ae8 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_filter_list_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_filter_list_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_filter_list_white_24dp_60.png new file mode 100644 index 0000000..cf26ba5 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_filter_list_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_hourglass_empty_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_hourglass_empty_black_24dp.png new file mode 100644 index 0000000..47efe9d Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_hourglass_empty_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_hourglass_empty_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_hourglass_empty_white_24dp.png new file mode 100644 index 0000000..8f67bc6 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_hourglass_empty_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_launch_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_launch_black_24dp.png new file mode 100644 index 0000000..4305f56 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_launch_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_launch_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_launch_white_24dp.png new file mode 100644 index 0000000..2ed5b0e Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_launch_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png new file mode 100644 index 0000000..1d1b0f4 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_lock_outline_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_lock_outline_white_24dp.png new file mode 100644 index 0000000..bd2cf4d Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_lock_outline_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_lock_outline_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_lock_outline_white_24dp_60.png new file mode 100644 index 0000000..16e2fc3 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_lock_outline_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_pause_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_pause_black_24dp.png new file mode 100644 index 0000000..bb707ea Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_pause_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_pause_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000..7192ad4 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_pause_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_perm_data_setting_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_perm_data_setting_black_24dp.png new file mode 100644 index 0000000..d9e712d Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_perm_data_setting_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_perm_data_setting_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_perm_data_setting_white_24dp.png new file mode 100644 index 0000000..60257de Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_perm_data_setting_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_perm_identity_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_perm_identity_white_24dp.png new file mode 100644 index 0000000..b08402a Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_perm_identity_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_play_arrow_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_play_arrow_black_24dp.png new file mode 100644 index 0000000..5345ee3 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_play_arrow_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png new file mode 100644 index 0000000..547ef30 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_search_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_search_white_24dp.png new file mode 100644 index 0000000..abbb989 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_search_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_security_color_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_security_color_24dp.png new file mode 100644 index 0000000..c0950f3 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_security_color_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_security_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_security_white_24dp.png new file mode 100644 index 0000000..7bcb2fd Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_security_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_security_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_security_white_24dp_60.png new file mode 100644 index 0000000..e8f3b25 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_security_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_settings_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000..3023ff8 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_settings_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_settings_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_settings_white_24dp.png new file mode 100644 index 0000000..eabb0a2 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_settings_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_shopping_cart_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_shopping_cart_black_24dp.png new file mode 100644 index 0000000..f9121f5 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_shopping_cart_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_shopping_cart_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_shopping_cart_white_24dp.png new file mode 100644 index 0000000..bcfb493 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_shopping_cart_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_cellular_4_bar_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_cellular_4_bar_white_24dp.png new file mode 100644 index 0000000..b191b9d Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_cellular_4_bar_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_cellular_off_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_cellular_off_white_24dp.png new file mode 100644 index 0000000..dedee39 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_cellular_off_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_wifi_4_bar_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_wifi_4_bar_white_24dp.png new file mode 100644 index 0000000..f4105ec Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_wifi_4_bar_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_wifi_off_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_wifi_off_white_24dp.png new file mode 100644 index 0000000..f2023bf Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_wifi_off_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_sort_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_sort_white_24dp.png new file mode 100644 index 0000000..b8ef105 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_sort_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_add_circle_outline_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_add_circle_outline_white_24dp.png new file mode 100644 index 0000000..573aefe Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_add_circle_outline_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_attach_money_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_attach_money_black_24dp.png new file mode 100644 index 0000000..49956bf Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_attach_money_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_attach_money_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_attach_money_white_24dp.png new file mode 100644 index 0000000..f3c5685 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_attach_money_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_check_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_check_white_24dp.png new file mode 100644 index 0000000..d670618 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_check_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_close_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_close_white_24dp.png new file mode 100644 index 0000000..3964192 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_close_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_cloud_upload_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_cloud_upload_white_24dp.png new file mode 100644 index 0000000..2180f73 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_cloud_upload_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png new file mode 100644 index 0000000..f2b75c3 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png new file mode 100644 index 0000000..8d322aa Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_equalizer_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_equalizer_white_24dp.png new file mode 100644 index 0000000..fa8d522 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_equalizer_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_equalizer_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_equalizer_white_24dp_60.png new file mode 100644 index 0000000..31cee03 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_equalizer_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_error_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_error_white_24dp.png new file mode 100644 index 0000000..830fb7e Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_error_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_less_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_less_black_24dp.png new file mode 100644 index 0000000..99c6e3e Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_less_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_less_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_less_white_24dp.png new file mode 100644 index 0000000..4261551 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_less_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_more_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_more_black_24dp.png new file mode 100644 index 0000000..ad852e3 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_more_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_more_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_more_white_24dp.png new file mode 100644 index 0000000..2859a6f Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_more_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_file_download_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_file_download_white_24dp.png new file mode 100644 index 0000000..ded5652 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_file_download_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_filter_list_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_filter_list_white_24dp.png new file mode 100644 index 0000000..cb2207f Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_filter_list_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_filter_list_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_filter_list_white_24dp_60.png new file mode 100644 index 0000000..e66a5d9 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_filter_list_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_hourglass_empty_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_hourglass_empty_black_24dp.png new file mode 100644 index 0000000..74b4072 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_hourglass_empty_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_hourglass_empty_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_hourglass_empty_white_24dp.png new file mode 100644 index 0000000..b0bda26 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_hourglass_empty_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_launch_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_launch_black_24dp.png new file mode 100644 index 0000000..4eb7b16 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_launch_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_launch_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_launch_white_24dp.png new file mode 100644 index 0000000..bab25f3 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_launch_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_lock_open_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_lock_open_white_24dp.png new file mode 100644 index 0000000..8ab4107 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_lock_open_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_lock_outline_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_lock_outline_white_24dp.png new file mode 100644 index 0000000..de1d564 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_lock_outline_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_lock_outline_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_lock_outline_white_24dp_60.png new file mode 100644 index 0000000..9f3f1f7 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_lock_outline_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_pause_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_pause_black_24dp.png new file mode 100644 index 0000000..792104f Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_pause_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png new file mode 100644 index 0000000..660ac65 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_perm_data_setting_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_perm_data_setting_black_24dp.png new file mode 100644 index 0000000..a451d53 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_perm_data_setting_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_perm_data_setting_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_perm_data_setting_white_24dp.png new file mode 100644 index 0000000..1916017 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_perm_data_setting_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_perm_identity_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_perm_identity_white_24dp.png new file mode 100644 index 0000000..9717ab4 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_perm_identity_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_play_arrow_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_play_arrow_black_24dp.png new file mode 100644 index 0000000..d12d495 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_play_arrow_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_play_arrow_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_play_arrow_white_24dp.png new file mode 100644 index 0000000..be5c062 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_play_arrow_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_search_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_search_white_24dp.png new file mode 100644 index 0000000..dd5adfc Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_search_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_security_color_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_security_color_24dp.png new file mode 100644 index 0000000..194e70a Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_security_color_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_security_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_security_white_24dp.png new file mode 100644 index 0000000..b1eddbd Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_security_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_security_white_24dp_60.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_security_white_24dp_60.png new file mode 100644 index 0000000..f226c1b Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_security_white_24dp_60.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png new file mode 100644 index 0000000..476d5c9 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png new file mode 100644 index 0000000..507c5ed Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_shopping_cart_black_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_shopping_cart_black_24dp.png new file mode 100644 index 0000000..6edc956 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_shopping_cart_black_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_shopping_cart_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_shopping_cart_white_24dp.png new file mode 100644 index 0000000..8c3323e Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_shopping_cart_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_cellular_4_bar_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_cellular_4_bar_white_24dp.png new file mode 100644 index 0000000..4a3ac28 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_cellular_4_bar_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_cellular_off_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_cellular_off_white_24dp.png new file mode 100644 index 0000000..b6adb4e Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_cellular_off_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_wifi_4_bar_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_wifi_4_bar_white_24dp.png new file mode 100644 index 0000000..58a4f9c Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_wifi_4_bar_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_wifi_off_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_wifi_off_white_24dp.png new file mode 100644 index 0000000..6df209e Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_wifi_off_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_sort_white_24dp.png b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_sort_white_24dp.png new file mode 100644 index 0000000..4796c33 Binary files /dev/null and b/NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_sort_white_24dp.png differ diff --git a/NetGuard/app/src/main/main/res/drawable/baseline_file_copy_24.xml b/NetGuard/app/src/main/main/res/drawable/baseline_file_copy_24.xml new file mode 100644 index 0000000..f52fd86 --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/baseline_file_copy_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/NetGuard/app/src/main/main/res/drawable/expander_black.xml b/NetGuard/app/src/main/main/res/drawable/expander_black.xml new file mode 100644 index 0000000..c9b36b1 --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/expander_black.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/expander_white.xml b/NetGuard/app/src/main/main/res/drawable/expander_white.xml new file mode 100644 index 0000000..7a2a352 --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/expander_white.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/host_allowed.xml b/NetGuard/app/src/main/main/res/drawable/host_allowed.xml new file mode 100644 index 0000000..5ecc890 --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/host_allowed.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/host_blocked.xml b/NetGuard/app/src/main/main/res/drawable/host_blocked.xml new file mode 100644 index 0000000..7ed46a8 --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/host_blocked.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/lockdown.xml b/NetGuard/app/src/main/main/res/drawable/lockdown.xml new file mode 100644 index 0000000..bcb8f1a --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/lockdown.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/NetGuard/app/src/main/main/res/drawable/lockdown_disabled.xml b/NetGuard/app/src/main/main/res/drawable/lockdown_disabled.xml new file mode 100644 index 0000000..3c12ace --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/lockdown_disabled.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/lockdown_off.xml b/NetGuard/app/src/main/main/res/drawable/lockdown_off.xml new file mode 100644 index 0000000..b411d82 --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/lockdown_off.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/lockdown_on.xml b/NetGuard/app/src/main/main/res/drawable/lockdown_on.xml new file mode 100644 index 0000000..dca4c4d --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/lockdown_on.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/other.xml b/NetGuard/app/src/main/main/res/drawable/other.xml new file mode 100644 index 0000000..8ddba52 --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/other.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/other_off.xml b/NetGuard/app/src/main/main/res/drawable/other_off.xml new file mode 100644 index 0000000..e3bf1ed --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/other_off.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/other_off_disabled.xml b/NetGuard/app/src/main/main/res/drawable/other_off_disabled.xml new file mode 100644 index 0000000..6e1f626 --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/other_off_disabled.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/other_on.xml b/NetGuard/app/src/main/main/res/drawable/other_on.xml new file mode 100644 index 0000000..1ee225f --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/other_on.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/other_on_disabled.xml b/NetGuard/app/src/main/main/res/drawable/other_on_disabled.xml new file mode 100644 index 0000000..3ea13c9 --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/other_on_disabled.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/screen.xml b/NetGuard/app/src/main/main/res/drawable/screen.xml new file mode 100644 index 0000000..43e91df --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/screen.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/screen_on.xml b/NetGuard/app/src/main/main/res/drawable/screen_on.xml new file mode 100644 index 0000000..5abceeb --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/screen_on.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/screen_on_disabled.xml b/NetGuard/app/src/main/main/res/drawable/screen_on_disabled.xml new file mode 100644 index 0000000..dec83be --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/screen_on_disabled.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/wifi.xml b/NetGuard/app/src/main/main/res/drawable/wifi.xml new file mode 100644 index 0000000..f042007 --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/wifi.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/wifi_off.xml b/NetGuard/app/src/main/main/res/drawable/wifi_off.xml new file mode 100644 index 0000000..27ffd67 --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/wifi_off.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/wifi_off_disabled.xml b/NetGuard/app/src/main/main/res/drawable/wifi_off_disabled.xml new file mode 100644 index 0000000..f006648 --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/wifi_off_disabled.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/wifi_on.xml b/NetGuard/app/src/main/main/res/drawable/wifi_on.xml new file mode 100644 index 0000000..8653752 --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/wifi_on.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/drawable/wifi_on_disabled.xml b/NetGuard/app/src/main/main/res/drawable/wifi_on_disabled.xml new file mode 100644 index 0000000..d20f995 --- /dev/null +++ b/NetGuard/app/src/main/main/res/drawable/wifi_on_disabled.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/NetGuard/app/src/main/main/res/layout/about.xml b/NetGuard/app/src/main/main/res/layout/about.xml new file mode 100644 index 0000000..34ca746 --- /dev/null +++ b/NetGuard/app/src/main/main/res/layout/about.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +