From ca1ffcf34f4de1ee7261bdeb304edd1005ea3213 Mon Sep 17 00:00:00 2001 From: Beau Kujath Date: Thu, 6 Jul 2023 11:39:27 -0600 Subject: [PATCH] genie app receives and logs data sent from debug server --- .../eu/faircode/netguard/ServiceSinkhole.java | 31 + .../app/src/main/jni/netguard/debug_conn.c | 166 +- NetGuard/app/src/main/jni/netguard/ip.c | 42 +- NetGuard/app/src/main/jni/netguard/netguard.h | 2 + NetGuard/app/src/main/jni/netguard/session.c | 5 - NetGuard/app/src/main/jni/netguard/tcp.c | 123 +- .../app/src/main/main/AndroidManifest.xml | 287 ++ .../vending/billing/IInAppBillingService.aidl | 144 + .../app/src/main/main/ic_launcher-web.png | Bin 0 -> 24286 bytes .../src/main/main/ic_launcher_foreground.xcf | Bin 0 -> 35520 bytes .../src/main/main/ic_launcher_round-web.png | Bin 0 -> 42669 bytes .../eu/faircode/netguard/ActivityDns.java | 256 ++ .../netguard/ActivityForwardApproval.java | 130 + .../faircode/netguard/ActivityForwarding.java | 251 ++ .../eu/faircode/netguard/ActivityLog.java | 643 ++++ .../eu/faircode/netguard/ActivityMain.java | 1304 +++++++ .../eu/faircode/netguard/ActivityPro.java | 447 +++ .../faircode/netguard/ActivitySettings.java | 1466 ++++++++ .../eu/faircode/netguard/AdapterAccess.java | 186 + .../java/eu/faircode/netguard/AdapterDns.java | 95 + .../faircode/netguard/AdapterForwarding.java | 74 + .../java/eu/faircode/netguard/AdapterLog.java | 370 ++ .../eu/faircode/netguard/AdapterRule.java | 1033 +++++ .../java/eu/faircode/netguard/Allowed.java | 35 + .../eu/faircode/netguard/ApplicationEx.java | 78 + .../eu/faircode/netguard/DatabaseHelper.java | 1164 ++++++ .../eu/faircode/netguard/DownloadTask.java | 181 + .../faircode/netguard/ExpandedListView.java | 45 + .../java/eu/faircode/netguard/Forward.java | 33 + .../faircode/netguard/FragmentSettings.java | 32 + .../eu/faircode/netguard/GlideHelper.java | 8 + .../main/java/eu/faircode/netguard/IAB.java | 240 ++ .../java/eu/faircode/netguard/IPUtil.java | 140 + .../java/eu/faircode/netguard/Packet.java | 42 + .../faircode/netguard/ReceiverAutostart.java | 132 + .../netguard/ReceiverPackageRemoved.java | 50 + .../eu/faircode/netguard/ResourceRecord.java | 47 + .../main/java/eu/faircode/netguard/Rule.java | 453 +++ .../eu/faircode/netguard/ServiceExternal.java | 146 + .../eu/faircode/netguard/ServiceSinkhole.java | 3335 +++++++++++++++++ .../faircode/netguard/ServiceTileFilter.java | 81 + .../faircode/netguard/ServiceTileGraph.java | 80 + .../netguard/ServiceTileLockdown.java | 75 + .../eu/faircode/netguard/ServiceTileMain.java | 103 + .../faircode/netguard/SwitchPreference.java | 39 + .../main/java/eu/faircode/netguard/Usage.java | 46 + .../main/java/eu/faircode/netguard/Util.java | 1075 ++++++ .../java/eu/faircode/netguard/Version.java | 50 + .../eu/faircode/netguard/WidgetAdmin.java | 99 + .../eu/faircode/netguard/WidgetLockdown.java | 70 + .../java/eu/faircode/netguard/WidgetMain.java | 70 + .../src/main/main/jni/netguard/debug_conn.c | 372 ++ .../app/src/main/main/jni/netguard/dhcp.c | 143 + NetGuard/app/src/main/main/jni/netguard/dns.c | 239 ++ .../app/src/main/main/jni/netguard/icmp.c | 375 ++ NetGuard/app/src/main/main/jni/netguard/ip.c | 611 +++ .../app/src/main/main/jni/netguard/netguard.c | 1116 ++++++ .../app/src/main/main/jni/netguard/netguard.h | 596 +++ .../app/src/main/main/jni/netguard/pcap.c | 78 + .../app/src/main/main/jni/netguard/session.c | 391 ++ NetGuard/app/src/main/main/jni/netguard/tcp.c | 1379 +++++++ NetGuard/app/src/main/main/jni/netguard/udp.c | 549 +++ .../app/src/main/main/jni/netguard/util.c | 182 + .../ic_add_circle_outline_white_24dp.png | Bin 0 -> 487 bytes .../ic_attach_money_black_24dp.png | Bin 0 -> 395 bytes .../ic_attach_money_white_24dp.png | Bin 0 -> 397 bytes .../res/drawable-hdpi/ic_check_white_24dp.png | Bin 0 -> 181 bytes .../res/drawable-hdpi/ic_close_white_24dp.png | Bin 0 -> 221 bytes .../ic_cloud_upload_white_24dp.png | Bin 0 -> 345 bytes .../drawable-hdpi/ic_delete_black_24dp.png | Bin 0 -> 155 bytes .../drawable-hdpi/ic_delete_white_24dp.png | Bin 0 -> 161 bytes .../drawable-hdpi/ic_equalizer_white_24dp.png | Bin 0 -> 106 bytes .../ic_equalizer_white_24dp_60.png | Bin 0 -> 178 bytes .../res/drawable-hdpi/ic_error_white_24dp.png | Bin 0 -> 324 bytes .../ic_expand_less_black_24dp.png | Bin 0 -> 149 bytes .../ic_expand_less_white_24dp.png | Bin 0 -> 156 bytes .../ic_expand_more_black_24dp.png | Bin 0 -> 151 bytes .../ic_expand_more_white_24dp.png | Bin 0 -> 159 bytes .../ic_file_download_white_24dp.png | Bin 0 -> 163 bytes .../ic_filter_list_white_24dp.png | Bin 0 -> 111 bytes .../ic_filter_list_white_24dp_60.png | Bin 0 -> 196 bytes .../ic_hourglass_empty_black_24dp.png | Bin 0 -> 154 bytes .../ic_hourglass_empty_white_24dp.png | Bin 0 -> 159 bytes .../drawable-hdpi/ic_launch_black_24dp.png | Bin 0 -> 250 bytes .../drawable-hdpi/ic_launch_white_24dp.png | Bin 0 -> 253 bytes .../drawable-hdpi/ic_lock_open_white_24dp.png | Bin 0 -> 306 bytes .../ic_lock_outline_white_24dp.png | Bin 0 -> 303 bytes .../ic_lock_outline_white_24dp_60.png | Bin 0 -> 463 bytes .../res/drawable-hdpi/ic_pause_black_24dp.png | Bin 0 -> 102 bytes .../res/drawable-hdpi/ic_pause_white_24dp.png | Bin 0 -> 105 bytes .../ic_perm_data_setting_black_24dp.png | Bin 0 -> 363 bytes .../ic_perm_data_setting_white_24dp.png | Bin 0 -> 386 bytes .../ic_perm_identity_white_24dp.png | Bin 0 -> 365 bytes .../ic_play_arrow_black_24dp.png | Bin 0 -> 194 bytes .../ic_play_arrow_white_24dp.png | Bin 0 -> 195 bytes .../drawable-hdpi/ic_search_white_24dp.png | Bin 0 -> 396 bytes .../drawable-hdpi/ic_security_color_24dp.png | Bin 0 -> 2147 bytes .../drawable-hdpi/ic_security_white_24dp.png | Bin 0 -> 428 bytes .../ic_security_white_24dp_60.png | Bin 0 -> 895 bytes .../drawable-hdpi/ic_settings_black_24dp.png | Bin 0 -> 453 bytes .../drawable-hdpi/ic_settings_white_24dp.png | Bin 0 -> 460 bytes .../ic_shopping_cart_black_24dp.png | Bin 0 -> 317 bytes .../ic_shopping_cart_white_24dp.png | Bin 0 -> 334 bytes .../ic_signal_cellular_4_bar_white_24dp.png | Bin 0 -> 157 bytes .../ic_signal_cellular_off_white_24dp.png | Bin 0 -> 282 bytes .../ic_signal_wifi_4_bar_white_24dp.png | Bin 0 -> 339 bytes .../ic_signal_wifi_off_white_24dp.png | Bin 0 -> 421 bytes .../res/drawable-hdpi/ic_sort_white_24dp.png | Bin 0 -> 115 bytes .../ic_add_circle_outline_white_24dp.png | Bin 0 -> 324 bytes .../ic_attach_money_black_24dp.png | Bin 0 -> 243 bytes .../ic_attach_money_white_24dp.png | Bin 0 -> 256 bytes .../res/drawable-mdpi/ic_check_white_24dp.png | Bin 0 -> 137 bytes .../res/drawable-mdpi/ic_close_white_24dp.png | Bin 0 -> 175 bytes .../ic_cloud_upload_white_24dp.png | Bin 0 -> 235 bytes .../drawable-mdpi/ic_delete_black_24dp.png | Bin 0 -> 111 bytes .../drawable-mdpi/ic_delete_white_24dp.png | Bin 0 -> 115 bytes .../drawable-mdpi/ic_equalizer_white_24dp.png | Bin 0 -> 88 bytes .../ic_equalizer_white_24dp_60.png | Bin 0 -> 159 bytes .../res/drawable-mdpi/ic_error_white_24dp.png | Bin 0 -> 232 bytes .../ic_expand_less_black_24dp.png | Bin 0 -> 126 bytes .../ic_expand_less_white_24dp.png | Bin 0 -> 129 bytes .../ic_expand_more_black_24dp.png | Bin 0 -> 125 bytes .../ic_expand_more_white_24dp.png | Bin 0 -> 129 bytes .../ic_file_download_white_24dp.png | Bin 0 -> 116 bytes .../ic_filter_list_white_24dp.png | Bin 0 -> 90 bytes .../ic_filter_list_white_24dp_60.png | Bin 0 -> 160 bytes .../ic_hourglass_empty_black_24dp.png | Bin 0 -> 131 bytes .../ic_hourglass_empty_white_24dp.png | Bin 0 -> 135 bytes .../drawable-mdpi/ic_launch_black_24dp.png | Bin 0 -> 167 bytes .../drawable-mdpi/ic_launch_white_24dp.png | Bin 0 -> 175 bytes .../drawable-mdpi/ic_lock_open_white_24dp.png | Bin 0 -> 200 bytes .../ic_lock_outline_white_24dp.png | Bin 0 -> 198 bytes .../ic_lock_outline_white_24dp_60.png | Bin 0 -> 327 bytes .../res/drawable-mdpi/ic_pause_black_24dp.png | Bin 0 -> 81 bytes .../res/drawable-mdpi/ic_pause_white_24dp.png | Bin 0 -> 83 bytes .../ic_perm_data_setting_black_24dp.png | Bin 0 -> 262 bytes .../ic_perm_data_setting_white_24dp.png | Bin 0 -> 276 bytes .../ic_perm_identity_white_24dp.png | Bin 0 -> 254 bytes .../ic_play_arrow_black_24dp.png | Bin 0 -> 150 bytes .../ic_play_arrow_white_24dp.png | Bin 0 -> 157 bytes .../drawable-mdpi/ic_search_white_24dp.png | Bin 0 -> 247 bytes .../drawable-mdpi/ic_security_color_24dp.png | Bin 0 -> 1463 bytes .../drawable-mdpi/ic_security_white_24dp.png | Bin 0 -> 288 bytes .../ic_security_white_24dp_60.png | Bin 0 -> 593 bytes .../drawable-mdpi/ic_settings_black_24dp.png | Bin 0 -> 322 bytes .../drawable-mdpi/ic_settings_white_24dp.png | Bin 0 -> 326 bytes .../ic_shopping_cart_black_24dp.png | Bin 0 -> 215 bytes .../ic_shopping_cart_white_24dp.png | Bin 0 -> 219 bytes .../ic_signal_cellular_4_bar_white_24dp.png | Bin 0 -> 135 bytes .../ic_signal_cellular_off_white_24dp.png | Bin 0 -> 201 bytes .../ic_signal_wifi_4_bar_white_24dp.png | Bin 0 -> 252 bytes .../ic_signal_wifi_off_white_24dp.png | Bin 0 -> 308 bytes .../res/drawable-mdpi/ic_sort_white_24dp.png | Bin 0 -> 90 bytes .../ic_add_circle_outline_white_24dp.png | Bin 0 -> 650 bytes .../ic_attach_money_black_24dp.png | Bin 0 -> 428 bytes .../ic_attach_money_white_24dp.png | Bin 0 -> 448 bytes .../drawable-xhdpi/ic_check_white_24dp.png | Bin 0 -> 199 bytes .../drawable-xhdpi/ic_close_white_24dp.png | Bin 0 -> 257 bytes .../ic_cloud_upload_white_24dp.png | Bin 0 -> 405 bytes .../drawable-xhdpi/ic_delete_black_24dp.png | Bin 0 -> 148 bytes .../drawable-xhdpi/ic_delete_white_24dp.png | Bin 0 -> 151 bytes .../ic_equalizer_white_24dp.png | Bin 0 -> 94 bytes .../ic_equalizer_white_24dp_60.png | Bin 0 -> 185 bytes .../drawable-xhdpi/ic_error_white_24dp.png | Bin 0 -> 431 bytes .../ic_expand_less_black_24dp.png | Bin 0 -> 171 bytes .../ic_expand_less_white_24dp.png | Bin 0 -> 179 bytes .../ic_expand_more_black_24dp.png | Bin 0 -> 168 bytes .../ic_expand_more_white_24dp.png | Bin 0 -> 182 bytes .../ic_file_download_white_24dp.png | Bin 0 -> 157 bytes .../ic_filter_list_white_24dp.png | Bin 0 -> 103 bytes .../ic_filter_list_white_24dp_60.png | Bin 0 -> 183 bytes .../ic_hourglass_empty_black_24dp.png | Bin 0 -> 167 bytes .../ic_hourglass_empty_white_24dp.png | Bin 0 -> 174 bytes .../drawable-xhdpi/ic_launch_black_24dp.png | Bin 0 -> 252 bytes .../drawable-xhdpi/ic_launch_white_24dp.png | Bin 0 -> 258 bytes .../ic_lock_open_white_24dp.png | Bin 0 -> 354 bytes .../ic_lock_outline_white_24dp.png | Bin 0 -> 343 bytes .../ic_lock_outline_white_24dp_60.png | Bin 0 -> 538 bytes .../drawable-xhdpi/ic_pause_black_24dp.png | Bin 0 -> 101 bytes .../drawable-xhdpi/ic_pause_white_24dp.png | Bin 0 -> 90 bytes .../ic_perm_data_setting_black_24dp.png | Bin 0 -> 440 bytes .../ic_perm_data_setting_white_24dp.png | Bin 0 -> 485 bytes .../ic_perm_identity_white_24dp.png | Bin 0 -> 455 bytes .../ic_play_arrow_black_24dp.png | Bin 0 -> 208 bytes .../ic_play_arrow_white_24dp.png | Bin 0 -> 220 bytes .../drawable-xhdpi/ic_search_white_24dp.png | Bin 0 -> 465 bytes .../drawable-xhdpi/ic_security_color_24dp.png | Bin 0 -> 2939 bytes .../drawable-xhdpi/ic_security_white_24dp.png | Bin 0 -> 507 bytes .../ic_security_white_24dp_60.png | Bin 0 -> 1208 bytes .../drawable-xhdpi/ic_settings_black_24dp.png | Bin 0 -> 557 bytes .../drawable-xhdpi/ic_settings_white_24dp.png | Bin 0 -> 562 bytes .../ic_shopping_cart_black_24dp.png | Bin 0 -> 375 bytes .../ic_shopping_cart_white_24dp.png | Bin 0 -> 403 bytes .../ic_signal_cellular_4_bar_white_24dp.png | Bin 0 -> 181 bytes .../ic_signal_cellular_off_white_24dp.png | Bin 0 -> 309 bytes .../ic_signal_wifi_4_bar_white_24dp.png | Bin 0 -> 431 bytes .../ic_signal_wifi_off_white_24dp.png | Bin 0 -> 528 bytes .../res/drawable-xhdpi/ic_sort_white_24dp.png | Bin 0 -> 101 bytes .../ic_add_circle_outline_white_24dp.png | Bin 0 -> 947 bytes .../ic_attach_money_black_24dp.png | Bin 0 -> 611 bytes .../ic_attach_money_white_24dp.png | Bin 0 -> 640 bytes .../drawable-xxhdpi/ic_check_white_24dp.png | Bin 0 -> 276 bytes .../drawable-xxhdpi/ic_close_white_24dp.png | Bin 0 -> 347 bytes .../ic_cloud_upload_white_24dp.png | Bin 0 -> 589 bytes .../drawable-xxhdpi/ic_delete_black_24dp.png | Bin 0 -> 191 bytes .../drawable-xxhdpi/ic_delete_white_24dp.png | Bin 0 -> 194 bytes .../ic_equalizer_white_24dp.png | Bin 0 -> 99 bytes .../ic_equalizer_white_24dp_60.png | Bin 0 -> 202 bytes .../drawable-xxhdpi/ic_error_white_24dp.png | Bin 0 -> 614 bytes .../ic_expand_less_black_24dp.png | Bin 0 -> 213 bytes .../ic_expand_less_white_24dp.png | Bin 0 -> 230 bytes .../ic_expand_more_black_24dp.png | Bin 0 -> 215 bytes .../ic_expand_more_white_24dp.png | Bin 0 -> 237 bytes .../ic_file_download_white_24dp.png | Bin 0 -> 197 bytes .../ic_filter_list_white_24dp.png | Bin 0 -> 107 bytes .../ic_filter_list_white_24dp_60.png | Bin 0 -> 196 bytes .../ic_hourglass_empty_black_24dp.png | Bin 0 -> 236 bytes .../ic_hourglass_empty_white_24dp.png | Bin 0 -> 255 bytes .../drawable-xxhdpi/ic_launch_black_24dp.png | Bin 0 -> 335 bytes .../drawable-xxhdpi/ic_launch_white_24dp.png | Bin 0 -> 343 bytes .../ic_lock_open_white_24dp.png | Bin 0 -> 512 bytes .../ic_lock_outline_white_24dp.png | Bin 0 -> 494 bytes .../ic_lock_outline_white_24dp_60.png | Bin 0 -> 727 bytes .../drawable-xxhdpi/ic_pause_black_24dp.png | Bin 0 -> 109 bytes .../drawable-xxhdpi/ic_pause_white_24dp.png | Bin 0 -> 92 bytes .../ic_perm_data_setting_black_24dp.png | Bin 0 -> 693 bytes .../ic_perm_data_setting_white_24dp.png | Bin 0 -> 746 bytes .../ic_perm_identity_white_24dp.png | Bin 0 -> 654 bytes .../ic_play_arrow_black_24dp.png | Bin 0 -> 265 bytes .../ic_play_arrow_white_24dp.png | Bin 0 -> 283 bytes .../drawable-xxhdpi/ic_search_white_24dp.png | Bin 0 -> 728 bytes .../ic_security_color_24dp.png | Bin 0 -> 4538 bytes .../ic_security_white_24dp.png | Bin 0 -> 702 bytes .../ic_security_white_24dp_60.png | Bin 0 -> 1936 bytes .../ic_settings_black_24dp.png | Bin 0 -> 827 bytes .../ic_settings_white_24dp.png | Bin 0 -> 843 bytes .../ic_shopping_cart_black_24dp.png | Bin 0 -> 519 bytes .../ic_shopping_cart_white_24dp.png | Bin 0 -> 569 bytes .../ic_signal_cellular_4_bar_white_24dp.png | Bin 0 -> 217 bytes .../ic_signal_cellular_off_white_24dp.png | Bin 0 -> 435 bytes .../ic_signal_wifi_4_bar_white_24dp.png | Bin 0 -> 619 bytes .../ic_signal_wifi_off_white_24dp.png | Bin 0 -> 795 bytes .../drawable-xxhdpi/ic_sort_white_24dp.png | Bin 0 -> 103 bytes .../ic_add_circle_outline_white_24dp.png | Bin 0 -> 1280 bytes .../ic_attach_money_black_24dp.png | Bin 0 -> 786 bytes .../ic_attach_money_white_24dp.png | Bin 0 -> 819 bytes .../drawable-xxxhdpi/ic_check_white_24dp.png | Bin 0 -> 308 bytes .../drawable-xxxhdpi/ic_close_white_24dp.png | Bin 0 -> 436 bytes .../ic_cloud_upload_white_24dp.png | Bin 0 -> 770 bytes .../drawable-xxxhdpi/ic_delete_black_24dp.png | Bin 0 -> 237 bytes .../drawable-xxxhdpi/ic_delete_white_24dp.png | Bin 0 -> 243 bytes .../ic_equalizer_white_24dp.png | Bin 0 -> 100 bytes .../ic_equalizer_white_24dp_60.png | Bin 0 -> 226 bytes .../drawable-xxxhdpi/ic_error_white_24dp.png | Bin 0 -> 814 bytes .../ic_expand_less_black_24dp.png | Bin 0 -> 261 bytes .../ic_expand_less_white_24dp.png | Bin 0 -> 284 bytes .../ic_expand_more_black_24dp.png | Bin 0 -> 256 bytes .../ic_expand_more_white_24dp.png | Bin 0 -> 287 bytes .../ic_file_download_white_24dp.png | Bin 0 -> 233 bytes .../ic_filter_list_white_24dp.png | Bin 0 -> 106 bytes .../ic_filter_list_white_24dp_60.png | Bin 0 -> 206 bytes .../ic_hourglass_empty_black_24dp.png | Bin 0 -> 246 bytes .../ic_hourglass_empty_white_24dp.png | Bin 0 -> 273 bytes .../drawable-xxxhdpi/ic_launch_black_24dp.png | Bin 0 -> 427 bytes .../drawable-xxxhdpi/ic_launch_white_24dp.png | Bin 0 -> 434 bytes .../ic_lock_open_white_24dp.png | Bin 0 -> 665 bytes .../ic_lock_outline_white_24dp.png | Bin 0 -> 651 bytes .../ic_lock_outline_white_24dp_60.png | Bin 0 -> 949 bytes .../drawable-xxxhdpi/ic_pause_black_24dp.png | Bin 0 -> 111 bytes .../drawable-xxxhdpi/ic_pause_white_24dp.png | Bin 0 -> 94 bytes .../ic_perm_data_setting_black_24dp.png | Bin 0 -> 846 bytes .../ic_perm_data_setting_white_24dp.png | Bin 0 -> 936 bytes .../ic_perm_identity_white_24dp.png | Bin 0 -> 868 bytes .../ic_play_arrow_black_24dp.png | Bin 0 -> 320 bytes .../ic_play_arrow_white_24dp.png | Bin 0 -> 343 bytes .../drawable-xxxhdpi/ic_search_white_24dp.png | Bin 0 -> 915 bytes .../ic_security_color_24dp.png | Bin 0 -> 6465 bytes .../ic_security_white_24dp.png | Bin 0 -> 913 bytes .../ic_security_white_24dp_60.png | Bin 0 -> 2803 bytes .../ic_settings_black_24dp.png | Bin 0 -> 1073 bytes .../ic_settings_white_24dp.png | Bin 0 -> 1074 bytes .../ic_shopping_cart_black_24dp.png | Bin 0 -> 672 bytes .../ic_shopping_cart_white_24dp.png | Bin 0 -> 748 bytes .../ic_signal_cellular_4_bar_white_24dp.png | Bin 0 -> 249 bytes .../ic_signal_cellular_off_white_24dp.png | Bin 0 -> 524 bytes .../ic_signal_wifi_4_bar_white_24dp.png | Bin 0 -> 800 bytes .../ic_signal_wifi_off_white_24dp.png | Bin 0 -> 994 bytes .../drawable-xxxhdpi/ic_sort_white_24dp.png | Bin 0 -> 107 bytes .../res/drawable/baseline_file_copy_24.xml | 10 + .../main/main/res/drawable/expander_black.xml | 8 + .../main/main/res/drawable/expander_white.xml | 8 + .../main/main/res/drawable/host_allowed.xml | 4 + .../main/main/res/drawable/host_blocked.xml | 4 + .../src/main/main/res/drawable/lockdown.xml | 5 + .../main/res/drawable/lockdown_disabled.xml | 4 + .../main/main/res/drawable/lockdown_off.xml | 4 + .../main/main/res/drawable/lockdown_on.xml | 4 + .../app/src/main/main/res/drawable/other.xml | 7 + .../src/main/main/res/drawable/other_off.xml | 4 + .../main/res/drawable/other_off_disabled.xml | 4 + .../src/main/main/res/drawable/other_on.xml | 4 + .../main/res/drawable/other_on_disabled.xml | 4 + .../app/src/main/main/res/drawable/screen.xml | 5 + .../src/main/main/res/drawable/screen_on.xml | 4 + .../main/res/drawable/screen_on_disabled.xml | 4 + .../app/src/main/main/res/drawable/wifi.xml | 7 + .../src/main/main/res/drawable/wifi_off.xml | 4 + .../main/res/drawable/wifi_off_disabled.xml | 4 + .../src/main/main/res/drawable/wifi_on.xml | 4 + .../main/res/drawable/wifi_on_disabled.xml | 4 + .../app/src/main/main/res/layout/about.xml | 108 + .../app/src/main/main/res/layout/access.xml | 65 + .../src/main/main/res/layout/actionlog.xml | 15 + .../src/main/main/res/layout/actionmain.xml | 42 + .../app/src/main/main/res/layout/android.xml | 60 + .../src/main/main/res/layout/challenge.xml | 74 + .../src/main/main/res/layout/datasaving.xml | 56 + NetGuard/app/src/main/main/res/layout/dns.xml | 66 + .../app/src/main/main/res/layout/doze.xml | 56 + .../app/src/main/main/res/layout/enable.xml | 101 + .../app/src/main/main/res/layout/filter.xml | 48 + .../app/src/main/main/res/layout/first.xml | 65 + .../app/src/main/main/res/layout/forward.xml | 61 + .../src/main/main/res/layout/forwardadd.xml | 143 + .../main/main/res/layout/forwardapproval.xml | 60 + .../src/main/main/res/layout/forwarding.xml | 18 + .../app/src/main/main/res/layout/legend.xml | 374 ++ NetGuard/app/src/main/main/res/layout/log.xml | 149 + .../app/src/main/main/res/layout/logging.xml | 38 + .../app/src/main/main/res/layout/main.xml | 226 ++ NetGuard/app/src/main/main/res/layout/pro.xml | 427 +++ .../src/main/main/res/layout/resolving.xml | 18 + .../app/src/main/main/res/layout/rule.xml | 488 +++ .../app/src/main/main/res/layout/sure.xml | 56 + .../app/src/main/main/res/layout/traffic.xml | 111 + NetGuard/app/src/main/main/res/layout/vpn.xml | 56 + .../main/main/res/layout/widgetlockdown.xml | 13 + .../src/main/main/res/layout/widgetmain.xml | 13 + .../app/src/main/main/res/layout/xposed.xml | 60 + .../app/src/main/main/res/menu/access.xml | 31 + NetGuard/app/src/main/main/res/menu/dns.xml | 15 + .../app/src/main/main/res/menu/forward.xml | 10 + .../app/src/main/main/res/menu/forwarding.xml | 9 + NetGuard/app/src/main/main/res/menu/log.xml | 29 + .../app/src/main/main/res/menu/logging.xml | 72 + NetGuard/app/src/main/main/res/menu/main.xml | 78 + NetGuard/app/src/main/main/res/menu/pro.xml | 6 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../main/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2147 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 3512 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4202 bytes .../main/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1463 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2099 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2624 bytes .../main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 2939 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 4425 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5905 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 4538 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 8746 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 9343 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 6465 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 10454 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 13532 bytes .../main/main/res/values-af-rZA/strings.xml | 2 + .../main/main/res/values-ar-rSA/strings.xml | 288 ++ .../main/main/res/values-az-rAZ/strings.xml | 292 ++ .../main/main/res/values-bg-rBG/strings.xml | 281 ++ .../main/main/res/values-bn-rBD/strings.xml | 25 + .../main/main/res/values-ca-rES/strings.xml | 127 + .../main/main/res/values-cs-rCZ/strings.xml | 292 ++ .../main/main/res/values-da-rDK/strings.xml | 292 ++ .../main/main/res/values-de-rDE/strings.xml | 288 ++ .../main/main/res/values-el-rGR/strings.xml | 278 ++ .../main/main/res/values-en-rUS/strings.xml | 13 + .../main/main/res/values-es-rES/strings.xml | 288 ++ .../main/main/res/values-et-rEE/strings.xml | 126 + .../main/main/res/values-eu-rES/strings.xml | 289 ++ .../main/main/res/values-fa-rIR/strings.xml | 271 ++ .../main/main/res/values-fi-rFI/strings.xml | 290 ++ .../main/main/res/values-fil-rPH/strings.xml | 272 ++ .../main/main/res/values-fr-rFR/strings.xml | 290 ++ .../main/main/res/values-hi-rIN/strings.xml | 288 ++ .../main/main/res/values-hr-rHR/strings.xml | 288 ++ .../main/main/res/values-hu-rHU/strings.xml | 290 ++ .../main/main/res/values-in-rID/strings.xml | 292 ++ .../main/main/res/values-it-rIT/strings.xml | 292 ++ .../main/main/res/values-iw-rIL/strings.xml | 289 ++ .../main/main/res/values-ja-rJP/strings.xml | 287 ++ .../main/main/res/values-ka-rGE/strings.xml | 2 + .../main/main/res/values-ko-rKR/strings.xml | 292 ++ .../main/main/res/values-lt-rLT/strings.xml | 290 ++ .../main/main/res/values-lv-rLV/strings.xml | 292 ++ .../main/main/res/values-ml-rIN/strings.xml | 139 + .../main/main/res/values-my-rMM/strings.xml | 38 + .../main/main/res/values-nl-rNL/strings.xml | 291 ++ .../main/main/res/values-no-rNO/strings.xml | 247 ++ .../main/main/res/values-or-rIN/strings.xml | 55 + .../main/main/res/values-pl-rPL/strings.xml | 288 ++ .../main/main/res/values-pt-rBR/strings.xml | 291 ++ .../main/main/res/values-pt-rPT/strings.xml | 288 ++ .../main/main/res/values-ro-rRO/strings.xml | 291 ++ .../main/main/res/values-ru-rRU/strings.xml | 290 ++ .../main/main/res/values-si-rLK/strings.xml | 45 + .../main/main/res/values-sk-rSK/strings.xml | 223 ++ .../main/main/res/values-sl-rSI/strings.xml | 216 ++ .../main/main/res/values-sr-rSP/strings.xml | 292 ++ .../main/main/res/values-sv-rSE/strings.xml | 292 ++ .../main/main/res/values-ta-rIN/strings.xml | 287 ++ .../main/main/res/values-tl-rPH/strings.xml | 271 ++ .../main/main/res/values-tr-rTR/strings.xml | 292 ++ .../main/main/res/values-ug-rCN/strings.xml | 251 ++ .../main/main/res/values-uk-rUA/strings.xml | 289 ++ .../src/main/main/res/values-v14/styles.xml | 10 + .../src/main/main/res/values-v21/styles.xml | 10 + .../main/main/res/values-vi-rVN/strings.xml | 288 ++ .../main/main/res/values-w820dp/dimens.xml | 6 + .../main/main/res/values-zh-rCN/strings.xml | 292 ++ .../main/main/res/values-zh-rTW/strings.xml | 291 ++ .../app/src/main/main/res/values/colors.xml | 45 + .../app/src/main/main/res/values/dimens.xml | 4 + .../res/values/ic_launcher_background.xml | 4 + .../app/src/main/main/res/values/strings.xml | 362 ++ .../app/src/main/main/res/values/styles.xml | 140 + .../src/main/main/res/xml-v14/preferences.xml | 381 ++ .../src/main/main/res/xml-v21/preferences.xml | 381 ++ .../main/res/xml/network_security_config.xml | 11 + .../app/src/main/main/res/xml/predefined.xml | 86 + .../app/src/main/main/res/xml/shortcuts.xml | 29 + .../src/main/main/res/xml/widgetlockdown.xml | 8 + .../app/src/main/main/res/xml/widgetmain.xml | 8 + 431 files changed, 38301 insertions(+), 109 deletions(-) create mode 100644 NetGuard/app/src/main/main/AndroidManifest.xml create mode 100644 NetGuard/app/src/main/main/aidl/com/android/vending/billing/IInAppBillingService.aidl create mode 100644 NetGuard/app/src/main/main/ic_launcher-web.png create mode 100644 NetGuard/app/src/main/main/ic_launcher_foreground.xcf create mode 100644 NetGuard/app/src/main/main/ic_launcher_round-web.png create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityDns.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityForwardApproval.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityForwarding.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityLog.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityMain.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivityPro.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ActivitySettings.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterAccess.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterDns.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterForwarding.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterLog.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/AdapterRule.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/Allowed.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ApplicationEx.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/DatabaseHelper.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/DownloadTask.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ExpandedListView.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/Forward.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/FragmentSettings.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/GlideHelper.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/IAB.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/IPUtil.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/Packet.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ReceiverAutostart.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ReceiverPackageRemoved.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ResourceRecord.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/Rule.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceExternal.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceSinkhole.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileFilter.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileGraph.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileLockdown.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/ServiceTileMain.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/SwitchPreference.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/Usage.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/Util.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/Version.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/WidgetAdmin.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/WidgetLockdown.java create mode 100644 NetGuard/app/src/main/main/java/eu/faircode/netguard/WidgetMain.java create mode 100644 NetGuard/app/src/main/main/jni/netguard/debug_conn.c create mode 100644 NetGuard/app/src/main/main/jni/netguard/dhcp.c create mode 100644 NetGuard/app/src/main/main/jni/netguard/dns.c create mode 100644 NetGuard/app/src/main/main/jni/netguard/icmp.c create mode 100644 NetGuard/app/src/main/main/jni/netguard/ip.c create mode 100644 NetGuard/app/src/main/main/jni/netguard/netguard.c create mode 100644 NetGuard/app/src/main/main/jni/netguard/netguard.h create mode 100644 NetGuard/app/src/main/main/jni/netguard/pcap.c create mode 100644 NetGuard/app/src/main/main/jni/netguard/session.c create mode 100644 NetGuard/app/src/main/main/jni/netguard/tcp.c create mode 100644 NetGuard/app/src/main/main/jni/netguard/udp.c create mode 100644 NetGuard/app/src/main/main/jni/netguard/util.c create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_add_circle_outline_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_attach_money_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_attach_money_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_check_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_close_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_cloud_upload_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_delete_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_delete_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_equalizer_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_equalizer_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_error_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_less_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_less_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_more_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_expand_more_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_file_download_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_filter_list_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_filter_list_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_hourglass_empty_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_hourglass_empty_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_launch_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_launch_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_lock_open_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_lock_outline_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_lock_outline_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_pause_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_pause_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_perm_data_setting_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_perm_data_setting_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_perm_identity_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_play_arrow_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_search_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_security_color_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_security_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_security_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_settings_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_settings_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_shopping_cart_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_shopping_cart_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_cellular_4_bar_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_cellular_off_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_wifi_4_bar_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_signal_wifi_off_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-hdpi/ic_sort_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_add_circle_outline_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_attach_money_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_attach_money_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_check_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_close_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_cloud_upload_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_delete_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_delete_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_equalizer_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_equalizer_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_error_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_less_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_less_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_more_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_expand_more_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_file_download_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_filter_list_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_filter_list_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_hourglass_empty_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_hourglass_empty_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_launch_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_launch_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_lock_open_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_lock_outline_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_lock_outline_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_pause_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_pause_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_perm_data_setting_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_perm_data_setting_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_perm_identity_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_play_arrow_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_search_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_security_color_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_security_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_security_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_settings_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_settings_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_shopping_cart_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_shopping_cart_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_cellular_4_bar_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_cellular_off_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_wifi_4_bar_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_signal_wifi_off_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-mdpi/ic_sort_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_add_circle_outline_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_attach_money_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_attach_money_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_check_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_close_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_cloud_upload_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_delete_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_delete_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_equalizer_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_equalizer_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_error_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_less_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_less_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_more_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_expand_more_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_file_download_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_filter_list_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_filter_list_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_hourglass_empty_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_hourglass_empty_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_launch_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_launch_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_lock_open_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_lock_outline_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_lock_outline_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_pause_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_pause_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_perm_data_setting_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_perm_data_setting_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_perm_identity_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_play_arrow_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_search_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_security_color_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_security_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_security_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_settings_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_settings_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_shopping_cart_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_shopping_cart_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_cellular_4_bar_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_cellular_off_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_wifi_4_bar_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_signal_wifi_off_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xhdpi/ic_sort_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_add_circle_outline_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_attach_money_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_attach_money_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_check_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_close_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_cloud_upload_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_delete_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_delete_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_equalizer_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_equalizer_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_error_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_less_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_less_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_more_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_expand_more_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_file_download_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_filter_list_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_filter_list_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_hourglass_empty_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_hourglass_empty_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_launch_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_launch_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_lock_open_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_lock_outline_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_lock_outline_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_pause_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_pause_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_perm_data_setting_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_perm_data_setting_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_perm_identity_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_play_arrow_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_search_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_security_color_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_security_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_security_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_settings_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_settings_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_shopping_cart_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_shopping_cart_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_cellular_4_bar_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_cellular_off_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_wifi_4_bar_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_signal_wifi_off_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxhdpi/ic_sort_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_add_circle_outline_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_attach_money_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_attach_money_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_check_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_close_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_cloud_upload_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_delete_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_delete_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_equalizer_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_equalizer_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_error_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_less_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_less_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_more_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_expand_more_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_file_download_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_filter_list_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_filter_list_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_hourglass_empty_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_hourglass_empty_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_launch_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_launch_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_lock_open_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_lock_outline_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_lock_outline_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_pause_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_pause_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_perm_data_setting_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_perm_data_setting_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_perm_identity_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_play_arrow_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_play_arrow_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_search_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_security_color_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_security_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_security_white_24dp_60.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_settings_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_settings_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_shopping_cart_black_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_shopping_cart_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_cellular_4_bar_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_cellular_off_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_wifi_4_bar_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_signal_wifi_off_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable-xxxhdpi/ic_sort_white_24dp.png create mode 100644 NetGuard/app/src/main/main/res/drawable/baseline_file_copy_24.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/expander_black.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/expander_white.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/host_allowed.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/host_blocked.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/lockdown.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/lockdown_disabled.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/lockdown_off.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/lockdown_on.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/other.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/other_off.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/other_off_disabled.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/other_on.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/other_on_disabled.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/screen.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/screen_on.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/screen_on_disabled.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/wifi.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/wifi_off.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/wifi_off_disabled.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/wifi_on.xml create mode 100644 NetGuard/app/src/main/main/res/drawable/wifi_on_disabled.xml create mode 100644 NetGuard/app/src/main/main/res/layout/about.xml create mode 100644 NetGuard/app/src/main/main/res/layout/access.xml create mode 100644 NetGuard/app/src/main/main/res/layout/actionlog.xml create mode 100644 NetGuard/app/src/main/main/res/layout/actionmain.xml create mode 100644 NetGuard/app/src/main/main/res/layout/android.xml create mode 100644 NetGuard/app/src/main/main/res/layout/challenge.xml create mode 100644 NetGuard/app/src/main/main/res/layout/datasaving.xml create mode 100644 NetGuard/app/src/main/main/res/layout/dns.xml create mode 100644 NetGuard/app/src/main/main/res/layout/doze.xml create mode 100644 NetGuard/app/src/main/main/res/layout/enable.xml create mode 100644 NetGuard/app/src/main/main/res/layout/filter.xml create mode 100644 NetGuard/app/src/main/main/res/layout/first.xml create mode 100644 NetGuard/app/src/main/main/res/layout/forward.xml create mode 100644 NetGuard/app/src/main/main/res/layout/forwardadd.xml create mode 100644 NetGuard/app/src/main/main/res/layout/forwardapproval.xml create mode 100644 NetGuard/app/src/main/main/res/layout/forwarding.xml create mode 100644 NetGuard/app/src/main/main/res/layout/legend.xml create mode 100644 NetGuard/app/src/main/main/res/layout/log.xml create mode 100644 NetGuard/app/src/main/main/res/layout/logging.xml create mode 100644 NetGuard/app/src/main/main/res/layout/main.xml create mode 100644 NetGuard/app/src/main/main/res/layout/pro.xml create mode 100644 NetGuard/app/src/main/main/res/layout/resolving.xml create mode 100644 NetGuard/app/src/main/main/res/layout/rule.xml create mode 100644 NetGuard/app/src/main/main/res/layout/sure.xml create mode 100644 NetGuard/app/src/main/main/res/layout/traffic.xml create mode 100644 NetGuard/app/src/main/main/res/layout/vpn.xml create mode 100644 NetGuard/app/src/main/main/res/layout/widgetlockdown.xml create mode 100644 NetGuard/app/src/main/main/res/layout/widgetmain.xml create mode 100644 NetGuard/app/src/main/main/res/layout/xposed.xml create mode 100644 NetGuard/app/src/main/main/res/menu/access.xml create mode 100644 NetGuard/app/src/main/main/res/menu/dns.xml create mode 100644 NetGuard/app/src/main/main/res/menu/forward.xml create mode 100644 NetGuard/app/src/main/main/res/menu/forwarding.xml create mode 100644 NetGuard/app/src/main/main/res/menu/log.xml create mode 100644 NetGuard/app/src/main/main/res/menu/logging.xml create mode 100644 NetGuard/app/src/main/main/res/menu/main.xml create mode 100644 NetGuard/app/src/main/main/res/menu/pro.xml create mode 100644 NetGuard/app/src/main/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 NetGuard/app/src/main/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 NetGuard/app/src/main/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 NetGuard/app/src/main/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 NetGuard/app/src/main/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 NetGuard/app/src/main/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 NetGuard/app/src/main/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 NetGuard/app/src/main/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 NetGuard/app/src/main/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 NetGuard/app/src/main/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 NetGuard/app/src/main/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 NetGuard/app/src/main/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 NetGuard/app/src/main/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 NetGuard/app/src/main/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 NetGuard/app/src/main/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 NetGuard/app/src/main/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 NetGuard/app/src/main/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 NetGuard/app/src/main/main/res/values-af-rZA/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-ar-rSA/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-az-rAZ/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-bg-rBG/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-bn-rBD/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-ca-rES/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-cs-rCZ/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-da-rDK/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-de-rDE/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-el-rGR/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-en-rUS/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-es-rES/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-et-rEE/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-eu-rES/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-fa-rIR/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-fi-rFI/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-fil-rPH/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-fr-rFR/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-hi-rIN/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-hr-rHR/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-hu-rHU/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-in-rID/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-it-rIT/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-iw-rIL/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-ja-rJP/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-ka-rGE/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-ko-rKR/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-lt-rLT/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-lv-rLV/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-ml-rIN/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-my-rMM/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-nl-rNL/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-no-rNO/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-or-rIN/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-pl-rPL/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-pt-rBR/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-pt-rPT/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-ro-rRO/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-ru-rRU/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-si-rLK/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-sk-rSK/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-sl-rSI/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-sr-rSP/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-sv-rSE/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-ta-rIN/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-tl-rPH/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-tr-rTR/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-ug-rCN/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-uk-rUA/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-v14/styles.xml create mode 100644 NetGuard/app/src/main/main/res/values-v21/styles.xml create mode 100644 NetGuard/app/src/main/main/res/values-vi-rVN/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-w820dp/dimens.xml create mode 100644 NetGuard/app/src/main/main/res/values-zh-rCN/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values-zh-rTW/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values/colors.xml create mode 100644 NetGuard/app/src/main/main/res/values/dimens.xml create mode 100644 NetGuard/app/src/main/main/res/values/ic_launcher_background.xml create mode 100644 NetGuard/app/src/main/main/res/values/strings.xml create mode 100644 NetGuard/app/src/main/main/res/values/styles.xml create mode 100644 NetGuard/app/src/main/main/res/xml-v14/preferences.xml create mode 100644 NetGuard/app/src/main/main/res/xml-v21/preferences.xml create mode 100644 NetGuard/app/src/main/main/res/xml/network_security_config.xml create mode 100644 NetGuard/app/src/main/main/res/xml/predefined.xml create mode 100644 NetGuard/app/src/main/main/res/xml/shortcuts.xml create mode 100644 NetGuard/app/src/main/main/res/xml/widgetlockdown.xml create mode 100644 NetGuard/app/src/main/main/res/xml/widgetmain.xml 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 0000000000000000000000000000000000000000..68fe6703213a51d85ebaf14cc3d46b17ae9612a9 GIT binary patch literal 24286 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..342fd91114603c584b6043f3fcc3207ffbe1c082 GIT binary patch literal 35520 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 LDY{?9!}}!^6 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..67e53a67948e668ee76c1011bfc14c6cbd3ec55d GIT binary patch literal 487 zcmVD<94tg9_0>CY_w^N>;39a7p-+%TvnWV3yJ_(8 z^U%*O?Z-Cz3>=OH9fQ*tS_i$ce#kypjiJ;Kv};2Y#FG^tIfJkcGI4rqU3gq_b({xlr8$5LVJ)cteC!TM|TzAVE+xwUl5#Jzi4Oz(%HlOyk|dbrLMC5yqn4AUOBuW9v=PWC zIf{<5d*TEXW{P9VcGV}n)S-)2zNPA%It@~isu(JLq^-np+moURRF>rRE!@`RNfwn6 zdHtPEoOb0&3YAt0vT*A7{U`XbrZsv{%3w;KSZ0_c(`-|b**;ow4evY<#g`f0JZg@J p;#&uEoY)mttkaKP^S{3V`~v_2;vAONoI(Hq002ovPDHLkV1nx|soVen literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ee833c667ac49d6204cbc7ee1a5939809d98d341 GIT binary patch literal 397 zcmV;80doF{P)~!wssdc#O3D@w zxL&AM9;%5DAuL-YFb=3!5X8u0q}ZU3O8LbE8NR%dz@bcj(8e+OZkeWBe$dG(FMM}K zll-BQezr)G^GJjIqlOutG4>P;t-NB?%La`&I5;$^04o?hvcV+Af(j7F7?lk=Fz%^R z9+olsWP?(!F?I;)6H(r9WvP=L2FbZ(igscQut*AHP4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..729f290104788ce25a026cd1842e625ed29469d3 GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8ji-xah{y4_S9bF@81T3TmS5N? z#C(9Q$LV3`iyt|zzNMK?OD+gTJ2maGvJ86jqI1fWJrY@L@@dRh+8HxyC3MfUOIWFC zSzj(@_&m4jkH)bAn}wUwn%lHbyQcPAEcKb)b;T<=reaCi=lz``&(3vDxn&&?Dog+*{ z>6z1@lfD*AYSPav7 rWuGes_%)|k0wldT1B8K8lBbJfNCo5DGaETu4Mf}?2B)#l zb-AONd&)YjO`v_^^{gD%3xA&6WO--#_jr6|V@s@Giq9W;kX&`+h3Nr=!>9&BGH7*ROEODjfLq?tE}C&^887S3j3^P6|k0wldT1B8K8hNp{Th{y5d1PNA=hW|4Ewf~tb z9PIip^F(s-FKmRW+J}dc&f#K)gg?ihMxz_^iWbkzL Kb6Mw<&;$SheLUU( literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..aa343b5b07cb443197a293cd4779f29f69c58bd4 GIT binary patch literal 106 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;p{I*uh{y5d1PRu|e1F`T7PIwA z9r>`Ikx|pQ!^WIXy1C<`S=NOWj*Hm>^DbyeC^0aIGnTDvWZiHAsF}gj)z4*}Q$iB} D&dwWr literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..933a1981017562ab334b43ec3f7d6b8dc4c8d6d2 GIT binary patch literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8JTQ SNu{v^nGBw;elF{r5}E+k$S-dI literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..69cbb1e0bd3807d27f4197a9328467df8397e169 GIT binary patch literal 324 zcmV-K0lWT*P)gGbr{!4DC&5DTe055otv=(oBcbwiQ9yc={9S=wqGvvb{)YG_-cTkwp}f z`yT&OF!RTU{~tMq?Z(U8YgIoe@jCRcP1rd4N|RY{%7e{VkW|k0wldT1B8K8jHioZNCo5DGuB)U4g$>&8x|}V z`|f4;ZaMR$Jg&1kNfYf;A2b)gGv68^{;X%K9nVTF=T)a|Ln w%9KMZL>5lzopr0IqE?zyJUM literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dea8988386907cdd1741d2cd17eea26f5fe9cba4 GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8vZsq|k0wldT1B8K8oTrOpNCo5DGp9LQ9eCO<@{1hX zq`d9P>fJkd6I*uv>3*%WyutbB>L9gyXZ|E4FTc3CAekqsa8C2JTp9VbJp7?qNhWb~ zO%|STrA@0z5=;cX|Jl*5m?PYIHXed%m|<=5_cK${pmUHx3vIVCg!0Fk0N A$N&HU literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..022e057995c587b7cf287f55da879f6db02e4da0 GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8nx~6nh)3t!Grn963L*@N3l;iv zOP1E{xw~azkg>u13(wA$Jur5=yhT2_!uGLvT4Jh}T=(&-M#c=cCm(vmsHG`0WkvVO zR;8@{E7_L0m>OBCCJ4O}FyvHCHu5^;GGz*92tQD>-rOAmi>^zlo4*0t$l&Sf=d#Wz Gp$P!|k0wldT1B8K8mZytjh{y4_Q*ZMgP~c#YJsK$U z)~(q1m-m)lF^^vy&zFBx;@>#sNTW$[{bH;=%sZG7QZ>uQBcw?@Ce|ClF7G156^ z#Rt_}Q9HRs{l8sYa>)1&$3+cMLFMTJXY2PZ*sH$V@BeR?lj;@>g(qYZ*L3k@0BvRP MboFyt=akR{0D4_J0ssI2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7e8a6b536bdfcba999ba36bfdba7ca975fd7b68c GIT binary patch literal 111 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;nWu|mh)3t!ix)W=3^|k0wldT1B8JTQ|k0wldT1B8K8qNj^vNCo5DtG-+Z3^-gKTFyKE z>BhUL0^5LJVotediVm^=G`zU~Rh8WQwuCALme#Y6c&FHI6=_>}E{I7aLFTEXi)33+ zTjG@TXNvnw_TBvRLtRfLU+~_R&*x7xeyhLv`!$|k0wldT1B8K8nx~6nh{y4_SFO1i6nI=5`==*Y z=Il<_U3OsyN8L7Fo)w-xFP9ojSiCUetFt0Uakd$ADub7Qq6ovMjQrhKOdba-E?a)c z$6MEsck8he3KuWBOxLZd3v@o^=3>63-Z?LRt9jkc?Ms**p0(hgd~H($&_)JNS3j3^ HP67;-*A7xBSs3ks@KsD$#A?k;W>GH?8%p&se(~3?ywDI?9cD%DOg^NUVnTwsaaj*V9v>!mxKC$S;gujkOfx%|}2{a1wligaPBd}%k21*4-`AjG`4gdfE07*qoM6N<$f=pFk A#{d8T literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..49726bcbf047714d9180a44ec606aceb2256e281 GIT binary patch literal 253 zcmVP3#=L&}U6B+P-G4@FKB(~vDl znBYOexKQN$%(##;nnE7)22#d_A{S+5zL7E(q>KeA<3meEVa%(o7=Oy(03a!}!C9+NL(kk@X#^^i{IV-0+QJBA^%Kw?)|0-vzAsPa00000NkvXXu0mjf DTn1)U literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6bae68f56f59152d7954b776356836e29d112497 GIT binary patch literal 306 zcmV-20nPr2P)z6=Sc_Dj*w)pzMgQ*Bw7UA7Dr$`r1xi-+ub_BF5eVxiW8nHlLhF*F4) zT5-@2JZhIG+Md8D25KlUE15-iyra;p4s`?Xz5$2Tp+4w+J3j>%0Vtx!;Oaj@Ycjbi z8OzkeP$ni>7|O*Y4?{pL(9n}BXxsU`TdB~2DVXS_K@ZB@r9mIc6lu`CGPh|^#{|6D zPyK3}&gR`%{nGaNm+g(^8f$<07*qoM6N<$ Ef{!GExBvhE literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e53f54186b98e10d7a746605480e85b5c7871d2f GIT binary patch literal 303 zcmV+~0nq-5P)zT|8+I z56y`ubKxNnrZ8;H2goN?s(d^rB*_X`_V;|y2!O%!LE8W}&j$?w7&ssF*D_f2`@FmM zL4^wMFZvukKnv%1kIwNzwuFa*F~8Y!S_A|4BYTloxb~PT13Ul#002ovPDHLkV1mqT Be?2Ff?J^$%Oy_0bof)K~zY` z<&?2b0znXlzkw48kp?s|*jXsGjTaIdA3zHW8ha~Vz{bWG(9qagNr66rc3vXJ-hxo6 z6p(OXYDODGnlMo6VgRJZH4tW5ACv^3(x-nabNZkt0MC8;3jkC4pb;W8 zktd=owy=q5+-u=blvh7cmh}K8E~Ma>njI*cp3_#(Rt!LPhBMUB*XiHewXuP$q18yR z0^CT$2&9(C5dk*Tp_ENh%Mt-#nygukQk~U;8R)*Vx-$cvc*CSc!Km0$R(oF8T*LND zgY&=nXHV#fw1|2mLMNL3j+{Uy9Dhee(45 M1S3@d0$9QLH=KHHZKJ002ovPDHLk FV1i98zgz$S literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3539b4ef1f0ea5dff36c9521d9fbed0fa0600d6c GIT binary patch literal 102 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K;uBVG*NCjiE1MA`h5fcRkyTU^c x?3o@3F5mCCn5|dJB|k0wldT1B8K;fv1aOh{y5d1PRu~43dBH9UPL5 z6iTvm_Et?8?vs(pG^ZmyORAR z_DgNz%jq#g&Zq3JWXy5Td>Hf1H~@9tD}M1rE#&$=;M>PlK3gHTcn9CDF258ka!RSo zS;(Iu2dN{=A-=AjVF2UMqhv~!1$=FJpA2Cf%OD-dQs6t6_g$juDLvqr7F~wATo6$w z;DZEJ=dFTyrbbjPZkT3?4*_0iGRqxJM(k2=kFj-1N(QW<{R7PkNl4ahs7C8-C6}45poB zSdnzmeQk0d#hK&>t~<3V&l6fqknk$`BNe8&X4;Nr0Ipa1{> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..78d43f08baa616824bdcda4ec3a1c0050d4de6d2 GIT binary patch literal 365 zcmV-z0h0cSP)E0LSs6AidPCmR*Yb92af~L4SM$dx8=~aaWH}K1bNLMVop7|13hHg}YS9(@IK2 zgHJAE7Q#8F5h95D`>uu=KAQ>u+b-us*Vty?NJmx3ui(lZp8&wbsdUImWM<}(s-#0* zFmo;lE-XMh9U6o4c&>qAI<&z|5VOSkIA|`2$&Q0Y;3A&ygF!mf0SmR?%vaD#hpHxk zr3~;f>HA}+CO~9v3rSF{s%>b!Z=j_jBtz*HU!SRa=7r~;syj=EZg}F2%;7S94cGq+ zy5*IXqOH7ivkZFV8-VZL>uGB2YwG#n2Y}3jBB*8uwqASans89XWBvH%UOqGeZ{02V zf_Gq$5B$TT40wC!Dm5vQz(YErpnuvK#BYNP|k0wldT1B8K8r>Bc!NCji^0XDaT8U~A|Io3<; zlH9Q(@j|`Ap5qU^Seq9V7&ddXvK@Sw-4@}+>vUi)PuCR5mWFsqFNqnN#vF|2Cr#G) zm)v~8#c`+O2N2QW;walJ7I=8UZk}~lINs>7>1Da>S2Pt$F=YO%a8hZ#WYUU>nc{&F pJqMI$1u=AH{1W@OI6;Jg!9HnCPgkS=OQ53|JYD@<);T3K0RXaJLVW-L literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..57c9fa5460323823edb0289c1d15f0f561e0c06e GIT binary patch literal 195 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8m#2$kh{y4_Q_k`_IEb*R=ADxY zV6uJDq_v_+Z-tYfR|K!cZcn#wZ_1w*Zwfx3^1JLFH&cH)|4gqfmxMH3rgg?BZ~AgX zO7P4Ssfnoy7dsLbs)cmuJnq)8IA+UruCTi)d!Bot@+%Rg#TB=_7E65dSjDj^h($AU t;!LNFTjoCMkvN)I{QpIN|NiIvIZ;mvueQI+xBzq&gQu&X%Q~loCIB@QN?HH_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bbfbc96cbce0dc0bf2a0b5ac4fa924dab5e34919 GIT binary patch literal 396 zcmV;70dxL|P)MlVO6$ z;_nvIa)$r8oBh5veEc|b;H00d8XJzB8#}e9C&kTuJTUVGfHxC^|AqQ$xx5-GhxGjg zEIhh0QF{S=I4p;1z^yImyK9a$ptVy59RMztM>fsDtPGlg+w5@HC)kgn4PRK;>X_bu z>liu$k6lAU@DxMm;HncEfHsE4VA2Woz#@iDLEQ;$gC>Udz>Bqx=?R#|P|q7U?)b51 zFp8mM0vel{;U#$YTMYT-70m4}o#qm7R{GY4KES6NLjyhAPCNtV)=Qz}P%ED~n5xR4 zWXH^M-kn(oJmjgYZr`;ht%b(a==+MNfO}N{1?^|3m7t{Z0Ju|yP(a=1sbqEPk1B)$ q>JBiDp_nQ^Q9zA@J6g3rP~3lFwlD09o&!k$0000 z`iJ!oYtp7QX`qRX^=Ld|)E-!?RZ2CURck%I&Ns_E-Y#!fcK7*M-2IX-Z05~;GvAMS zGv6GK(fe4NfxNdBLUDcsoJ*W6>0oohd$36t z2IIEoOFD?Jl{G+O#X7JqtkcBK_6@+5>VV^Q7l@W{rP)3T!7(&gnt?Klmcx;h8SqTP zk=EtoVP{Sd%rs;}56jcK&48FV|r2s7}OEqa&Q|bqh zvOWRcsw%S!iX4D$sfU@sVY(zUvTFfVYSmU9w&nG*7tq6SFZe|Qj#B`_-;VYgM*uy+ zaVNk-5>B@VB-OiY1#~6I(;^&~??Kl?hFOK<(t!R68~$84E(_?tWnS=`|4=s|OHuQs zK#xwraY;absXRLk$K?RsRu8ucj!Oah*Ge$x@_;ZHycpnNGaQ!z^kVSr2-o1R#3OrP{b!0JOYxrR;!OgFR(|gX)(R z!vH;00?-|FV)3R70Z>v&qO5=#()?tDgF0vJat7#*5`bFJiFxH&0-*Hbc=-TTZHB~E zD)=~dmfgBHat9c$pQy8BYCL+$R{tA&0=SiIBIS}XNK%3F$v}x(`M7b z)n3iW4wnrMYC+-x0npopYZ#y=C0?=p2&hl>X1mX>%oT${96RI$q>q~;0P0_?aok2MPX-ThPcD-2)RH{3toNx^e7n`qh=6+6l+*i%)~}~{ zL9@U0%`Z6s^&t`S_ras6y(21b=+z!UH+I4G---nGnw8~+TMY)UbYf>v4nQ%hLj-%8 znF0jc(W*rJ!|E7 z($+4zM4r!j(Yk`D4sh<-a3bL-pdF+lhgB;haW{>$9^I z(pDZqGs58f(ihu<64Qc&I`A*nDcaUJtK7P)v$AuG7q??$3uzo}WLSC_ob&gP9Zr z)UrN%B5CMV*65LBHT_lSc!vPJMtV(t;zE-ii8e>JuhfCoiFJ1i;q!))2A>o=xG{d7 zU4Ry8qT#!c(I$D(7TOf$J4x&8R)T{huiGbf_QR}LXlh$4nQK+-OcO9;bjh$ga*9bF zEQ>ZwAYvU(Hz3;tDmRrla-TVb-)|MK@ar&u|Df`L;rXk;qhK{u#LY381u-v{G5hxG zKDGT)2%kHSICMn(pC6T%(xHb)DdxevX0P56YV)NM{1UlTt08(%?Cb(E#(W(*t^*iM zThM;5hi!V@qc)uCt7f^SD93|1`#HT1*XH;uC0ZKjH3pBSioY>U1m_togJoSt_?%f} zfIViq;gv(xY!E(1^SY??Ag@dW{dC)Wil=Gh8Rl_$;rU(!ND={akt+ViG`G0LEpBmf Z`5(~BDM35R;`RUl002ovPDHLkV1nE@^r-*< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..262800a4d89c11f90c2607e2596b00cb8c917f6d GIT binary patch literal 428 zcmV;d0aN~oP)4fI5C-6YO+fBAn}8q+78XVk5+sONUl)m_IRvrD9}u(=3HJkR6@sJ@n^;)cg!GDH znM#W&R@X?d@DAP0BiIb&yWQJw3qkOma^NYZn3=i17=3+nv#P#5Z^J&=w_&~=v|wl? z!pP7<9<*rN1i*KZiEWF`puSy^1NU?t&u%)kbRv;A)}8p%Ns~-1nAMnw!j2WutIEh~ z3Jn2=9y=RIEyS=2+R+K1Jrs@q(7yp~wt_ZO=s^usNb3mPPocXiGZlXd?xfI-Oz17R zo?&};u_W!7(x W*Q%FH(_9B@`8bR16hrB`g{VlG?KAn(JS? zJrgI0VeU9@XXeh#dEfz2=bSS?X3p&0J$IE7iA17;tKaVrz|-(YviB_69Z^kFY4(>Hcs)de{RCgvHCTo|wNAB`lK@kN=i%*u zMIOSh;7K1jxElI4)n4U-(}aHoEUFDZg3krWbIk0HeJ&h;f)w!`zUAyMh5-8Ov;eKb zH=-;w^9blS{06S2aHSd#XbV1{;;n2vpwB7yIsqhr1dsp{Kmter2_OL^fCP{L5 zDk0{Ck_2?eV=kzok`NLtnh1y{;C^aiFRG+uPmG0}mgfefTMiurnR1H@Qqv?rIQFW? zq-|3rEgBean*fMztu2o^$#e$HIi6@CYAAWExsrKuc@$@Fgj-1&(1J(2mDD%pp@31% zqL3XBUBNw%$NH!;K{UypC?ML6=(YV}Mwz6zo@zdO&b<@XgiU3dP}C{-JtrW0Fdcq* zLX3BIEy_gE39NAd!rw0W_+(`@%yfU!kSb9iY2rPmfpxEA*AiQ6#dIZEQ2-H`d7nF@ zE#t2oO;1`10qF|u8ua_pVaeE@;}cY!>CJvSW?kh$IFX5{&D*8lOsG7GmL;Bt2pq2N7fXkB(9aB|B5qbdAc8oEppaIOE(LXPbaD_uiXgfuI0y=Ui?$Rg z3N9|rijvq&CiQlchlhJevpDqnjrr%mb8?c{t6qzumxlIL6n!zIh8Gl(U}e3LlM0Ef;V2BeJSjZb=;SQ|z+u z4yaibL-bf>PG6*TZCbM}(t2i;JWOXDi<+RJrJZgbD4Fy&xy-!MTym*5Ce+-mj<=}R zs*bfH=Sq2RG}Riq#p042E*7&*ne}3EUJe(E*_zBou{bA(tHo?vL#bs|A+IvyeHm7t#M^wEtHOK?m*3tk?OFDx*kh6Lst!v$eu1NdssLB&DM32-Q z^h4~jZ>A+~7n2>4r=NU@Dq}tg9ji`Suql$(%vHn+eg}v!7dsOm%)+nCb00000NkvXXu0mjfkE75f literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..97ded33b5558a0efbd7812296a52325b4170076e GIT binary patch literal 460 zcmV;-0W=4>kIV21T<&3Kg>clSSXlU}7 zNsTbZb7CDT8exhj%*e(#KY#)6xy}u?uzckjH(4PAzEPEh1-w0q?BPAtVVvU+$MuUd zim^-?Z*&k19JY)eT?W{4>I$)hC8bSG`$%Zf$FisplYAzY(o}#3ePW-OP>dHiLTyE8 z5z6Vv#(lhunj$1*cz0EVJ9s@A*ucB3BFy1+WndHUmWuEIZ$P35H8Q+=vQXg_LqaV@ zm?z|TsTk9IA=am%09Qzftuv(%OIZ4}NVJa{^Q2f76=H@QTgC>PWY~7OsDpS*8Lt)N zEWbIfpG@m8p5YBC;_%wCP~|(2vdmRx>0#;8WR`dQ1-2NIjXDoGtr048h%M+pCV0dp z9SxU>omUy0Vn}G0s>)%9(6-8A4Tr6%Lb000008h6JoGk)|Jm7Zg*wy=Lt)1ho5yGGnMn*+D_++iF&(%_!IgctQ{D6R|F5UKk=`kT)^3M#8p~d7z(!Vcvz%B7&o2<`PLWk@;d8 z$weY`fMlV1_>)7Nu^ncS^l@K>PACyR+!iAg+O0ybxa68dE#t*GqH3J$p;T&#J?Yi{ z%N%!fxuv0AZFN4v|8HEo+BWfF8~XJRLld;|)mn_HS6hP{I$Sd&UVGm+tjMe!vofGK P00000NkvXXu0mjfwGfJ_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a68ce43683dc06208c7363b4cb4ec321f6ba539b GIT binary patch literal 334 zcmV-U0kQsxP)J%A9m18p~kYr>2{MW)}5aa)A zBpNgeS2)FyWXzZU61WUv`Tu|fgX;0cT@KO4Jp0d$&mhkKFNriLl~8)BB+8g8|CtFH z#Q*;T5e9h?lg8T!8ncJE%;G@6n1BBbi8E*y0fU-|GsYdP`1k)7V5xu!tNvRPXAIN- zi(rGMQCgTlGVZ7UVw4(G12*V6R&Sv~2mA*45uHUE@EgSVe?3(U0vqM~zm{UJQL~n_ z|9=5kgr56vL%J-oLb?B6;pLwUX|kwF3z32|pEOw-86*pI^nYmTmL^RWU9rvov%ogM gng3R#%MMcj0Js{YKzSAb`v3p{07*qoM6N<$f|}iuNB{r; literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1765e9478daa9bf4e40b890762acba66a358c523 GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8K8il>WXNCo5FGYc6H1cL6B$=bcD_-gxGfz@($gssTLA|ie zkeo=dgGE>t1^U%OH+kxqdLu`dBr;Tps!UqUViT3kv?Ml(DyJ`$h~iK!^m2u%DhYjK zm?um;kk)U^U=vo8N&Q8hu-4QbM}%=8Hzc_vsB_|SM}|j&s4*ySOwq)T#kl;jj-M3; zLlG}~3W@|5xH%=R;7D^%J9nfM4ii{7HOMMFR&iRC8TR?9AXCISqoqQR%rQWXPq*|b l8pg0_FskTSVnH4M-~*;USqwV8p;rI^002ovPDHLkV1nY%i#`AV literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..996d8a3408b7f011c584c85c09c68ad5b6524931 GIT binary patch literal 421 zcmV;W0b2fvP);UsPR^10!l*ecSw8X;bHT8)ZP47cDAHms2v0 zQgEc&>C;V1H8p9HmkF0Bd|bFig?LgR#?){*F|GPyfS5V8Py@}0h_Gc&?Nu|k0wldT1B8K;m8Xkih{y5d1PRu~2?kC7dHF N!PC{xWt~$(699bUB7^_{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..803fc977026c0327203d0a137917eba4ad7264ea GIT binary patch literal 324 zcmV-K0lWT*P)(x16_%tiwKe}1eA#4Bm+fVOag*?P;@CGE7>8mIysTx>=iQ=PSij$8!wV>~e$zvE! zUNZk~IHQ?v^+@t{NU#C+O0m$xD)nmGP_RZP7MAG$Z3w{-^O#1ce$9~fkCcej9E;(SS#d=b%S06)lO_)xo${+$uT{HvlF@%qiFruVz)7 z@*%_qI(hX=*Wx;qc&Z57xAG{X++~%e418m!kgGua3W3ta3}T^tck8I9gNu% tu~Ku5Ur)LsT0N~g7ZF!B`rN~y_y_Z)Pv5W|DYXCq002ovPDHLkV1lJPV>JK( literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..707d167616765fe8f34d8e637cb2a2a1ba681389 GIT binary patch literal 256 zcmV+b0ssDqP)zJK z|MUJvlc)bXuNjm~`JeuC|Cf3T&Sn4kS-t-Kly_`O{%78>)n(FyvK7-bg!XV3_cwNy;D|IOb43H;OXk;vd$@?2>^BlGsOS^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..af7f8288da6854204dcc4e6678b9053cd72032c4 GIT binary patch literal 175 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+iGEW!B5R21KC-3Avpdi4K$<+1X zay^SH!@6{thMdKZi+L1hS^BMZUAdKCT}(HRw{NeWg~AlBHtr3jj>2B5kK^|J))MUC zlz7ydwCsb8xn}W?^r?}5wtsjdmvX{3=JbT!J9sKPrYmk={m|7^>A|Z*7j^D$`>}ry Zqi^Z9GQN4ct^!@a;OXk;vd$@?2>?I&LQeny literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..aa640629aa3896f2e18d858329f20b53ab21746d GIT binary patch literal 235 zcmVrJ(b+pF!*eGHa&9-%p|e5>p6)l9@$6v$yxlWK`3@rgGtqq98Q$Sl|-9kNqzpk l+F{I=dx09_)yDOtOuIF;OXk; Jvd$@?2>??mBq{&^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e2f5f35558db0965392b15b5d8950261a8c92aab GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1D^C~45R22v2@-!E>_7CM{eR+v zgY~R6ULP4=Whfl~yJVLAs>BdxJ>K;jR@FB~H~;@Xea5N(zxh7d@H058PdfN2J>(P6 O6b4UMKbLh*2~7YiM=IO^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bc600d35d2cb540ec203b55d6b6f14f256803cb8 GIT binary patch literal 88 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1c~2L|5R22v2@=a5*o*W;@VaC! l>t&1Sax=XUeprBw;o$|bA0mbRJAg_VJYD@<);T3K0RUy87qP)9pDS`rt8>j14g%vJK^s@CixA{JOk4QTA=JOC;K#wgGmprXKRz!nAc zA3zb4$$%jWbOzK>BbED)TQ7myeVhozrCJc(6r~Wf1 zGHagt&!8B?o%nRW5n%T4k;ON9MoW7 YNMw{fmt<}70caqDr>mdKI;Vst01Rd&*8l(j literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a2e4baad0c88f4808aadd3b33c8b163a29cb6c51 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*14^J1z5R22v2@a^dPjKrk`Tzg` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..feb85a775db5a0a38add95ebbdc4ac6f9129eb12 GIT binary patch literal 125 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*17f%<*kP60Ri75#pFCGaWWa;Ip zmrx3DS=6Y+YjKr%=|5&smqi-?`7b#HaPKNQ6t(EW6ya-40e(xwW)v;j%J5KuZ7~DG YOCH%@2P3>!0nKCZboFyt=akR{03QG+0ssI2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..910bb2a0a09fd1029412c996ce98f2e1d5a96cb4 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*14^J1z5R22v2@NM%x z{PF+%e-4p}84Em^^LTmx|7R9;S)}p5-<^4tn9>yG%ub~s%SmEMQx9(ySTD0`X`;zQ c-ZTaVvvkjMlFf-nm+mvS-{o;0&fnzA@EQ^BmzWWo}eRZ9~msAiWq$nrXiF)+AFy^vQr>(2u; OhQZU-&t;ucLK6V({vp)> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..59a2ec755e90e6be335d3d6c79ede6d85a7ed488 GIT binary patch literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1MNb#U5R22v2@&Hr5(PMPpn nt`GJgTe~DWM4fp6nTa literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7b75cca5b193508ede394f86cb7b7f1542627451 GIT binary patch literal 160 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1rX+877l!}s{b%+Ad7K3vkw8&y zVGw3ym^BBap1s7=*OmPtn;eg_yvRqBYM_vzr;B5V#p&b(3Dy=y<(6OelR1{YGm8OMrHi(%#ZP}_YC3s!qhRGUVO&%sJwRyy5_wl0Q d@r$Ai40FQD7cQS+^c83-gQu&X%Q~loCIBtpD=h#3 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..616df098dc6863f99b42c991e9e0050636f1aca4 GIT binary patch literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1KTj9O5R2aAgaz6^`k4d5nGKUl zu6>ASbai3e-^RA!4Redwf=}!Y6WQiS1#A^qC=mdKI;Vst03546jsO4v literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..474cc8092d0737af7949cc6c349104abe9f88726 GIT binary patch literal 175 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+iGEW!BkP61L=MHi{Q8 z=SNY*f}QM>PO~i8$vBDsMW>xdyw1h XS<4nSyWCKq3m80I{an^LB{Ts5_(49t literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3f47b54cfb6ada12a333d8692bf59767e568853b GIT binary patch literal 200 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+ieoq(2kP615=WYF39VFTU3o2Do zM9NYhEt+!nOuwPXtZ2EF8guS6O7y%Bu5AhSDL;5dZ2MYs|6TJ#<@Yp{a?k5O&{N)A zm;I&j0vmVR->M_o(s$PU`djtkFiI?Tz*G<{nz+kZ@dA?CArZ z+4J+g=S*of|K4?O<5bo9gHBnl^TiFk-ZQ8h?>m=}^&03f22WQ%mvv4FO#qKMPVE2y literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..19c3962b1dbb5a5abcd911c86e18fd2bafe0db7d GIT binary patch literal 327 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1rX+877l!}s{b%+Ad7K3vkw8&y zVGw3ym^BBap1s7=*OmPtvpAcfM$Yji-9Vumo-U3d7N_@4u;x1CAmVynShOXDH?pxc zPw}GcjiZN7ncn0u+A1v&)9m7t{!ofXD9Bbj+3wk6_IN`Bub}ut*9>#7bf*7F-KkDcMewL+kEDoZE0Pn~%+5Ftyh1tRY9>MMUk&twzMttlfuT$)~sg58~az|2$5> zvM+H7M&iSw)?Ozs-MF8;^sKf1)a5;`_3T@9E#l0Yflh6!{MrKk0Oh*H84}q(jQ{`u M07*qoM6N<$f>il_ssI20 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b2249e055c7dd0348824f31e4d1f63320cb2ad51 GIT binary patch literal 276 zcmV+v0qg#WP)fsPk_RPV}2Woe%ZQ z|1a9|S1Y34?ELF5-vOHysB*eO$Dwt%;LL^tF)JUM6ME{~ZO$18;Teowx-kKd%{hG` zEcw8>5bp72p*iPR2+KZkA%p?mErzzefU#?LCg7E=&B# wc=B>S&sUzDB`$N=G(DB)Hcp&$#GH%4Jltn(&bImcfEF=$y85}Sb4q9e09LOpNdN!< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c61e948bbf7441fd3825bdeffd615dfe30964dc8 GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+i6i*k&5R21qFBu9p2MDlS@OSQP z(BP0-#3b6u9UvHUOCMtu*=z2v@^=eS1UZ1g)78&qol`;+ E0Fs_H=>Px# literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..faefc59c8e574774fda7a43bcb2214bd109eaa1c GIT binary patch literal 247 zcmVO-uP3I_MSNuDLaanhs#Vk4SgG|lugnQ@C z%>Dmn&z*B-;JDSYTKuqdHOFzCRtwi@yXG{!raHea9rg$mYCyY$qJiwO_9M)Md*?H3 zLv8({^w^(3L29&5QQ#Dbe5j)t6i3w1SD6NXXGOn~8{Ev|cdMs27q_2FSA*;q6tjqRJ6 z?g{3)EpmLxaeU#Qf=eW{YQe5iz>UJqE|J#@zUI;KTkU>cRf*lsPyMG;f(SUjv6qrij(W6Z+kpZ37 zB?H{Z42P?GLYaLh4~L*ejr0ItmnJSMbSVHg6%j7+J_Q&oe_oX6z*)bDCxD3@*&OkH z0U-7lxSJaZKgmN^CIBr4BN<`y#IG2D-?PFO6u%+>#$J^zD&Fq_{@E7^Ke!R^*8umF zQE)9i%&mC82KY5I+@pA30Gtg%fJp@Sq-^H{1KheVSh=&y2(YG6O8{q=4&bm2Q0n`4 z-g@|~Y{vru+{uxN7EktVdV~|etQ(L3pydqZIuojmaK1FzYk+Wj{er>Don1_q3CY|U z3HGnqs(+5eerrqn#mcp>8LBgND3v0|fee@qG&vJ=3Cl0;Eyv0&pEtY}uf1^6;E3Qp&2 z7B)3@DL!I70eo1oTeNrzRzITOdkYX^tb;3i!k~BmcA-u$g$pSOpc}W0V#H5juo~ur zmA3#ggPQewyXZyX8&FDc-qV^u05$aoL;?IZD_D68@KSXi>sM5sCAd(+(gE7rL+I#( zHte3n-_D*Jth@!NEqR6Y+g?{h_mf;zRfsev6ONX1bSQNPH}Z>#r(kt|lIAVId;8+y z8;k+;E#T~*lklvw#YAp&`mNLj#s5nu3iA0$nl}KEhI05G`#4m|&bmgeNjiXTODqCR z(-F!-JFDx9?5w;2Fyn_yeNqpp=zf|rYhtB6ox`k@HtQ6PX9JAxT|7w>3$R{a#uB*x zoHz(HX(h@+=edzJmE0(;xyp68^@oR@l^6i>cM4qOK09g)_|-?7>YFoChwUUm(3$3B zt}|FI1%Pt?4)l3yYdDz3*XvLE_&D7*64YUV-u&l0W|i&$9yZoOrz(lH>&iHdpyohsMJCan4w`ZF&AjVASozb$ToiXH8$6 z#{5?d0DYjU3I$xukg-10H_*9p8DnEEYcU!&p{QXJmV!&)=ULHi0pheJa3XgrtEaXp z&gBx=7&O-ZQpjJ;x_%P!ctbU`6udC!*GQ@@geMV#>M6^lXLL<%`CO;p_df5r1mMb{ zW^RC@o~e7Cr33^pgwc73kI+;{&v@ZutPl0|n_||7qOd-SqU7rtl}Z@Mj$uaEe1Mu} zZK$o^7PDp-3jK;^IvCgaVCVS`usV9SLP`MFsLR3GzRZlS{mNZd%W83z{{UpyHF>Lk R4E+EA002ovPDHLkV1grf!KnZM literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..44ee7346e3844a18a88db8df5b47ed4981a2f12d GIT binary patch literal 288 zcmV+*0pI?KP)T(FN`3Qp#N3mm6la}KEbFt(9` za6c2A<4#C{x?(EWH8!%9gK(Au+XiS0Ik4n51)7GqQJN0IifaHndf>{kNJ09McK`zi mB0c1X(!XZKsG11!YB`LS#vJkVv%Fzt+A3BgC+~ zt~;}{8_mE6BuLu--`sX@Hp59 zU6aCDx^!+l2!$_$bI^BCI17o&w?9Z*cA8*b{&WqG*{N^qQf8$niC3o1e;)n z#!Yionskk@$iib_4|Hr6{sxy|)g*rCf(~6X##?+D^lTRT1g9WrlVivMS-Z?5V3qhC z6u}%v4#$WTsDo`VE8s(rE>IVI6{QP2DFG#*1eAah_;&(E00aykft~Eh@3jEtrkJ}AS#w@gYeknJaLvT(B6~wg`XAC0 z0nFQC;s$oD8n+T9F9a}W(WEI{@}jjEb@OQ9ASZj5BWpyx>W`+umk0rRmT=3=C%MyJ zH+c)Z0<_=yU^2fTtG!-zD}cYPfH#x5RoQ1?`ZNNhj|xsKzJh1c&(NsEp@V0oC2$Nr zEDkpgd_4ff;yH`rCP89yK{@0jPpwx|6i? fI;E+hM)1ZDVD)&ilSBp$00000NkvXXu0mjf8({p1 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c59419c02b6273e09063a0529d2239784818207f GIT binary patch literal 322 zcmV-I0lof-P)J%}fHzV<>iUk{yCv=|P(yFM9CIKrv6;vdj!^e)OQ=oM~3L z3Hov zYMc*TMS6^4Y*s#v8YK}AT(ig&XB4{J=afm7xZ$2C{CQWp)PoCx)G7YcXPY7T1DnU) UWG_v5V*mgE07*qoM6N<$f<$+V!T>O6 zS70QgNAU8v+(?*#6#o9*V@~mgC;x#Rf^DRK!rxsIH1Y+{1W$v50*~CV#54`Q2vW|N zVwqbWh7Wvdgwql&RGT?0V4Y7)9ZgCcW4f$baDbN(6Vt-m zvtW!jvUC`+K-kE98fAa!hPVoxo*rAD-<6TK|tHbe(;yX<@2!TrMbw z$A5#?|C0+kvhTLAz7002ovPDHLkV1hGqU|0YE literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8d76fd423d54a07c66b22e1c784f41cd27b9974e GIT binary patch literal 135 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1KTj9OkP61Lrx!9FFyLW!?4Nfw zw(f~=Sm(d9^S7m^r+@pFZq8WAP~`Bm=^X1^ZY%K;y%#YFatCZ0Dj7aAJYzf~J;U4} jKjGeiJqQ2ZKghoH%Yn2QyPNxgCNp@t`njxgN@xNA`f)KJ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eaaa180b6f1b7b5a4084bab1cbe1b73a5dc08366 GIT binary patch literal 201 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_+i37#&FAr*{UFK*;LWFX*{=pAy- zL9#({#_`6c&=+@EH;KJb$$Yq%!kr8fS_~Eoo6NiSl;05!UECqQ<8yHel z8j9|IQ)JA@^I_6B?~#}yEPa7B=#nK@&l}ch)2>!rnWeQ5=r#sVS3j3^P6{ckkrF<`~X@e{>T@_r}74_kj6Ry00000LI}LtPK1BLDCx3*cKL-E8O)?SLeGlL}ig@ld~aezrINl%6wbw}^OWW2`+ewAzx6=f3&rw~eKlx2EniL7cNC_beSYGtt|F zE2e&csz~^5q9bwFSKuh&v)gg1q9s{-7%}C5j0)t5M8um`kGgvhw=5du zqDJQWQU`IODw4H<9D;aCfn16v5U&d)CD8S$KnfuG=ISl1R7PbG53KF_<-XxxGA@I7 zY}cdO`3GeXFTgXAEqSS<3|#ZUdsn5g0*IS#NEwv(|KJZ@XZ{SRQ+Cw=0000&Hr5(PMPp1 m`Um?TR`_m`YtqBChk>Dge}Ca(gVG$JVg^rFKbLh*2~7ZY(iejO literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ac9427430a0614b8e109020f0bd3b02a1f1ac915 GIT binary patch literal 650 zcmV;50(Jd~P)NNch#_KIeS@M1k@z4&J$R9Xyg*B09za{KiY(Y(6&uK<2z`rU@MMgl z=tbQYR5aPkzlV>3h23R$CxlY)_g$Cy!zMG#s{g?YGAvT3$uTY-9xlf;S))h>6*qAY z%&^HdQLgyRw0R)UE*^1uIOI$MEBq!Ym*sQ;n{W8tvqy;n6J*Jfqd=K`9`JLpk_JZk zi3TUEGD7%sCE93k$Y_#)?OXndk65Va!KTWyFJZ+8zM{2xk4iAJbkLl*zzW(g@~DI} z$r0KIq66glgJ{e0DH0vT9TU-kUBpww%h6QG%+W>cL5T8(~1Bx1=$J_A0CgKFk zC}4An*a!<`xJIm^i~@=>;*wXvz#`(F5#zuZkBEg}ppMu>83z;xh*~hvM3njpR1l3| z;22TpD^NtVf&mxd-zG`f zQ4I!)hzCZE1Fv~R6v7U>D@4gSP(fUT9d?Q@h&HxSAj=tI9TgUs#zRz%0(C@}H{k(= zgLq~(bzq(@Vha@&$l)S7OrlcE6z7N=#-js@WwfIdi78rWOK}%32d%>#D#6Tij<$`8 z3s@YYJyXL*MGsl(bkTk=l9h|^_@ar2MBcWiJ;lnd5n*O~@oGAvM|L5o{@^th!( kgDM3%(#!(}^Aa$azmr;dV}N`t9RL6T07*qoM6N<$f?G8ir2qf` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c94dc6a74629d2bb776a030350b6ff0b903dfbcb GIT binary patch literal 428 zcmV;d0aN~oP)Fm?7{}qKdOH{zu}Ki5!EWflAO<7^2?h~E=XSA)$$&vZFc^f0L}D=t2DNA~Nt)`7 z#biLQ4wV=xxw)_8=F6ApDZ@GEHiq;3#&`Iyug02bv_!7E<^#>ZkOOXtT+c<@)#ikc zqEOcZ8&x*!uJ~)8Y*W!4A?KS}vld0JZM_Qii&WPQVzL^nMEQ>;)P1ZNE)pg%Rq4&nrrsbep=JbS|j@@du$)q`v`(Q-vhD`V=QWFN^ zut+^OAaB#aS0$Yksm)gNFkP3Gv{R&}tY#)%7nIf({TA)A(ovCmrnEC6wd8^=Bs*O7 zE8kA5;E|BiF|Tb=$a$fXmiyu#O&e9&ar2^>FQ%+hlYOp1)?crW`5VcO3 z7IMJ1scz{azh_gqUL1M+vBGUim#xw1EZ%)WJ}X2kJiav6MJ_(VR4FP~EUIoj=b^Ow z+fRLtb8-TnasC#`HEf&Ad+u}3eUY^nw6!hrKFl$F^(1tsPIQUkF{z7IsX(0?^&D zOA5m<3jYBNi%A&<@ZO?$P9};P4Y5CjG5$M&YXI45J{s}~# zf|&?x1_gn4B7+hS@X!l}&!voFhmZP^sujifL@~PKMMM~{6xH}^g$q7WOzwCQ5vHTU z6`v~H@rlA8e;CUh_(b84zg=+ih`wG<)HiJjzSlQx5#CnjMR;A)R^jtaTa9;7rSy)7O%~`cm?ZjXImW?6TYRT<;U^@VKiSj`soFk00000NkvXX Hu0mjfhD&W| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a9602d11b8d4e33f91367f9676eebce86d88c117 GIT binary patch literal 405 zcmV;G0c!qYU6oB!gw#7w6AHZ6C8lfO9C{)|G5Tytb2Nyx8_-E0{MU0E9)CbTPaFR+9g|>FG zON!-l$YQ|cp2S{o$amIoesFUk5nX9%X-u=i8ISb%Vn~+#XgXyo36S9yZ8t$e2V4-Rlej>QxUI$k=IIl+4l^2Hi5>3g5tf5P zSi2^mfXj%e90Um^I%K1O8Urw|k0h4Efi2LiHxP|5psHJMBs`wDq(mkN6x4@fCj6c$ z{sdh0dOZOLO!z=Sb8`|Ahdxj@uTM;r5TR*YpPXou72eS$!9iC=7+M8f+rW$PW)<*+ zrfr~Z6=(^IjO@N-;o9ys74i(QQ=6P12pia`j0pa0GX9Q&pUibW8D|Y9gMhHW6{9ho zeL3v6CC@f@yyB50j}|T!a!3%cI4O_<{D0sZ6}?4(_}0Fh00000NkvXXu0mjfY7nTZ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..796ccd22559a9b362e6cbb0c804a443bf1b3c113 GIT binary patch literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DXipc%kP61P7Y=eZ7znUlT&Wh3 zX2lgXgX5!|^oI$(N1D&Q`D-6>CQj>eo}T~gdcQ3PU;8~!-+H4~OaKZjoIg)3i005} qiMH7Fyj-Y9Kzr+B%hn@D&gi|3kyH5eQnCbS4TGnvpUXO@geCyhvNFd2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..388b5b060af924493b057a63216fe7db75d4435a GIT binary patch literal 151 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DI8PVHkch)?FC64-Fko=L=%?Iw zv$u;+&1tXu2b~*Nwk`N$?;xkbvPe<$)w&zy|9BUkVz|Zlf5mTGpk6SDXXMMa|9v2f u@g#fN+oPXnt4Z(ZaQ*pHX6dTRMUVWXo;a(XeY#{ZNU5i*pUXO@geCyi%r-Ls literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..40c572c306064f3de08ab9c831678ccdb4c73d99 GIT binary patch literal 94 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZA`BpB)|k7xlYrjj7PU(^p8x;= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9829698ddf0ad7df1e36f212ebb6b00c89d97075 GIT binary patch literal 431 zcmV;g0Z{&lP)2|$d zb8`?1e$Og=AO=p3^FQ2v1vo`5um^p+ctP+U`du{Cg2BIYu^K^T*z8PSr06@ zpYw0-H`M_jNko7vwcEfLL;x7x0$h$_z@D56d;>86zNo-J6zHkIN)+%_AQS}x6*!9m zkqRWDK>P&Y>5nKxvcT|~X3kfEK9Y4+powG+6>vF-{mDCWD&Q~@1Kw`$gF3O$!g;S- znKKb!qTk6CPGs#5B|YHKA!{wuhu4fO{lqZ=9vf_1lQ#nnMN49>{rFYJnO_`r@5V$XU?42SH^tMqdfas S$Tex8?F^o-elF{r5}E+-dqN)o literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ae36d91e1d66e1be96f658099dab13c9508fa96a GIT binary patch literal 179 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DDo+>3kP61P7dP^@1n{r~mb03) zc?j%JS-?17mEHc1^rNrOJ~Oe~&p-eC^UtCyO*)fuQe>4YnCU(F z7z_Vu)#FTIDy(mk6)Jx2XG-h%-`&xnZmwp{zr_5kp-!2dN$ZD2FB1!&-PY}r6T7W% z&vV4-$=RgG5}(o(KV=ohpZI+=>7KsW=Q2YP$7e#!c^^(-Y0%@RoZ*qi`F<)`EKq{{v+@Hw5*sR zFA(}!c2Utj$B^^;IgbBnE;xI?rFh=CsojiM7+vgNGclA;TJ|n(&Igb&p00i_>zopr E0CTxJ1ONa4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9416c70ec0a0a230648075ebc449e2000f239d72 GIT binary patch literal 103 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tJx>?Mkcif|mkfCs7&s0X{F%+h z&%)%Mop4@>VctnaroRklqT+)Y_}1iWG8C+2XgDPM|LJEjE1*sWPgg&ebxsLQ0G~@6 Ar~m)} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f8c91227dfc2c1b98040477d160fe2c5f42c5307 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}trX+877l!}s{b%+Ad7K3vk;M!Q z+`=Ht$S`Y;1W=H@#M9T6{UMtikG#4+!&fPwkcX#>V@Sl|x0g5aGAMAc29`vo7e(nV zG!#oZWBy$$M?ox|jp6;&9}>2inV)yw``7r7QDNtk$*Yq0*0;Ky4A83#|F$Z|i;clk ZSZXnw*oL#-xj-`+JYD@<);T3K0RY9dHH-iN literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e63d9cd4a2a2bbe14f6e01f3c5d4c2a0e029a585 GIT binary patch literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DJWm(LkP61P*DU#56nR(z``*p| zf71E~_a*PPyr2-(jW^=Xp4@4-j%&^XP0>G%3D;uN;tf{bxbr!P@k1xWk0T5<3mEJQ z8RmO5@Vh>ks`N4LLH~*PP4&zEE&dtwuzRll^?eV2OuM;pDU-#%@PfQq^0qo>QjE8R R{07?0;OXk;vd$@?2>{kkLi+#! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..35b7eef2a07ad8373eaa4803faa3021227b20829 GIT binary patch literal 174 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DQcoAhkch)?r+e}pP~bUIQB~*n zzGKsrjSi{LWNr$1Gih3WSMxu{7&QA9S{3q-hKhL+3 Y72P#S@Ko$epaU2@UHx3vIVCg!0C2cKX#fBK literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4ebc46b56dadf35505668f454745c822a913ab45 GIT binary patch literal 252 zcmVcs=_>48HkNAg=}6iZwq1Hw!{B{sidvVzC9xo>&E*| z+!XWAII97StaX8@G9V7hyRi5Goi_uKUvhxX_c=i4NdY=<07TvjME*?=20-KufXEvF znU5*Jk{5u^pTPha@&b@~!$%hR1fcQ;;5UB)AE5IF;3)EGV8=D`ZXk%rTY(@B;0N>? z@Rz`g)$nGDyIX)E_XkA12OVq?H|E5TGtgmPTiXYC3r5g{(m|>K00009Kly>cAgv3GD+BhRbZ5;x;CULb{0sw@Kf!?I0|qP)1Df;2!>A;^bm0sMOyR}fh~lHkZK#ocm>^j+MLA{9gfZ!Y0-TD z%fb1;zz@U!l7YE}y^D*zN3RToYc9CvroQ{LZzk;kJ#VCXtyd1zUw^Xo(Wy?90#h+N zV|_u;H}Of#%MxG^zdi3fuRrm^d%#!y=D-}Vi?9Ek_zaje2Nr%$8&Hx4X0qB6%fsu_Tw|2BPyapKgsc8qpJg`yHDi1sfd zaJ=`Q{#r@P3=kLFpwz-cawxV20<$Z(Mj^ZeYybcN07*qoM6N<$f~W?X AOaK4? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e2d109130ee373561b490926bd4e56051666bfe7 GIT binary patch literal 343 zcmV-d0jU0oP)p3tS7xqJ;y3vQIM^pSyk=TedJOXhB1xS#W@cJ+lDAAE!9y1zsS9OR??+ zPYRb})dEA|jcS3R@ICi}{jdeiJf*?CAcHOL*(A-iKopJau}1@GzU1!}c)@2G7SvwBM$%o0jo(wK~z}7 z?Uv6<0zn+dKVw@`7wb^r$y1=Gg!CG{L9le{1)?YD(lzo8#3v{SMS6lBATuOHx^>(v zs*A8GOb3Tpb=}#229m$avOBZi&&+S;`P}fDQJ%B)kE6^{kJ|E5z+s zuMmKYTH2Ot>jMKvuTX##0A2SSi$3p;UZDU>Y6jDyuiuG@S=XZZ2e=qbrZLH~5>Uye zLseK>fJ}47YRKXCD2*c~Q)Gsghc!(78e}7-Jg2XwD)-_fn6j(@kX-oS9|DX3`()^F z>F(Ib0EfeO*^wo`2>&T^I^4yv(E(X+$3_RpS?{9G$Xh#pK$V`OEZ1TO2#XSr*w{3= z6t6Szfv_kqYS9AVYZuwWb5)(OLBrP1pc84pZ9Ty0*x(nhnxCuVHYT~r3y3y3ZEF4< czKFkzAIipP80Ho^$N&HU07*qoM6N<$g1J=XJpcdz literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..74068eae0d0121441c8c15b9437c91b3faa012df GIT binary patch literal 101 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}t9Zwg>kP61PXBGJv7&w>>|NYOr vpwSdtHCcG=3ze_;ezP+$lmnFk!KFziX4m!waNe;`1aUoG{an^LB{Ts5hM^k< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f49aed757118a941b567629ec217cde1aaf257e8 GIT binary patch literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZA`BpB)|k7xlYrjj7PUzopr0A+FkzNU)tSWj*atv-~X*s?Y*56C63l}Haf-`_AFX(UV&He zi|8WNZZmJ?0ZPyY1~p;;jn1oR{sm~9g8yFomX9~$``}?W$*g;`8MYt z+ElzR?w>C+IlV{au8T*R*}q;>p?7?nz2@&ee=1!r=9vGkw{v_Z*9UK2R9n-U)@O2e zb8r3M?|FG9`#0_VEo!M8Q+&)s`AzWkKeM?Gp9y@d1(Inwz5{tS{@y(b0!6;WM)tv? bf)CtD!XnA#y7ltF7-8^q^>bP0l+XkKVi(I6 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e444922eb4330bf621838552ca07a81de556143f GIT binary patch literal 485 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}cwPFs|`*aSW+o+;VmyV}k<&^G5l< z$Bwb~9;&I{XIZZP{C45y2&RoJNt{Q7I#dO`6{b1pG~yL_@c!S&YjJjK1zH0o?3Z29 zFut7OGUdEMq{mOLO@=AvXFSfHS}`#rY2_Q;-kT8}ZQw&Y;<4oliieU z=E8$J#UwtgS1H}j!BEp={mMt#%;wGOyh-Xc#qQI2`CaecWBL=k^`YYG-<9{~x^H(d z*F442H+%C##l{tGJHz)c$aCGBy{Us&Ua9Ze)&AAjpVt+M*|7ZJ{>8|kq1?gBKfx&^ z*=B|P`gdJb+j*3oLPYOe_v}>3)av_vEaXd*?;qyui`+ssk7i3haw<7nSubk2WNH(i zo<&*{IocAXSw@qfzX#HaNS7<~+$ Lu6{1-oD!MPXn~%&!gWE2a57K_Y!t3904wQ0RRCau^`|Q|Y?=6-EZaC5}*!I{>^M&8cyXA=; zLBDUVmjRZXi2357%8(D>k*{KoEe;1>0S?@EK~`cHJ#YwkIUMMMLsw+VZ_yDr7!Cj* xWg~7E_-B9r)j0qF002ovPDHLkV1gc;%4+}s literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f208795fccb514a4b7b698bbf9b3550c5cbed580 GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0D>7Fi*Ar*{ory2?#3J_p5=S^i< zz+kd~N%R3zNCUUe5oW#@nU_^~t_E*7>YF#eLHFOe+F8CAO!U94SW~mpXhze2wZ3%; zsn5=x6HLkX>r;rXFtTcTbNHrEN@uu`N}TV63RlNRS6Mp4MZlcZTw-!PuJV=7($cI7 z4g8O`9<(o8dakrd=SQlVMgTe~DWM4fm)lPg literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bfc3e39394246b221f7d4617aa5600a6406aa7c7 GIT binary patch literal 465 zcmV;?0WSWDP)D8CL_;(V?AVEarj0ldk$~bl6y3&Eqek6=6UA+aBGp8+4bCOt0yJyz3VGY| z^>9EE59hXLP{{L0Lqkciz%eblTs;71sDpKM$pE;wf;`qWNA#EmA z3}Zav^tfQ1X+{`jh7B(9+LToU=QvL+>I0VO;G8NRYdGH+S07Zk#aU4-eqf#$*8o)L zVD1%%1x$}ceS&42s$y`AxljOCm_x;&g;`eso0uELpo^JS0JE4E#URIwD1Z{?O)C5yhj~>DnwSj*u!U(V21l4n1#pdNCjP>$mkQ{TM zA}DjpU_DNl!r50btZ+|1chvL?-kyr0${{zr@Wwl@G-*)PA2jglN`V8sx>8^tZ&xX> zhqt2?sN-!b1?qU2QeYP^Qwr?hWJ-f=e#?{&8JUKL{zpFn4bJ?-H56sJ00000NkvXX Hu0mjfDd@)s literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6f25407a5fc058e38bfff9c3ca2d191b62ab675d GIT binary patch literal 2939 zcmV->3xxEEP)QZ-gd;Hq+6*B9d?BCM_yFG+Fur6R#+RgkZOIfU5Ls?J z^q4lZ9nzTsJrdd>1rlhWrL^fJrW{FW5}-rdI4KDfY`(s4vBcYDCHdH0ugv>qzOlx; z``*+4xBK>cr#OxoGiJ<~F=NJzSs<9rHjHa?EaF@aC-KWY?&SZ6$CxQ#070D7F_Q@0 zO#C8o@ZuNwbMRcehS>nkxh<63*=8c@8RB<*2ru?K{55zjropTaw!7squHCVY_)#Ka zmp74zx7lHVqnda8>;py@pBAQxX)}|s@3uZn1j;009VC9ko0vhwXMUpwwwPDKx%{~R ztHOE5&-gz)7UcWMxWVUvd10Op_MN_aG!Y|?_>YKp`l?bwb~J#oHXBYC&4=z)lRd(V z9gpETcrIS!r<&^Q$vciO%L8Hi_6?^Zm@B>n8o$d1Ya8?7ck8|mw=yHVitf$v8oU#~LLB>lZu)p*L_#$hHPk}oM=0US54IIW4 zxSl=L=ed{`rip1|9)4h4;LD0-4(te~DU@^BO+67zx3@1mvaJPTn#$k@6|wM1&NQDw zewh0dY&Rsqv#kagM!k5;>sR;s$-q1?FU%A3_5q^%ZrRCq*j67Pk(9G#JSlJq@gu$> z7<^G;^s=iu5w5J6m2vPwOF4}2BG{6aiau9S=$JQ_0n6g`<-KbQa5hsH z@gtngP{P&Lj@O>>E=K|p{Q&uU)7RrYecLA3Orn+Z`E&eK8B^iavZau+c|DAAHv5hH zpYJCC+ObSnHY}qjZ6^W#QVnoc6YV#NAK)y;1Z{}lW{>sNNDBUDBN%!j7=JI#->iQQ z@+}20Zue&BL!(O;0JLLSvCI>!RbugJzeApiSS{TB7O4oPm_#3MkSL@p(?{4yj-4!C z0F|T;e`2SlPogUc0R380D7+UR3_n~o57J9h;gR}^`ytp&7A-shHu{CSk5nDLBA7eG z-y;!>r6vQWZm;dP=t=_kZ`vgITiir}zmpIOyK)x72#bLNV76%rz(dX^_&iHGB>YG2 z)3D8u49{Q$QnrH1YoU^ z7r+QF-Od9AaARc{obw|-1JE-DAUA-nj4*gVA=t0@41fYv5C#b!=|XxXJ_DctfPW>2 z_F8-fKxP2fB^p}{fJy*vk;c}A_=ya_U;wy78e5lE1ov5d20$hNR}(|}B|ZZn1Axxd zN&Oa|0T>{Fo4ssoF#rkx=pyxgZyQ?-fC2#e(%51EWCrl>J~p-(02u*%P8wV1WD=hN z=p%sJ{cLOn9)R{%tJcN?u#>I7jV)jPXE6Ya(W-@65e;r3a1PJ)>`0FgIL~G<0Q@s4 zM0WAf&m>HO(Sj$y)Sv|bb#5U>69Au40Gdo`sss3RfQ>C*{-+g_V3YuWp-uw;>@_ri zPpSExEv8ge0bpb6eEdYk0GwVC2BYX{DHdam242Drqi1G=vqdn$w$+%T8UUQp@IgYb zV&bF!fdCjOm=|O+Y5_pyy)oMsxVCzR2fz+Pl4<}x)`ci4KKk3b$uQhvq@Dmnr4|6J zrDr{VOwD#~*RN0oK(uAO;^L#fnK%W4=&6Yo<613*p*ge|@8`|-0Qi1otnvVMZh^54 zRWM(lC#x^2S_6&wO9TK8Cr=a34Ye3@H1GtprqY7ETkwnrz)#ATDJ#D8^l{l7@AjXq zeA)xxSE(~alXDHJ8h8R6F|;Tri=#aNaOQK6k^q`&6%*g>-^z;i0C;ufZ1EEiqlGGH zJ}t^?8x{*1MK4$DU|Dk+|Du%B$2Hd|ESH2`405B7k%0c%cvT-GS{353rb2$zVbjfI6r;n=vV78%9kY}_$mp*=2O91w`0fg_U zlU@8TBxW?I0U(*&@pPWSn>Nr9&>rmoXlt#fdqg|^ta$H>-_8hwixQn|Y5>p|E%)U2 zN9+j|4#w1L2!KOJM?!C}T_B72II{JjPP&Iv4FKQCit^;QFJ0USGwQShpuOoy8o;-! z5@cJe>#bu!#aObtD;6>pk9aepW z_5j>{51e%an7h?TqL|6Duhmrp0CqXJ^Epd=hOw9)2R^Ia4Psr6Ep#<>rYK7GwYo|J zz=>CHQXO#n(`E^)AFF8#r&)8gM$$WFl{DtbzE)Rh0J-ZE1^HxbNTKikF0R2G$!XeL zjzjcJxeur@Vma38DhUAh1UjA=;mPBTl<=qU8}1kfTrBV zf;=)eB#X~ku6+Rf?&hs0=uMp7DSTG8wYo|Hc(Ec2I^ynCg9l07FPemPB5-zRcWqrv zZ&kU_p@-X|%~x*?aQe8M076J3>y$3slQxE+a|{LaIo({NX)za2=5p+%t0G)0Ajeu= zxdDu?D~H!oX9&_RD~=VNXWYvLoV9J8z_mH9x&h!utNTlr40x@soB+nvR|@9+pu4i8 zVVKoOpLdm1KNGlsG*@dfT@~SWlqXitk!70TKmmNEt_*&j_LLyaU#8B4ha0MdRgcY( z%muW$TDRWYg|-PECtv*alo^8xAi`V%hf<~s(!(-6URx%vdbV-_Z#LTqe$RfV9k|V# zdiq*6-Y0-s0G5(y2S>N)w6H8#Cefat2l=uO3Bw;tu-6@@zaZXiIMq)r0N9|~RIrrq zTBg&%GDzI{$2qfki2cBWP5&slt4`5h^h*tT*qyUbaR8B~wXk1Q-=kyRSO(FZe~K^b zkThG>BjigS6A%4i@8i4i^7jSdU0+cY1Ak2rMqQW}<|$Zgao3OWWgOCGvyCBl*(+kP zqN?-YNYYf<04&n4fnTTI|FXwCFfWOHpkLw3_COdm>2ZxMhIyjSu$?)J1_S`-?l`j- z@csDWYcOqzO}M-HvOJJK0*5iS)pSj&U*@Qvr{TG()%P{3Oohw%O}547Vp^D{1h8v? zALkDkLQM$&>l7k!ut8+aU*h%MfD0!cYSiIEW;H)SMEf2Q@V3<0T literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7e306c303cb42512db0e5354ea0fd3ec38fecffb GIT binary patch literal 507 zcmV{_!j` z(65rV@qh=MM2Lnc{VF;T-}p_AA+fQ@0*E#)O97&dFAWf6S-=Wd0V}{;2=GG(zFGh~ z`e6F310P6Frw?J8>A({w;1s4uI&i}YxQgkz4qR{oo?$wt14kWz3O=T~4hU~@0~ay9 zMv{PTPN2XP)1_qK2!1YL1Cz($WI(u=1K7nB(*Q{WS_GMa%a{V1>40!IBe2E}rrU@O zP~%$$V1KlpFI4RSVbdBY@szOhh%F!ttbn~dByg;HXZ-~002ovPDHLkV1k+$*vtR` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8ed175d9492fce91d277090db0b2d860b74f9741 GIT binary patch literal 1208 zcmV;p1V{UcP)9ueCF9 zYGcuzyL;#E%-s7u@C8}-&YU@)yYsk@GeW>%Fc=I5gTWlJqobo!q;}FYOa6TdkqJKV zm84$MA}N!jMgCtUBBej@wWJ56jbj3zs}25cEh6VX@Xe%`q&*LT&($6u+l)vS9{3n( zl9VbU@VQEHSusR5f6yyPw@3>bf_{>|avc?jcsB6Wq&uXQ00W<^6|S>7%7JerJtb|I zFz~tBW*-`(67(|C719{#z-RFGNv}15IAEWoZ)IU!ICYRdkia+4m|J17AlPB5i6oBR`XFkjhV}E9h}zliO0~ zLf~6S3GSc$_Q~V2Pxeq}sRrzQZd;3u18W=UorZ-|!Lt$rGp7`{waw^&%?qb)(l-r_ zS}k^7Zg2{l;kI`hS=dpfYZ@9b&nwe%AAV#k(3%MVetyD9(ls4FVM_pHTmT@iPQDtZH7ZlU( zeiHy<5dzSw@FQjdK#gK%5GDZm24ES38ezgTz5Qr40Pg?uy^OLXOc}ql7Ipxa6+R?P z0_chWfMJCXT_yq4X-X+$B>?VzzAO6wv%;j21G6CppqlA@5vGlt7zi9P9#SjV zm3NLD8VmyfDzl07TT$mkF}n)NBnI6Z+=0Qi!_TMza12-}StGwT?DyB_MAbtaP{IeJM$UeJ;N;JpV_ zD|y+Li^3%%hacGvfKQYi5A{40u9-|jMw+t~0MGZmOc>_;q*OBvK{oQ2Z2)A}F47MV zE7CK0+U z>U_m?veW?h#>)Ew%1nlKhYf}>RSEz+3+pRzgNUinZ*L;AD3T_E1VEo(=lx;|DQhC= z43D<05k_?a_-}W;k42O->g-(K=fE7-@O)_#53ox`9DB&emWikupS4>)H+d-LI9X@A zfc<;4Wu}Cev1hD#q{Onz*!_w(QT%F`>%h*dih29E=7+sfTxLBY>>Wg_hHbC@ZcEjQ zh!zhb#Nkla9V}f0{{|82kXi@9MRd@?$weK4A^ju7 zLRBcZi6~YV4Y?x0YCBk^ee93}@A5qN9`;=V`Fy9md5-r894+Q7Vwa zKGHbsTJ3CbB>k$}G!3du63TIxO+;yvI}8$@RV#hZwGsJa1 zG(UeLOQIW*%YP$A_S8JsifkZzY=a`Er-+B1U^vMR;-MW*pvdqV@zg66vq)lJDKSEJ zw`0sxb4<_B;v8FuS(#q`)kg&}+hPL6xOrb?GQ?%fExvFHL!Kq|$LU4WPhFXQ&M?@z zxR1mtVOeOIy~lo>;tz)rETgf>fK;}TNLegBvB^KiQ=u3Ot|-Y( z2Uu+mzU3;1m}L#YE?|a1uJMlWb*&qR^Q=+kG@*msHr|$gaEk##XL&}z6#u1a9Hidf vu2=2VHre2#z3N8+Q5wRd6(rV6I@-ivWBUX&LwAlh00000NkvXXu0mjfs0IG% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5caedc8e57497ba9d32c3a1798f7394bd8ca7cff GIT binary patch literal 562 zcmV-20?qx2P)lpy;7r2s24Mgk2IZK^^iBh`hXFsLp$Hi^-XdC|o$nN|w}JYolv9aiaA3)AHJ#jL^+Vnw!iASI;~WiKXmD0RMEHO!?-j&6;kHOKLX;?@ zWGE7DPJUcs54$WqYC|tM?Dm*a5EB$J%My_f5ptOQ;JnO8utv}pJu;w=5<;?v>&>8 zhF?Y&EaUf-F4>V}ouDGUG9XTwpfwUQW0DePIU@2QLIJZP6AEI68g@DQ)P^_(>}pKO zkE?_$vBVfLVvMs)nQ&JW#5%6Ll?i8P;6j~onehS_p38;=e>tq*49Si(?RM$JF6|Z- zhLe1y=@Yj&%{<$LDKkeuxA{!dCfy3hWwyzZQVXYeKoyfe+#{|QF7lc(Gdc+Jm}KP& zE12AuD=c7gL#~j*pF literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3da60271c036b2b2f87e81fd5f23e4c30ffc9d63 GIT binary patch literal 375 zcmV--0f_#IP)hj0QnaL=oM* zb0`X5VJuuYcz4b{92mIIQ@{8A!eKxRDbZQz17a!kfkx;9Vva!0k$2Cm@~0Jqe@~>i zLf$=+Vhwp$CPkhbhUzR^VWu6R< zP`5iq2w(yA+8~Gq>UK$vAUdeqHi5KJrmWRNABbp%Jpd6kV&vGrGRp}cw5bz$?JOR- zN#wgTRZ*gv@6OahiDKWKscC%ZwKE-~L>1qismL4Z^-9ruXQD)nn3xJhkKI5R`UQhv V`7{;jV0Hii002ovPDHLkV1nS%oVWl0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..16b6cd01fbc994d73000f532c9f0c0623d754680 GIT binary patch literal 403 zcmV;E0c`$>P)hj0QnaL=nAt zb0`XL)4`S9zQ4QuU}5iX{T#1w{4{DfBH0BR zqL~z!Yt)-BNT14zbo{ z8My+p{9vieIg7|0H~|127(>>x153nhAs;{s0Jx^$2j~I-yT}jFAyams=^F?L{Q)u2 zAo2j@yP*Q+Ipd2CO#*~4@7#ZKNPsZy>_DAl4TNcD2ihcy5T>0S2#r9P6*whX4Pn~Z xfifS&y;DY*c6LBiXb=%mi$AmPhV3PffPX10FMX7kh8h3>002ovPDHLkV1g}9qsIUM literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..62501b0bcd95d980c2714e660e3910b9388b9882 GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDD3?#L31Vw-pPk>K|E0F#V0~QCYR{;6UB|(0{ z3=Iqizs5`jazi~`978G?mmc4k%go5bvf=y_^HWos>O7YkM@Si1+wK2ZJ^y>MkM9XN u$zb74?#HgQyqRcds#u+ZTc}z8Gr!s92~V1G-bVl}VDNPHb6Mw<&;$S`CO@|T literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8ed814e1242707bb04bfcf5031707c05c8e499ce GIT binary patch literal 309 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}b0DKb|g*Ar*{ouNWG!Bnq@Wv=`wD z72G6K+-7ajXW78avE-WEW8pH*rSpC*&t10vPwgf-`}hAhrCf3QSLL0wV5<0q^YeKl zgqi>DX^gvfpmdvzR3(FXrGL>JCAnbt1n)^oX`k8hJ~J{^GAK-7H}JJlEp~X@=kk;yca(4o%6|W zm&m`iPeNE4%sw(L@Z9s1%c1**mi~hMpPA$L9jYyw#4D=}37l3 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..28b5afa9d441f7e58d63d0797638eeb10584e0ae GIT binary patch literal 431 zcmV;g0Z{&lP)P*@Q~2t!X1e=k3L|$sH+Y~FDHYabh zPQ@}s6FOyFA#60GQ>OGqgx!AVgl~4nA5QuuZ;9ivLP_4P#%dnR)5EBz&$PUJ)Eob^ z%XfKLFc@odLL#fg(P+$=tS-m;Jd(w|XvPMgwClZMG-rpe+VRCew8k-sv}z7V>s*o6 z#Yo3}Y21ml^!lJxZxtgwgBG-6-u7sZBWjwdIvDM9Ni(C7jl0%!E3#7bP9v`sB0B@h z)-q>nl(K*gJ6J1@a Z{RW}ye!)7Ir*Z%Q002ovPDHLkV1kE(!><4U literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..480145c087981521ff0b0c874421b6deedfcc4f9 GIT binary patch literal 528 zcmV+r0`L8aP)-32ZI1rG}y*L=3~B z*+|iX_z&j2`f9!0!#&4y&$&a<`!k*CwVdBK{;vEd%3kBgh%ZJv2b}c{KaF+@cmo+k zA)wzzl29>b(yV2lZQDcL|F*1I^2i+*RI-3{qUyEu_PjM?Boin=0PF5Kkp+ZH4iZH7 z5efD^b*?oqY|$YUAUO2ga1uD~ff`Vlpyr`tjlfNtKq|2;Pn&KC5xUI*r4!8R3Vqf& zGVw~D*7SyO#*dB=yM{yv;hKZeiMlI|f6)vj6VuJ_V6jwUK{NxsKII^~ENR`Uo+J=X z`(6sMW3bgNy6T`XQ8$rx?Cz6`c&WK5kp_fkg@}2P1@u^Jn^;lJ2EvftoWu*TZ9vEd zgv;u=iJEb7R3J=~lejMO0AV2~5qW{C)iQ`Qeb7#EBBX6Xy(kgVH$^cbWNltr3pUb; zY@ljIt3IgcPbWh525ckQS0{x?5_y3zuBN$P#)L>Aasc6O0=O-NB(c;U5T0mkP6$cj zoxXN~ZXW@>R}qrLV|}7sAe^!VHua0_F7ee{N#+^a1JjboFyt=akR{05Y5zCjbBd literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fdad9ba4dfcce6d5b9470a9a1ccbea97e30f26c8 GIT binary patch literal 947 zcmV;k15EshP)$jkt(0Ud-j;p<@gHib;;sgIBWy4H6k4K4OFx3(7TSV^Woyf>*wq_L ziUKifJn4<}JE z<`i4#UXf})=mEM_vZ$ET&lA>x2W4RNq#A?KgI4fb zrPoMMFD1PGh#GW7Kguo7j-cgP+QMrzTF^U$%Sn{6AjN5#h&NG!F5`6%Wi&`JgV#vw zpf$uN9OFS*_7JPBf(B_KiYVhjio1vg`S76Ih$@bipbYznTj4?Lh&hy%ApI729Tw!^ zBFH>LVh?34sD}#TYH-jr;<3db#S_F-aL_Vh!fH?v@h~`O6H%}lG=|s+ z4%$Hsq0lEjf9Nn`J2v$~x{)cidy&X@3|Oqg@>< zR)d}*CWC{nAwK_Sm+Q~LK}T^BXaE17ABwk#d22xnh%zcHsEF9du@aP}hPc`4M}7lw z$4by7!X+0Tq$nYF$rumn;|t;$s#VY^UiXa$&Ej>Tb&z5m(cqNPpmQ|!105yk5?)*M z8w)zdN5pq7Mhj9r!mDKHb<8220t@4D*5O^igD1B62H=0Mt8m4 zm!9+JO7zFhImUBzGpKfhQoKaB#pzf%=lF>31&7-YQe=6LuE7l1wqp92rHSruoY<=c zUlQzbmrTSvYl<)6%gH5KzGQiY-#!mGPfy4bb%6zH_`SeMx}Hig$H6K*p~x7+`9_(W@_^e5k~zXvrg+E(+f=Air@}THEHcSY{D6u#3cJ${>IQX#{sqo1 VnxlwN%Rm4C002ovPDHLkV1lQvvMiCb-BoQ?~qg}X>qDF35TPU$`F)qliBx{AT3yO9@$d9c& zB_$~>nH6D$wIU_SJezGFcRM|&XWwV5o$t9hulGOjxjCIvhhXXiK@b!ptKl#x3<`s$ z&|(^p)d)32Bre4Px=`-;MGKWAXeVD#o(5>75ceph4Nb7SMDFK2Mw93PML2aCVVB_z z9o(an9{wS#CLG#;tnRRt6h=Aw=|z|ZS%foBt+TY9TxOC+n6BbTCCX|GLgAR!n7GDP9jXlh_e!5YQ%{%2vZ$#W*|(hIB^tVx`cN%2-8pI;#oOE2vZjo zcy5|OAXPSb;8ddX}Ow~#v8`Hph-nlij7U=_{uAv><)Q89b?h^*G* zRm2sP)qT8cwI2HAcoxx*Ol-ot7G$EHDOboa8tWSQ96}~e;Xp4M>mB(VMJJxLO%X;3)!bkNWz~Q) z?Z}P+x=HhhG;hfuJ6=(YGt)?;N!7;!oJ!G#CR{gFxW^7Yp}b`{PeejWsO4GA6ZSJ_ xsUUIl*oUk}Im`+Qa3uV7gh393K|v4%!GEum`b85=kfHKF6dK{HEPibm;W}aMg z!aH%T&QjH4v-9V!-~DF)|MRx1mGl0e{ruBoqKAr*rie|>zL-OYpPg34V8~9p)RQ}> zDv3+_smzk;Ic=HCqaG%T-#?MwBGx5pbLvIPs&|6H8#EqsUj8^ih_zFDiT8(TdG9Ve z+yCFNRm&phNZIt= z-M+(j)qImcwepB#8rvMtZFX8$Fy)M0!Nj*`LVjc@?`FAcV&wnukwE>))E?_Anm#(u zHGl3&n0x4m+AhyY-jn!}xP(pTESw(`XBeKCY}i|3=#rMdVgk3w#x^rHk8nHs zzmfG+$Z*}HSB)`?nJ;ThE$WtNo?&*{^p>IN$_pCD(l3=hJ(%dTJhY-F@ly5EgPcC< zp%r%yg{+=a(ULiztN-qdUn>H_auhl6Ch;s?DG_#2`cAs=VIDX+pjW zrdZ_}`K?N0ZPmNfd%=Cq@gtS)O(p9;iio5Mq*t*;EKvybQxCrG-rIEC&u(&j6Za7j zKCjFNmY$n_p0ZpeVkWjhK6;Y!nnfOY4-!8=6_PWH*L#T00LSr`|I&|yH{Eth)CHz2 N22WQ%mvv4FO#qM@Am0E0 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2c2ad771f72c8beaa5adbff66526893d8767990d GIT binary patch literal 276 zcmV+v0qg#WP)i*jo}i373|mq*}I4NcaPr1vPBqUj4{So^{}zU z+2V@M76*V4(wk$0gq_FHCw*~rLF6PjJvrp8;xy!tvzjxILyidcxS;vzge%E8;0~bA zCza(K@t8(4No6@FJiXx|O)AO}!Fe{GAi-fQe6!HylPYi`B&W#9B~|3)kkD4;1WDtE zgcdi2M1XboCryi6C~+>FcwG}oCT5=CqmNX1W4MP8OZ|2BU#~0BugB|7-NjF ae|iA|xq+-YC!0F}0000 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6b717e0dda8649aa3b5f1d6851ba0dd20cc4ea66 GIT binary patch literal 347 zcmV-h0i^zkP)vMkH8EXyh{JTRaptiwcq@D(P;=ehLbl?EMKm*m7(@7_siS}}k zNFtnck{HL4mc!ypcyUoqJV~4rM^fS3C#iAnkyJU3biCmDy`VZLOv=LXld^HVqBp}VWfd*+9a9l|5T|oUj(Q*`|2D6-HH+>yJ`n#hb#l7Yv#lL|nHXH>B-ck7z zl-E6UGZLI5A_j59nxi0HBZ@(f>^Tb3#owSIdybMc@(XlA$+0JWpo9oF)f{`$X+(I6 zzD@vbO$x$ik3cXita=5S@C~%!+mU?~>Ry3b!VTX*$Gifq2t&SIYT=<@&t5p}*9W5z z(eUQ0BQFTTUi@f0{*ix$>|3T3gNi&R>#9f$63RR$%O=+78J86lXD9?ml%z3p{T2}VQ_sEUk%94MA8G5qXUn$f7kye{^E`RUy!i@_O?8T| zXL1UuyzqByVgb?~6M$kc#(9qAg0H0*LzCtzFK}8j^|;CNV@?~Czk8l*EYMch+tR2V g&c&TPbLLHMiMh-N{H%+20$s%5>FVdQ&MBb@004zT7ytkO literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3fcdfdb55ebcba8d2fff8be03ea3518c137e3464 GIT binary patch literal 194 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawIz3$+Lo)8Yy>yn>!GXc$q3uOx zabB)cz0QC}ts}W53MuVBD%UsZPv&gw=#W`?Y0ciWQ2$4FyseJx>{`Oj#KIv^yWsd< zb^!&41_nkD7m4wnX|34#N6cTv0=_gxaj*E+Smb+hj$`D9r9sI`%Q@~%Ssl3i>7S{h gq0!eBm41aw^UfFNx4vv@4|EiRr>mdKI;Vst0RDqR&j0`b literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d603c4f53939ed67bac8e27f1bd0f701e9d492f4 GIT binary patch literal 99 zcmeAS@N?(olHy`uVBq!ia0vp^9w5vJBp7O^^}Pa8OeH~n!3+##lh0ZJc`}|Zjv*C{ u$r26+{`^1iAm#6W2WGMV|0f=i(qLeCvvX}j-Z~CBkZw;`KbLh*2~7aOUmcPF literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f23c2db1f8433c0112766a8b9ddd0389957245e1 GIT binary patch literal 202 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawn3BBRT^Rni_n+Ah4nJ za0`PlBg3pY5H=O_J?e8JPHEqldCj=LeZWsjv*QM-d+#nVi06-aD1}v+>u&(pu=yxb_T2U|))O8PvYH4L7velF{r5}E)my*~N? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..abe2573b1a5e6be01a5f82c03a089f8ae87fa825 GIT binary patch literal 614 zcmV-s0-61ZP)qtG0D3I)9=d59Kjp*^Xg722Dkhl0YY!4OL5BZS<; z61=$^@!wOAvpYLG^KB0M-}Ax`HcMva+o_^St*D4Hn>1TvB%jTH^te+N{_IE%A#iov~;Ww9O+~8_}=>n&BI^ zI--;nG>?zXu6dmW zsy^r^5`EVNtszxi5A;CV{@^j=ui!6F8|2Y@HE50q*%Fo?Q`{q+AHQPUL%N-$MgCaywU1ax z+6G;Ug3gooWpz=|Tb-?RTMXpu?d?~P>4jz8YL)m<^&~w09$TEDXaAWylm9vhgN*rOUk4)Pc;58VL zwSX;~BhD&6FzauW`W`-6GCecK;!P$1b;o#Px*Fj`{^R2dS@F z);Z*a4t<7%gbeA^;g|#7@k-=V3xlfw6`%t24_R#olFFzoX8-^I07*qoM6N<$f=(S9 A<^TWy literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ee92f4ecd4663703e2f95bae65ba8592e88ff401 GIT binary patch literal 213 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw=6Jd|hEy=VJ$IIqiBZ7$;mQMC zVcK8SYgcVu(aceO(>Z~;dC&CX2dnpYKtRZ>qm{MGBInFYE3BGz^kt^l^5sAdTY*i` zyyErk&Zml}MacxsyKK3{=D7M(%exm9pITN~gO+=^7qm*QzFUjKBp08Tq0CXvXr>mdKI;Vst0R6^T^QsrM(T z{K~l&Yj@SA9If=c^vXIaY>wlkMR7)vg;t&u=kNuYJa3$GaHnEwp`ORgIc%D?aa9)4kR*{W|XsGR4!?&t;ucLK6Tv CS{)|< literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cf26ba53b85d7a694dd28de4824453e3a2cb95a0 GIT binary patch literal 196 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawn3BBRT^Rni_n+Ah4nJ za0`PlBg3pY5H=O_J?e8JPLYwOzBNPp-@j3$B>MBZ?A3SJ)pqjdQg7aSsx$i zs(^r?290-K<`E`!oD2+SVjjO(DLsGxZ%@m&?K&xn4A-6?-=!d#d{}q$gg*=uWZjGcTEO7x>gTe~DWM4fl@>m1 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..47efe9d04ecafd9480976fcb4f31d28eb23387a3 GIT binary patch literal 236 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawHha1_hEy=Vy|Iz^h=Ks?Mcvk* zjdxdA?-X8X+BmWAH;eJ@1CtvPR_YpUKHvY$w~2*w)|>YVjzC)IO4hP%awXxjAKb}y z5TEeOGCZ{H$ehh?fSA9E^7&PVZ5{-7xK)%U~WYk=&{U623( literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8f67bc62b6e96d50071c12d9ada9cdd5628ac6f7 GIT binary patch literal 255 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawj(NH`hGg7(d)=F_L4n8h;&zc~ zT)P6xgPtozU8rMwf6m*FNm28Fg!hC$B91JAOBgH93AqRWSzC%$Oj&s{iQnprie}M` zhHs*mrYc(tc)YTZ{+HXAa*mPV=9VRWmlG!UJvnx9VvHq&!rRU(g<5Z8^Sw;?q`t>)mx^ONie-u~ w*&LrSB|q@Y9HA=SGvii@`e=&a#a)VnS`q={|+$*S`OdJj3&9cxY(oX}$A5&#bJj zW8Hpk%gO5h3++}g_Ho?;;wU}dFQu6aK69^`RxN+T(vG7jMBo93!hxHs6%NbS@Z!yM&C3wNf%K9)x^oJTAVJLvYy$_OwQ^0628@jsFQ8X$ATlFj2m z#{D`s8D5j;oCPdk(*zwG9yc=_XJ_o=-^DB=YLd!t!Iz=R*vA~@*|4}F?2<0f%78{L z7OjBBeZ^8H$4kCi@oZVLdUoQ1++DLi8@7C&ra3paNaDawuV2|a&w2M<3Xa?KcF){Z ct5&^YUtlcfASy4u1sG5ap00i_>zopr0Lf~C#{d8T literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2ed5b0e188e31e936bfc5fd6ddd3459357cb0491 GIT binary patch literal 343 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXoKNz$oYG;uuoF`1Yo~7n`Gi%g4U7 zZ#+%zF=D+e{$E-D?QGIkT45e2w%I5)rHM=KEX&_lO#AmNTD5A`rl9M~#A{FM`+e^I z&)r+LmR~Zpao0gl*4#@m3u?_SoX=TM%((V7BjZFFmLr7<0!JS@*gR)y2=HTe&`__K zaJo{akU`)iqk{@N15Cw12cCA8k8?}{u1`KF$-}@X!@yF&AaEejk)fTHQAXxSvERl8 zN1vxOh}lZZNU;<|z@@}oiYqScJ%5tXG?vGYDZ|Cc(k8#fpYG4bjm1j~92c&W-ur68k@Ae|tCkfw{L~I;i=L5FA$Cn3PQ|z84W5z7Gr`S7b&?Wi4(&5B>`iK{N zC*Or6=!_Ul4_t9X@l1;$e{zrasT^pS<2>JZu9G8ONr3|HtbgV;m~LswfX;Kz&QS&a z2biA9fJR{IRGxkbrZ+O630Wy878l@wtj{u_n5?DM`EO+Xk^!Y;jjhgqBI~ydsP25k zq#y+;Xu}*cU_rvJN|@6pYzlh94H8APn}CkFL8=u&eb4~Odi6mIB%9R-B}lfe5BfK6 zM*}7WDM&#II#iHZQOs)L7*t$QBHHdiB#30000u}fVdXGbo!zOf@PtGlZx{iKQ zm9vxO3y*lj7qXPQq<`;}`pI4vyI)zte*REuv+k1>LF75gtny`;%}EM~km3`^$&#EM zHJ964l5|w>H5d!J8_po2O0P;bB7QQfLHvKjI-Q`4h`kM$K8!f06Z9GU9GB}R{E&m)RtrH~f23r0hI2L3<7E~ScnBXHRS|!B?#+ch|&KiE8 z;Tx-(J3Bwn@|8LMK@(^i^$+@pruY6qDKsVggAfWY4lKxmEXacTHAqGj87(>)BqNH9 z7M%;KKB9bOPUnIqsAjZtK_98+edmHw4MqzK`am`BIu|rXHN%|?n#(uJeeS?xFIdIb zvYeHj{&dV`l=mb^@Qz{T*vGX=7GyydWI=uKcgzPgz4H$mL({NNi;NZuWJK{HqlE$) kQGCg0(JC^c7-KqS3OnC-IP@zyWB>pF07*qoM6N<$f+f7(-~a#s literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..16e2fc32a0b7abcb3699409f1cd802527cb0dc5a GIT binary patch literal 727 zcmV;|0x127P)2GA5u>+D`xg0%%D@K~!ko z?U>JN6G0TmKeO3@#vkaR3ad2t_9Ug`Aee{+LH_{hKcFZ56FhkFUc3l`;6-|sP$XDr zC`ba@n_fJLt`)t6DphdP@lrRFbTjT`cQ*c*cbeVzcK5@3Z{B_xaQ2s($nvoCnP-k! zW~czWeC92$c_sQ&0Fo7!XmBLK^_2~tiEo7h$wjVlOcSufs(4Wlkn~vQLW;l*R>hum zAn9?Pb18#g^1u|yH%`#<0h~QP&;w2o;0<{e_`+kWKyq=USlhJN65sc|hgje;rxkxg zzA@`1VYsvID6K4MvNl>0S>h^Jl=tp%+hoUiMhRLP!g(l~2RlUc#SG{as`x?FVLL!2rDSO=nq>T%&7lI!0XZP6 zB43uNQzMu_HI3VMAzj)cng~dqd#G*+2jDDqbD)~l z-mRGf1y+FqbCIuj#EpC??iG%m@E2=CEfr4002ov JPDHLkV1jDmMzR0^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bb707eab97b3d48167a29bb8c21fc02cf582ffac GIT binary patch literal 109 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xf3?%cF6!jv*C{ m$r5W0{Noqkjk>0hJ%NeAb!~d=tj~okAZ?zmelF{r5}E*xz!xz9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d9e712d015ae3c3a5894872b2327a4e90a62c80f GIT binary patch literal 693 zcmV;m0!safP)}8?0Dv%>YwY_mhAhGOlMg9htaYq)taVy}>a$6m_t&ZGQM{cVl zr`3_u>d0wz<+)sfZe$ZBFE*Uw8-Q53-Oug@ryVJd`*NZPniqDYM*P^5??f}(%G&CtTAAX?QznqnY^ zh)76_eh?`AgjOx0pdum^YM_m6Yf*+R$}lD-=Cl|F=G?h=F~hyS_c_aRkS`4H%@}{q zFJ_qHDmk)T=1!bBvz(@vCczA+5R$$7>4B;`D2Rg!WZ5hW?- zB`a}xqAn+vBj+pYaQm)q3*mEweYmBK+uz}HrUADdb@v;><`mh8wv!AgDpqg-G3GcC z6&)O-$8U~s4XvFo2#ZY8%N{-@P_(cJ65Rwc-Bi#Og4x7U!#NAAA)v8_f1r&LR2!cWelpHh2AH$v z%&?7J+-3>kBdrA1&j{^Qw34mMIYA2*tI5#KGV8Ak+Dxp{Moy84n9Zx8Ib!#af7PMH b1Xlk6+d}W#vNo2v00000NkvXXu0mjf``t}M literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..60257de102ae655c2876062a75eab0ef219b1f25 GIT binary patch literal 746 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXoKNz_iiR#WAFU@yxm1Q=J?|jvP2y zW1aJ_^*4{fw8N=UUJI_x`xCi7zD#D8!$MAxEC=>;i{$*6H~x+b$Y5$!gAjXO zZ-Yv)IV8)h+s^`#D7?VX-d#E20t=V#0xjc>`N|bN zk1hUvm1Mf(dynFd-N%cNAe74-xs28jJt2>8Y6%NrP= zD9EL{3X83zg=!+n=V*1Cq70s>-+32cclUx`O3R*^(wzT4`iNO7O+41YkBX9@V??lQHpnS zcO6^x+}mdT{^KR9UbncPRaw9!6DRtz=a;1WwX?e}_kX%!ntrfNxaPb5dXe^(|NrF8 zpB}%B>BucQa;Zr&059tJYdP^UAy*uYnqpT z^{WpjU$oL(?OLt(2Nta;+pk>tUP|-eL*0wp70+8uFUxaOsSc1@KCMOL;6n9JFTH-- z?5@{eTeVR8U}EHx;{GjD-|SP%UB@K);6n-Hg|nh77}`T8o>#uK@5H;^m)@;*V71tz z`Rm%oS20rEN;jP^S$tk1u&~$nhtIu*yIr=j%bfguAi{b&w5QFT z$@exVcygRrZsWS*oC|z07v(f``2RF*Q@O>``-5|Tbn)WK>JzuiCg0xsR>EE)Ph2GL z4!ZfWh|-WExLE`p9U?fE(iTOq(3cb{ zr5Xuz5n7x~V@Oa@ig_7Ya`}48@!W-^dAU4Khd!U*nCIXNX}PC0G)>bq?Pz0wNfPAQ zx0fS9jD8G1^Yd_sc`RyK%o03|pmEAHQX--{C`vPrR0fUHiZN9|A6Yt5?tp&qV#;wr+D{Gb@ZdYxbU9+C(X@`L)Z z5aO74bKW3S2&i~<%;L7e2;$31;x?`3T^6K-SB_PCdAtgoQFZ-@(27x&7qrJT^HB8_ zc8ns86c{~>OMZYEENWGl=B(;VzF3eb%VgQJmt~0w0uufVQZVH~I>_WAx0qs{6d8(C z>=nt7Vvc7-xPYlD=n64bDAG)U6=H;C23_VQKWJBm7YuegsE3<;!H*<2FuN6Woo%{d zi);RaPVf0vHZNl`{K0_~I?(mv#)R*Q`r$OiW zL#-+wx!2;mD!>EQsZ#4V=Q;^G&K6#OnIhD|{{o(~i`Q3d@VO8vgiq}U{loE{6BzzS9~m6G?FR|(F}kz`1c^a9=qC$Z+AlvrQm75m oK{`nNbdV16KOLllbdV1E5AruaU6!QcVE_OC07*qoM6N<$f>G}(9smFU literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5345ee3c4a7a3576b7493cfe3b4243d183c5d996 GIT binary patch literal 265 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawE_k{)hEy=Vona_=h(Y9NocJ*= z2gVEsR;>f9D;UIQtYDYfuzAy#I~!Bp9Eok4p0B{HuiHB}$V+qS6!&d`8=uTAZp(j*(RKzESv+D^YJz!E~ySM++u72Lv s%Lwqa0{zC|>FVdQ&MBb@0JEE2djJ3c literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..547ef30aacdebbd5bc27a3831971aa49be8813f7 GIT binary patch literal 283 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw9(lSrhEy=Vy|FR(PyjfM61bK%=q7O8Xj0w7EPdhfr5^#S_IEwe==vgFzHZ{arrqiL9p^2}oCQR8HlO`a zfBt>_q&-EaGMH-9&z)oSy0%IOibq!5=i7xQ8?znl*Ho#q?l1C zmLwh%pdb>pKr}hv!ldkF|5nzo_`k zc}eR_hl%VF4-<)DT=alRfHl0KMErSu6_?hE1x;YW&OCE+==ML_XMw(D@O1TaS?83{ F1OO`naDo5; literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..abbb9895108b56aedde011bdde5eb8ddaff78838 GIT binary patch literal 728 zcmV;}0w?{6P)18zk+dXf)UDAjLvsN|p{t6$05L%?Ktw?Z^gaZCQV^>Zx>oT5B!UZD>LPSm z|L~VCszm}4VSKu%ZO@sc>73z9GKG1U!+E|uoS*N^M5!=g!h{LqKXj94ff8p_aHw!f ziFtB#wL|6=o7_=9cWf}!N|_gY;@|neKr3V(v542)QzX`0ojyKemomN0(|N`@_Bkcf ztl6p6`NkqSGQ^1xCqtekuIebI<#is{EY>;WMC*)uR$b$Z z(g)oNPHrpSfn4QL-e6kR;0(D@Nb>GoKwwK4j`B+2oi$`B^GkO6(b* zLPzpSF^b0NX%vSUqI4L~4#ky}qMuz4Iy=7(y6gF2Q~ohxkTt4*QK_=7cV&)SqNEg^ z3^T_TC*0t0%L&`eGUC2MdD=21&+2^~xs(oqsFZHE(qX9Np|QG^_S>&?P@~inD!G)> z|AW$(_ABiNO30vw6u_#jj0000< KMNUMnLSTZewp6SD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c0950f37171ec7afa5d059a64f04a851c35aca74 GIT binary patch literal 4538 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 VkKCaaKlfw+JuM@2wT6A<{{W>MScd=r literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7bcb2fd013f9a7fef71171f0852ddf109135987d GIT binary patch literal 702 zcmV;v0zv(WP)4uW0Wf`YiI z1yL{#LUF95wDo}?CDfvAO_Thc%J~v$J-LrBpx^(_4Zo1n<(?b+4~rr}KY8->lOSe$ zY(JolL1tKOhC$kz2TC)_0`)C0N;-T{FFDGrRhb;U;eb{rMv>W6nM{l#^Qp3%80jsN z)l}I`jLb_PF(j+0QcaBJySthy#l+~Q!_`zNCPw*96=|kQF)_+nP^9^!Mn3vGBkTOP zn575lfBEo1c}~j2G#ezpbMry+2N5U&MW6^2fg(@@`o};kp@B*|(0pj1q7L-M2(+v| z(kC70Jtk@4+mI%7peJ5I8KlQL&@Hc^OGsHAXuvBdhjc~<>hTDQ^A)LE2THQ!4RjFc z2MO(E!?-ulHKb?y+mAs{pg2=V7xkb$oztP$w0nbB%%8 zS#}N@Lt13B4kYKkW6%LANOv2*^xQ?+Dd+{#3TZt^&O?WwQv@3`yB6Nbq9afTMWml} zm|mP;aRN&4l3+vTH~E_=H~`%y*d!ZmRttSpEJ5c9R_3tn@@3cxbebx`uG?>el8hUJ z2B;A1ISCi*sjW;KfZ|-GwgT<0_GMknH3-_mQ)>IhK9?KC9_IBRPVkx9igdePaUP+l z0`2Az_04g}#}aryvws0~@qiNbO|#e6RyxHS*6J*;sPbp8+3s_1zkw|Js@x>$|7F8* zrqER2SXfUjS{Y`U^;+Tvt>HdK*}*+ZNYXFvkZR6zofJcSB-kvMWr)KB$!Q_O2p^av kONN&4A2j(q5lC@<1G;jC0F4+6x&QzG07*qoM6N<$g6>#9O#lD@ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e8f3b25d3b34d0ec37ecb0674de62558aed09dd5 GIT binary patch literal 1936 zcmb`4`#%$U1INF+xNHt#BBFNOSvYRF4HKSimJ!Lr>Ebcxk;gx7dHO?EInVy$d)P8##H-WitgRyX!Oldmqvkr(WyAO2j&V{) z=9ES<@{?^%{Hm{iQMLR z3dG@%V~A8v+90P?*8Z-t89}}1p)3A;L8VrsUF)|Y9ji{Q*VQf4Xu-J+ z*rPXdW~M&9nk&9zytm!5Yn{ZDO{iTSKN;6R%=(4i5j)es!J6eqx4Y2CY7ga1Vm2}Cb^4R!{nA3I%=78*=b`K%sXdjJ$vVR# zA&sg%&st9Py_#cb)ETR{7o2>*IMpqp^xt z+r9u!SCKLYmK*_Zk6TV!Er0_k4k!W$8o=p56+r*52MJNV=dWq9=mL1oE3I(8F?hQ2 z=!qqyNuFM}_Lu}LAn2v%B6VCg7P=6d@g%@pHm8nrrz~7F9D0q6Zd*~uO+wHHNuBll zB1AFJf#H7p?;`#T^t>w2?tRuL$aafE+$RIB>s#M+R5QmCvfln6_)4|*ecSW~^;jz6 zJ~HXNBY0Xlm@+NISes301Fn5nmuVBlInGyF9{{)}E%s9V=>Wmy58m`a2s*kg72l{A zc?MoHo_1_vSeS*;s+Yor{|yVoyBU){G0RINUX zfxrGzhPfCw_-MoGT(SL;zUi9qKW%SXRrH*O;~I-CPwl64Eue63c}u{Qx@T$DGjMP@ zTh8sSNfYoFbqqZX7dk*E4Q*?j0GHj8twH(2sC-imeGgSChx2i&4Hj;(i*+NRhrN}r zX#q3w67^@+5WLgp;mi#MoRL_#!GKKcU8&W`jUEJwaZk zX32ZJd)czqYX`i_1zNMeZ$(JvQZ&^21zMV^@&uJ1WAK@2v_ORb6~9kW(mUB;m4>7U zla4+Ynm$BSC^e6IBCfmMP@~|D=V!1yf6`}`vZ@h3%RDJ2{s8RW301Xd#>b+5&}*A_ zs5^7|?dYWuJ~(l;?Od6vLHzY}iO;(wnxNL>7?!zxe4LE|i1k?n8#jG)j6oNnOIx(j zW(C(!$>xIR2?{Wa5`)rbYUs~x4iOBiyI*{TJ<02>oi6X|EI{wkObnuac3@_(^02;} z#3hJKkC>F!us!@Fp@F8X0hQH;GM33i!T_?wsGN}akS+MM|Bhtt`#xSE=8F5R#@jOM zE}MYY<9ilSFim{FVnFcElph|K{dP6Q@~!app(dDqJ#pFj%1j|88aFf^td*)Wjcp^w z`efE59FbofpbnlsY~3}IeuxUIsMDxEiaiy3CcL*IQV4sLEAuS*L#N3=!Q|k3)bhS9 zf%rh?-7nq`PS+~GrhUapa>WdPl%s4s7NYyrLsIZ&=p+2ac+rpq7eAtaYN*KOMWy+# z#ym6q+M-f-SEj TIU76C;9H2^WUu;TVT`{3soG-y literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3023ff8daa925ac79e863caf679d03c56c3afc93 GIT binary patch literal 827 zcmV-B1H}A^P)>`06 zl!7Kix5q+Fq6=w?Bq2zRX%{Q=l9q~JFAfaD-fMk(ql{OMJp*hZ8Le@FKzb36b`fRvPbJZ#ihbmi`-huQ`v`H+dnnz5 zIO;AGB7BE9YM!%LJ&ic*2`mcPKpgfvmoR!03D^-c43Z$hAhR4H0ecmL8#qMtu}v3c zWQvny2hqm?F??uNd0r4z>Rub{-SwiV_KcJlc$@G(Y7OnY{oYXaZ@4M{dXjJ$v z`$eh=sKvO?Cei3I?)A;6Md`CXq2t(8SWgZw6G7n$pCQ&JiDGgU(}=YRim_lq5{{Tt>$z%L(Sw8>(002ovPDHLk FV1g%Ge~AD9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..eabb0a2ba41bbe0e109263ee9b0d6ada40a6e792 GIT binary patch literal 843 zcmV-R1GM~!P)RPWZAHfP!)RVB2S(!`lt|Um(uYf2_cGfFjG~?QJ_SKM>Hu7Qz_%` zC^KY0j^p?%F-hrI#t?oEkx>e=^x$WZC9-20$8bAJMiyi_f!j|s$&C!#uq!e{4&*3d z*U4D<;$4cQ>yQUJNq3uf6oz@6$E?Ckd0-Y5%+4`a;aG`TuRL)UvsTrDjhJ=G6Wy3? zP%Zcjv%EY}z-*IhK|5x7d7^;XCe?zCn03h$-I#4qEm(P<#_c#+Sul>1xE*7f>{!k} z_~{|56pZ6*{0y;7>1g5}{!WmS0WF-w-(4mv4bw>oQKFMsszM7}DHGx$QauHt_cw57G zmNrf?VwFPCz(EGt%|eYrmJb-fVG|Z VF?y~eTbBR;002ovPDHLkV1i=jh>-vQ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f9121f5a372c30995a9af51f62cc793ddb035cbe GIT binary patch literal 519 zcmV+i0{H!jP)xCV(^u?_mn{=F~ZiE?Ym3==!Z50q0arg8`6(hH{4WQ5$?<|I?QkKB8N z!7+X!*AnJ1d5m1UjnN6@*e4EP_6)go6$hu0Q*YUWibf?3w$8U%GZER!Io{y&ulT~-(| zqR#?#`>o9!Z_tj0%=+Kj{)g5G##@69N~vSKHRz#~mW{Utn<%Bke*4c1dWBNz8E*~R zD5bXX)*vuIyILifZw;CZcKu}2erpqce|{wNXcJ7grK?mbzW|>;ux1f-#*zR4002ov JPDHLkV1j>l>YxAs literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bcfb49303c55dd96b6b7a89783c440ed31f8e04b GIT binary patch literal 569 zcmV-90>=G`P)WtJ2Gs%q zY1Keg1k?fnt16thEM(*Z8H@Xm1;|Rq>K@%m0=-)WElC2^t%Ce`lma@i{$Q_3;Cy1t zIw*juaL}f`!=Dz+c|pHD&;V*eK`ZuOklO+|75MFghVdl~6mxco>s;ko%I@J-Co5ak<2PKiOgywlMXDd!bK`~%wsA!waqf}ADeJfM$esJ$iv zEmBR{F%nLe(FH{U&qrB3E^4Cc|yo-Q0JeaICDEF={N<_v*Hlc(t(au00|iK`9KBL zKq=KgK~$6iF@^}rN`V+d5J}{rDFgD-b3q7c6wyQz1*8y^RcqEViu;advVw?=T08Hd zvbAp%JhkRclZGcZDZ19YY0j`|UUaQ_(}rPFO>}J!fo=?&=0w+;H%%BeO^B{FZyGT8 zm{+oR>^W+?wGIDiTS8byZTFS_jvpFwNZ4B;Ppua(Uc}@F<1D?qnZ4AT00000NkvXX Hu0mjfA9Mg| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b191b9d85fc259e3cdaa0ae837b3f7b020b74f75 GIT binary patch literal 217 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xb3?wz0qptudo&cW^S0Mc#1}qL(uK@CyOM?7@ z85$T4evO$5nPiJ=v8UCDil}G#g&-(cHpZ|UG z{o3BO^;&?f*0m_-uPY+5d9N-`T5B58PoCh9$G;dBu+BPn{?#{Kpd%POUHx3vIVCg! E03c0T=l}o! literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dedee3938e0f75486752ae5821f060f40d398be2 GIT binary patch literal 435 zcmV;k0ZjghP);TKm7D-natct-`4XTtRG^;YK|Ln}^&AiS&Z6a5(8zh=z_R0b(8vKI(gp-* zhGR&A<3S4!AV`wqK@-b}v;hzDB+v06Px2fO@+8mkpoFxA03=9)qd}e|IUeLmj^ja| zbR{_+_IxgrI=*i+vC3Nj<11c~DR4K|RTXdQuO{Ng-(B6=q03*`F5l0N_a; z#L|b@YE}LOu zJMC#n8k^acXlLh~uTE=?+1dW*fBu*A@c(=ypU>y>rHdR^vx|BfX`-D^^zoA@aUu-y znGT+Dm3sD2K|oHTgxy@El?Y~y(8eWdDUrQcLj!HZuqsX)Au5zU$_Nq0E@945Dr+#P z<_R(E7NeO;45fkq`w8QuF7^_T8O-7!J-DHVePnBT>?DkvI;m1JEafR)Xd=?%rW2>Vr%>jxoI#v4z!If~Dq=V#$~L8rvpD6Xtia$tPPikhDB=Tldq;t+ zqnrWk)K5_9VLLJG5@n0h$7$?RCwp)gJKU08D5QgQy&+%rA;@>q(MOr=#a5zNHOeO0 zj}usROwQmoR@{(NDBvxrdQG03!(#eL#a9-|S!^bXS)*)_^Ei%KN8}D}V#YPOg?!qX z$t!Z@9!mMfbb47Jcd?OCl8LZR?&B!Q9FjM<&Qu!Z74m3hvghQ;J1nG^zkOkzyv2GV zj2mICyvJe29Z+WA3JI5$Dahp|Ed-P~n9p4Kd_JEQe*p8Nuxfxg`z8PY002ovPDHLk FV1gfTAL#%9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f2023bf8974b63912a5350bae8a2c2481bed80f3 GIT binary patch literal 795 zcmV+$1LXXPP)5Fxb>ei9 z;GH%kI*0>kLgK_KM;8)zNYDC{2r^_(qL2pK>0^))BFwYMGLT@2MLrT?gj@7;j7Ex7 zknU%Y-e!`hW{f{LO^k7_bCj*#NDnMYr%2#73C0R^zD&(8u6RlVFP0?3g-6Y`XhF zf|t~o{ZYM+))#+Bzd1)i=CcVq?rbE5l#pOF^_*zum#eOGK9QodB@144-PJGBeoNC^&`EsR$rN~t0Bo3GRX zUaSl0Y1X};&CKMSG)bA&=DLSP1*Fcj+lzI6QfI&4CvrxLbI{3&Xh3SF%u0|hIy<5T zc$O2=Bj-oha;B7!PFj#6l)5=p*Aw-Xw7_n+H;-1wGsQ@%uqCa~B6At)liZi=Nf+cU zi~*h~B6VbthUBl+WlSn3O;94_LoGj)lD@G^cnb$42e%n1Mzio9dXdc=1F2T1iQ#RC z2a+mKDG}XBH7w{xYB9W^(SURwbR!LEMxulX-AL7ZcZFE7S}N6$nq3U;C7QTrbzV|K zI_ntJBLu-*knTF-46e5JpTs6!+j7Dbr`j7ybLvS|e6{3!Hf*aoAT_a^l@nv%n)W#$ zoyiJ1ZQ4GQG_UM1lJ*m~O*uESw*N<(x4qL#fj07L4QGrE7Cn+Ae>g%2K_y=@IG@;V z(<8_=-#9`D!G4y~2J>FA{-GxY>ehfGgb)k@a6=Q05EK(;RKYiBg`^6$X~fAVpM3tO Z`~{g<9R*{#^Edzi002ovPDHLkV1g(+V?h7_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b8ef1050ee3ef47ad8ceea9d5a74d02eeb7a4939 GIT binary patch literal 103 zcmeAS@N?(olHy`uVBq!ia0vp^9w5vJBp7O^^}Pa8OeH~n!3+##lh0ZJc?zB`jv*C{ y$r6(m{IloyqdeEOIQ*|dyK8c|twOVD2qVKDEC0Qm#d|_QCV0B~xvX3Aimbg1Az;H9K$y@FdscSWsMW>9oW z-MXl^jg+al6*^;$E%i#hG_4g!&H2vyd2`}9NzTPG`TYKYd`p_*;y!JAc(;36s97^a(T#J`t-GFs^4Bl()9mkpGK z8&E}(QHn7_JC)%C#5u})yzrJzVnGMA@fo8`;_spLdXKt%U@{ zd5-A{;(8`=y+1LH5z9Z|2&Tux0|}@ih3P=P0ad(1%diFsBtZBH3+)XR`2{2~bs>QT z2;G?W_W>=KI*~vFgcF#W-3Rocy{9tpfNIic*W3nF@Dc44 z5_o`c2JHhecL51Bi>6QlmSWoMF5n{CeI%3sVG!+{yMPqh-p~S)Xn(s2n1`t;w1DN9 zYMldG(8iF^0)&@n>zxA*q4kCu(1+IH9B>(}Gt_`%Xx+{M_t3V48jwJ{?Hn+Kwi2(X7fcf@C_F%D1Dk}MKK=B+kgc`Y15cO8W^vGm z_r688^GLA)@ZfcPt zR?lVZJ*UyjgRjeSBtV)IR3im!4kww!-ZPeZee}4Ar;sz^3_s@iv~`wAav7%4kHg>f z4Dv05Bw6m_KMfr`;9CZ$@$2|~IXyUeNgu~Zu$uYAh!7#pd{&d-I5&8OlXJ}W_X5N& zCh^MztwCHtX`~-N++;y;mw;Ls!%HJ<5BeHbjDx(v3*#h-hI0|Ff_*%t7%BD;5Bn-& zGuQY)zNWa!&&>K>E`!F{%sKwSjUl?&sNOW82I$qXo({UX%_Ckj%~!rM&1)WUiwh)a qrWWyXLf9(>gt<~cDWDWk3iuCUr3JX4tg|`*0000k#H93t1Ikm=p zMx52d5mK}qUob@*PI`i$5yQS_oI7#Oy?lxo_cw2HKh7Cu7cuT1ZqkTjY8ht-F)l?v z&Z(lG1X;weX)1A0Gh-w;LJV4>3P;6wmaBYO^m)wTxHv;B20o8bBA_AQSNsp|Arh*2 zoiu{(29eRqCZbAHPh>Rl9in=X2zeC!j>Zg;vaAkB5-FqVfIULCqE=`t>yw4Q2-#a| zg#pYxrKU54>@~H*dzgD!O{WOiBj}%D3#Jy;^g5yY0sTo!1tvPls@YjWcL71KQAsiP z@jasICv+C`LIy! Qx&QzG07*qoM6N<$f?UsD+yDRo literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f3c568542d1a733c45204f1ed5c6b9bf6a926d2d GIT binary patch literal 819 zcmV-31I+x1P)(jc!6FGXE_?3M6S`p_sHP)J+%&hSjD`6m1NG(g?$nAdMEu zNUZ{6k`f{bQG!CsM0BFtWHQ8=@u~HD&Wx{)&F>7npYL{liw7K>TZ&;AhG7_nv0BK$ zS(uc6vlLc$Ccp%k025#W;#f9SQ3DFy9pIq>A0c8HrHyX>19 zW~g>cz<>0x5)sb1A;62DmAfb%kcFT4G|L8b65Sj_+^75R`tb7&XCX@D0xEGfK`TYd zV^b;TIw9$Ou%JSiyY9n+ry%AXR%fy zz>jKB^YSsOdWnF~s7`2JHlpf~2neJ4U-PmQ)u2Q`1XYFh3%{_kml6R#H2#G2ClD}- z>S=NX+{W%?G6mFOXVH{A0UiQ4dBj%P0R6S&%%YcyWC+M&oan~6K&ecC{z?cE+Xrq@ zOP)Z0<|b*xJi1x?a)38usLP^- zN%RG%kq=OpP7OXRbajv>A7Ebr&G=YAM?dKT0b28LkW0M4BDQXs0R4HXr;i1qYmy7l zUv7-2_;=Pz2Iwz?(*$vHRXRX_MT}t=Bu6?xfBC${?x=i#{&o>T)#>g4bpxpU$raFq zYC6#YgQ$w*`exy0q62QCs@1<{AF5EI15Tm3tAEXLRDnbX>_io!Q2V+M)kxx>#P3o0 zNY%P5!$Q@S_<*z6b!oi5-^e@cY7!sd%WXvRr|0gkcG_UO$t zGK{k`QZMEPi0%_ZJm3z!JjBl|(T$NQ6_Cqo;uU1ONfC(@Ge*n6{IDmJ|3g!R+002ovPDHLkV1fXzd(Z#? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d670618c7e96225f7756cb4c2743e7ebbf688cf8 GIT binary patch literal 308 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgetWt&hE&{od;P3vlYxj!B3Hnh z1714EonD+``t$ZU)3IJ|2aboi6~%ilJM7!-={Kv*@R&q$518Ox)_m^XpXp1^&DOu% zB6r^~@kqJ%j0e`LXFkL-JviL|Ir_(khlktwLa#r83bn@|VwIw^gKjzH(>hUq>tlAsh9~)aj{(R+p z`Qx=v&?j!r?}<)Pb+Jxa|B9Td>MRd7#ru4CATBh=u6rtH32ETvhYyB39D^lU%DgQgv&#U%8l^-CA%qY@2qA=!B5=cm zUgehujQG*l{{`@rPr!f|fEk^>KI9WteE@ilV6HEl&_rJ@p_zU*;T}RirIc{5NocNLm*7JGdV(AM zYYDFOvk5~8{Z*t_>U=!XvoehUSEh=adIga45oe)B9kGgT}7UT3Cirmr(oHPv^YQ1-p=Hlh5u;xggf zX{&yw+El-OrrKQJRl@b7x{HLmNkj95`a#LLnW{Veb2C+!`ppt#r)=g4@?c8RY{yJj~W@W|h^mU4q`i)2y~Rw@J`jIh$1%|In!~{Tb{nMqaxlgb+dq eA%qY@zJ@mriVM?qfwL0;0000 z8s{2&w+58)!OVOi0;Gttdvidqnfamwn4@K5K-kdNmONpNK0^NjMg-jC>n z3(S`!j1lqzt_sYTCfxP{VgmE!3FrO*8bs#H6Bek-0Xl@{%M%82fFYsz@`McaD8&nm zw(V4t^`R7BECzffq)1u}fMbdks{uW>111#Ttp>#H{t(0eisx1XmKB4x13Ih*Boqg1 z{}3zQS_~LhC@$OmMW(1BZ7rZFchkF;0_F(iZrZ?-m4N=g54L)+uMA=Dqs5Qd*3kNg zyGKc3P2PX%y=OD4*i+j^2OO7d`t|Vwyo6PP-y|iQ0TiwL4E+3ETOYF?VV2-FM9KCC z-DTWliQu(B+sigE%|WE~;_ky~ z9J-g7C00J`4^4Q(VxGV~>&)ykk80L8RtvRxOj23tnLW?`p4GoUTuy)g1Of3yPXFiG zazK+98kT&N|Lg!_5daPIFZ~QZ^;?g1j^_KtA{&q|G5dQ4KuY+ck{{`y~aU3Uxr^X+awlIm)m zLim6tF)Yw+SYp*L31Z?03ltA1m;Rr_zhgPmvYG>yq8FYs_ibEqHvUn-r*1=DgDFx+ z?bd02*59bTR4g&(=Y(kztIpQ%Z4JJY^zZNE9?f4`+x0ysZP__5=_$iXi`hvQAj>>m L{an^LB{Ts5t^ZVx literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fa8d522d2656031008696b64482561623c4dac2e GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^2_Vb}Bp6OT_L>T$m`Z~Df*BafCZDwc@?bP0l+XkKoU4nJ za0`PlBg3pY5H=O_J?e8JcgTe~DWM4fN}oHf literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..830fb7e1e6860117fa31155fa24e5002984d1444 GIT binary patch literal 814 zcmV+}1JV46P)OpZvS)hbY2bjDdCIt{;m^E&&TAfu=g!~1xk)e#84w<3VA3%)v zT;W8QWNGmlFhK<;J7(NFz$5aw(K=mT04Ay7R#jfR3y83c+nI#j0wmeRi*`u30qCQQ zR~6}T8t{Y@ysSdMlYl->@VW}UP63jX@uMOMCjb$4@v9xe_5sWInQ0d=iQi4w0rXJA z@2Ye)4#*>+^+o{`NNKDQK#U4@`v3FnbVSrT;2nqoz?@Y;8&{$L7sRarG9U^7(^dc> z%HjYeLi&JV5C?!keZZPXz=|#)&W%Vwji?@A6hs1GL=Rw!1?2PqMX`W=9Y80D1wcX# zNQ(xf)PSsLz?>SeBpP6<0o$Shc{QLQ8nCAZoQnpWssXp60d+OtUNqqDF967Yh5q!9 z__zNV-9mDkYQQ3rn^yy}NN!dQNF%u+H6V%P+SLGq1Ef~K&;iUNwGVoL=Sc0T9w0_t zMlUZ89)0gtC|Y`CEg46|8RExAI@7f^-VVS=PR7ccdu_U0y55j zS3mLjFXyK}eib)`TYmmysoZDlI6vP{qxPTMp}Qa1v-iy^khaz={O48ie@}_5wN~jr vuYxo0TTJ8qckncSo~5rghYjW^hNK$CJ-K(&KOMZS2NLsi^>bP0l+XkKy8mlQ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..42615516b9bd18eee30acca54796791dcb18d3c7 GIT binary patch literal 284 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcg9(%ethE&{od-Ww}vxA6hBDY7C z�n`H{DRl$`|%k3~A&t1Ymr*DB&)R3-~l4+aPPdH-JcJ@5a;-}By^CK}#PK9sd@ zdrR-nc|uB`_o}EotM<4tbN##thVhn)#rv|HQvM}d*{HZYmtXc+bH2cQ`}v{%9_Od~ z-$GZqiam(3;Lq9uP zb{{ofF+<*SN8}#Xsh^z>RTnN_eYAW<;yi=tQ+VUtW{*iKo?!G+IsfK|6V-=)`#s;e;ll}kE72R_R<;o{ zo|ap+KIa#E_VdM|bgR7^3jRKw%stiby!f-9SDcm}=WhP zIsCiJsvsYs@VSxI?*BigY5Y1dl}bNp|$*m|mw>m<9F7W?YAH4ry>PFfQGdg(o}N6X*r1A2+U)78&qol`;+099;o AqW}N^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2859a6fec55d59e529dd26a2a3ca91802f2b2dba GIT binary patch literal 287 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgo_V@BhE&{od+{}IlY@Y3qLjd_ zD|P|Z&bxR0n;;Po@h!0|ebQ%9gPrP^OM$8(pukyt?!Th+3p8?e!e@s%`$llIC%QcTo(V&=0bbU{`dM|Q9toP>AyJ-D(ztI aHF(MDP{ShqDJInhB;@Jp=d#Wzp$Py8%6Ibs literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ded5652e406a8bb0788402b1ea2034968a241ead GIT binary patch literal 233 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeG3?%1&o4*=J@dWsUxB}__Fko@OdIgZrQWE4B z%@VZ8#xa<@HhnOS1viNAnEC})a|0sJS72xJ-2)f?&QaR z?3aFfZT$|bN3Ya!ZWwgOIdJDMXyKo5=w(d|$6F^2ai?7ZNyM@uS}y1~X^1JRa37T8 i%D!pbm~%M(0r%^vOJ5j@by@-4#Ng@b=d#Wzp$Pz$!&MXj literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cb2207f11fb53879554df5541522fa4ee564e975 GIT binary patch literal 106 zcmeAS@N?(olHy`uVBq!ia0vp^2_Vb}Bp6OT_L>T$m`Z~Df*BafCZDwc@{~PY978G? zlNE$}nEw3__|Q0Q&OttfcS{V@Ia-)>EuS<8GB8}PYOf6O+;0yu#M9N!Wt~$(6988y B9v1)r literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e66a5d9e69a8ae0643ebd30336bc2020e8b01387 GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgn3BBRT^Rni_n+Ah4nJ za0`PlBg3pY5H=O_J?e8JW4D;b=QDG@t!V@Ar-gYUO&joAjshAXtG-E>Lmtt zMaMHT*HhzT{s;ioF#HIVk-buMv;Wz@?_YlLatJ6m00pn-J_?Sm+)``1oE0d-z|e4c pc15Z2`MkLEnT$*<96%Ke4R57XYW^Uu4^O>P7PnxnqLQ6~66Y_+%9>@aBR-71Ny)tWF9&4ib$9Oo=RqI9dcb zlm*-n0v(*{dqR7E7rquR@qDGU>&;ikpBZ;~Qh)DwcuK4^_FVdQ&MBb@06$S$i~s-t literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b0bda26060a86e92c5229502dd7fedf43b0c0b0d GIT binary patch literal 273 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeG3?%1&o4*=J@dWsUxB}__Fko@OdIgZrToU9L z%)rnfe$M?Lkh{^-#WAEJ?(LP0yoU@#*dFS5&f=MvX~ciIfm5J1 z#uVgiyP9tG`QzNRae3?po8@9ytD6H0{v0UY$93)}8^itT)&hZ*M+E*X&=csHAkg!m zNl>_9V$#75&I2AR6IxuDl@gehRHOtHFE(&=@>v&@DmX1-6cl276fWt%eRll M)78&qol`;+0E~idHvj+t literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4eb7b166c5ac19068a0493fa3922017aacc5b554 GIT binary patch literal 427 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z*ywz;uuoF`1XdQ*I@^S)`#0K zKZ^9yT=Bp%+OS*BsX+gzbI2Q2k?^x>g_a5$K0h9wJy5+_{vW>+Q{cDrn{-YC(Ke}) zYbz=r{xp3suWN0IM)()@h?l;_=Np=z&0~x{Tg%*bkMa3E#^m=6r??wzU)<(@@VT-< zbw2xtpLz`Q^BF$;jAvt*RiDqW;HMG8i5ljHGv?T-4S%dpGAMlJWiYxArVQj|86JH8 z%U~xDG^&naK0m{PKj$aChjN&I)EsW_IsY9<%QZax%~-Qf{=oCk3<~%8R-E5IbrvJT z&p3t&V8sWxC*-r%;4r z1-e2^T073U%u(tVc%!XAnAxln+}UMbF#JCde`oK$1r4X%nY83N zSJbF1_*BU-|34?gjX%~52cDTT9C*sipeN5z@ROn8G&@6#9m9i9j11HH8Fu{H$7uII zj29up_~7&M`<9G$2*q{hfkp-Xlx3~~Y2$3*{CW592{0RCL=D9B{rL^&_Xje=4L{Fd zGrNZ6ftU1Teg+Z8sr*bL3qJ)mD23Q@2ed3d#m?}DRltFPkp)D^T)F)1GUNMKnYUjy zxPRU9wB$g{;;Ne~UcYQu=D0U!X&KLhAnC6g#A7QOlzF87|9l)d^AFRE*{>b*Z@s>A qJ9~9%?Qc1U9SnZ;5Yt!$4w$=&<>p-Ti<1Gy1%s!npUXO@geCxg|E?AQ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8ab410727c653344ea279fd97da6a622a9ee3e64 GIT binary patch literal 665 zcmV;K0%rY*P)yb?`#6$F;bZe(w3FQj2{G?EaFY=-y}OalQDL>eaT zh_)T>*VS{2F$6%KeQw$( z=Hvm}jOvs1KQTB>w5nu|Mxv}fc1p! zIp5#6p0lh0)*GQb36btr-f1A00k&O0SZt+g_sliOuIhEG%2SGDAFR^5@HHz z0g8M>q5}%)09r`ZOao9su7t8bAV#u=Ki~w(j{E_ABfF|Rrg%2o`Fsg(S@&JzyX?-?c;sOT@>nrmC11v&p{UIgPnV+qn(`6bQ(WA#9 zHA*^Y6rcbFC_n)UP=Es50s4<_j*;xpAJ9aynm?dSf>a45e?UZxRCOW`fVFY6%wa=E z4jVVS95!_1ym6uYV%s9AbRHb=vOgAFNL_0{tiQe2%sG=XfmR zipK9dR)0!9{g|3IOZ)iz!`!nz_T0?d^GiSTS}^;)Rw?;;d~(IT**43Xvx2_u-!-+Q z_{yK1DXhm|ESKcN_751BJ>9qLdcrR0x&z^#uYI+>f4pM8GrvIijpX~E_0PW6`NB}h z9`o6@{&r09A%26@+Vk-T7cl0rXEd+@(S}Z*T#=a>uD3ikZ`|5G-<7Z5pmNEKo4X#@ zA8Gr3ChO&^JjYe0^K1E|ZOmBsPb35zRX9kS{pFAgII_&?1B2odZXwfMfa?5Na}6>3lG9Nx;_N^btmD4FHCOa6kJuKgW;jUaDFW}W($eY@o+ESuKyn^F4X zif8h#7Z*%ix2GX)zintT8N14c?&s9h~8`PoJuoGkg9C+xYv|< zc37lxSa`||SC}BHG;eX&B&nMD;6&5(>)aPF56>>*mw3(tHkIa1u38FWnPaeN`j5ny zzy)97VH3iT?6$K4ES0iz`X z&~@O~YUuk{14c^(pk1l(!`fEBXo&!16rB~;8qw5b#^tPSa{vy&0XP7=rd`I$K1L~$ zH&xLZ`$HX%nplYtKxUa@D2@-o{*PpddC`ajKxUcYtOf7zD7XIbr4s{FR;CXnCL;)9 z*~gHz2pyHZ<|{CosPC%|K4K0~w3;PL<^XxC06ncbw9e{E4>28c{+tarKUYdP00-az z9DoCG01m*wVpF*L8k%q#Hx&SihAvQGL~3f>6u6MgGQ+SUg9KyTk=f%B1AZD>OXFGx z2-MIT8rS{`1ZijujcXsEXr^)P1LVy#uG@)?tsZ!FE&TwV!PQg%)bKxEEUBd)K&-Ij zd$Fo_AjftP&-*yaI~I;bDIpq^sVa^nsZfrin2V(v6}!yI8Y7hG0oL%SMejT@>jCkZ zITA3pkvRYd-~b$e18@KiAl6!_aj=i_KW^OCFkgYX)fc>G4&Ye@@XP^%G;Z9cp`lv> z1Zv#4O+!QLWTAOWZltB5br1bbCVcQoQe`2cLw|cC_U>ui_>W!2s-c}ZbLPz1zh!>_ X^JvGdh7#+{00000NkvXXu0mjf$N!~1 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..792104ff329b894c2df7d2ea574bb0d076b36c23 GIT binary patch literal 111 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeK3?y%aJ*@^(YymzYu0R?HmZtAK52P4Ng8YIR z9G=}s19HSYT^vIy7?T;A13&z?=ipaAr;$Fzn2UkwzT$m`Z~Df*BafCZDwc^29w|978G? plNp)=Km51n;8#DVkv_$ki-GCDeZ`wzAGd%sdb;|#taD0e0szsl7@+_F literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a451d538fb49f01e7b41761f29b70c276caa8a88 GIT binary patch literal 846 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z%1(N;uuoFc;y@;BZC4D%fkMB z&(e7n8GqmDE%ScA{QKqKFaLh2tFqra|DHb;xO4ukK3C(ks$-GCzu5=tSG26}S>wQC zcuHaEm#LC7LZ-P03b82OIG4&IXrraXywP4nM6au9k(jO)kLQX|U8RDj$6N#TxdI)} z2E<>{UfSDgm7XVj?#}JMZ;!m^xi8CCe(vWz7Jo~B=OMj~+YasdtFn`2Vd$DXBYTrK$`aF$yqdi1t-Yc~|IuI4 zY;U!zD(oNIESPs+q-f@pi2to-(-;`^dP@wQ*4q8P>+$46;*GJ3T&Cg28$O`6)X{p)#GE#Zf^31XQuZfls>)z%gMg2uF{tojll{aL3`kOhY zK4;-pk2e#dJ&vCjZtePBAA4cG^P_3*=FZZ2TfSX={X;9R^OvVrq{p23cF8V0&Q}cA z{IHm6I9;pN%u|)^MsroI=S4@=n@={i`dvEc&KWDiwbgU`V%hfV3va$Z#PM;0*sT01 zNgcZv$}C_`UXxOE@f-We;|iTWon06X8*}HaUo4?I`}=0MUzTn?$~{_d>yz}gJtoZg zyIY+7u!+%;y_VM#ddlW+$>?F($k literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1916017b4dfabcc2eb6fe37e21d0909a1ffee0ac GIT binary patch literal 936 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z?|>t;uuoFc;&n!BZDEsp$(qv zKhJ6Yofp9TWRI=oe_4I;(+v@9M~oDvF>d7PNOjO*PLdR$iBO?*!T*2Zdw;gLNV+iA zg#BsvN|B6gln9#Q=(RGHOKqxZU<->QN7-iyM;33_6oqf?O^2KV1FkG|?o{L~UFFO& zZ%>6r&SC-4D?Fu(uic4Pw|{l}_>Qf{pSSKWkE(fp_Py?7gC^s{#d;ULOjXw?aJ$;6 zzA>_IsQfFa7wr9P|62Ji|Ga0N@HndA@waYO;7YfEU-K=Rj06w9xUVjss&?nKd#3YD z?(4sT-?+@=+5Ss;%HlI^xAW_bPnGPr^V>Ur9#7kc&tCD5z2u`NzliU0Xc729 z!ftfCw5~hWTi3E@$+ZmkMXuqGo#gq{cAg4VIrkzf=7qhps-&&QooiQbT)F=Kp|j8D z8E)^31+RA~eVocSt@3$@@m`+*b)E#z{r+!eopxtRr9_gxzZ2#=Eoj zJrfH)uA%UG-X{B^?S^b8c!C=@t>ljmND}!h)hT&R{YjW%zsvlB9;s{XQ#GEPcw*HV zBT)SQL3-oo?&HnAwI|#%li2K~kL+Pf{9Iwbb*22}SLvKT6%C6;>z4gxzHy;_#csx& z7l)Y^?!V(CeRR*=U%DyY@(VmZ|Glp?u{-Hq?4KhqKdig_?TcxB-}D)!5k2uJ_YdT7;`G^y%4b4x|H zW>xb~*%gX?ocn^suKaFfpRquD2fIM_4AoFKO^#2TUN_1+BWCS*|HVr1jr1%TI|c^l zIZGxy_F44Um_5RG1_y(IgT{++pR+w5Cse&JR-AiA(a?2<&3m5gN1BhX{F%?csCvQ+ zzxm>K!zWlwwPW6*Txe0tcB9#)@)u8lmfZZEFO418ntxO-T>q2x;P!`?KLxJ+56poK Mp00i_>zopr0D*v~!THM^CH%{Zgr?rk+G6Ue$>bY3LO-&?u&#= z30o`*x`rT@2sx{VU|rW8#ecV$8D_uSndUvbzvucK<}W)VJc^qnNs=TDNpg#mBCB+Z9q=p(?y1J$D^yVfdN`yW2gF4T7$Q%-bOuEW z=pj!dbmAffggB&82dLr%Bydt7MUo^b3OJb+C7_pk>`s{^_}_-g6ld5O#Kj0mV)p|f z>|0?ru+u~cc#d29zI{g0L{)-gFm4(X1aJUv08OIEZ$4}9ad9S zw`aurjE-S-M!3pdtV1U71cX0u*L(Qeu~N)=5*s$>%7d1gIR)s4PKo0u1A)1u0k7*tc{3xZ-FcjIW>;N*sH=@Rp}Yo7?CtgwepnKC+itS~}Y zfb+TlBXWQYkOA!sFvh2R&kjfE6nOyd$aBmNYkbDL4AL%Mz{^aq#P1l?!ywBt6U6)v zXyr{3reXQmQpm;Q` zEMk>wKHwkBens(gTF78lYCgdEp6uPMVj)<743GiGta?0uxEwc3d zf(=X;8n{9lxC0pV3s}T;nne-? z)~>paPb2Pm^0|K<=BZzL=UhB(GofEdzxb2E^TH}`gTLOMTxqQ$XU@Gn_$BM&>AQ0e zeqmy>mM7*}PBNfg!`- M>FVdQ&MBb@05rae3;+NC literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..be5c062b5feeba5eff766b2fdae6dccb60cb4b0e GIT binary patch literal 343 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%&M7z$oYG;uuoF`1aODUuH)UwnS^i z62=e)(*V{`2k`*b5=Y_d%(4?EPx_&_WMu+pnDW{BolE^Uxj)}+yLIc`8R zg{90Lt0f&`?lL^eViO2AZ`iSm@yJ!6R31a2DNst&xD8@?udTeN#I8%r_sg2!eom$?*X_%R&Z!t~|1-cBYK4gm#+1_nk5gW={^?qALV VOCpPJ@c;vi!PC{xWt~$(69D@(h;je` literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dd5adfc7f990333743a5f3b34f09098c1377a4a4 GIT binary patch literal 915 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..507c5edd44bfb5efe41327d5cef511a108d35f13 GIT binary patch literal 1074 zcmV-21kL-2P)+t)}?IPi4`?n-BcG{I5ur- z(S>O`4876<#~6}k=`>Up4hWM~CUltm-)|Ri1Ni>m=Xw7B>2vXZK2lazR#sM4OxVB` zGWm@sMM^ z-;l({UGfbz*r=0l*o=*rbn~#1)BM5?7AY+r;d^YHV6D>PUbfT6IjR&Lx6#YE6o*+) zuBKoO-MHxHK}|s}>p4WgxIShmD&AzWe%_@-6DZ+pqS#BZCQ!mV^fOt5qM?`(rW@k` zYsq0Fe-hOnJV_2~IlvgxjZ&g0_?+}E6PL@R*DQCeAjPd$B{wAK#0SSo$OW75#b()K zHUs$LSLVnT+wsWmw*M;;$z=L|Dth}oRO&PU|Q78@DG)EG-;f#n348sTZV zVF~9kvs0GXg_$1ilRM_pLS`uz%MxXz$ZS8iD;g5KO+dIKazGp50^T5@IM7A7EpotC z!W~mwxJ0;WIiQwsy^0H?ge#H*l7zdexR6PK98gHOKye|H0y&_NaDn2&2;t_-0ZGDL zRb044xEeWN72z%_E_4yDUJlqsxMPX~IW&?Y+)+88op1pSBoqyIaEQ!Ol*$riq{ys= zxpK!+dNI=^OYFwX1(wJSoA?`3V^qijl?0d?Wut77&!^Zq%PbjU4n5fUgnXH!m=5f< zk}m_~a~ON=BxQz5hH%r$Y-PY44&!E!3Yp*}?#{7XA*iGWcbzi9S)vHoMVW4ZGIkRX z#g8(<%S4r;jjhy@q>w_AtYRDOq=;&>OpwD#d~}?I%ut7qs$`Bm_@-GlSja#4WRw!w z;XQoPAY06$A7At_L-u$UUpyrjB^DU#RVFRX2UE@R5j!x|C?D|xrk<0JScR#T@)5nxBvhE literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6edc956f5a65c870ef2a39883c4b39b1df7a0152 GIT binary patch literal 672 zcmV;R0$=@!P)6d<#9m|$TN|8T9&9uAoayHY{mq6i>T%Z5T9Fdg~; zu_yEak3t_Hc7#6QPUr&`83=oTxItj1Q7Zb)JvPyS11L59=xoCOC^b#cfx{>@Rp`J{ z-i18?PNI}_myRsw6G}mg4AP-9CBYGr8T6SRyx5OSI)N7%ULu2@k->{&$e=m;@nVI^7uiGl@Z>yl z*gPM&$v(X4<5$yz-v$CwKn;x$kOAhK{(M_QKqgQ_=LpCy)X+zA1mqfO=qN#%L;v)e z3?XTH1D9DvP-@6)&dr8CpcwjqJoT^#Kq>SAIVQs%069w3k(bQmDNv>w>VPPUwo_!5 z8Z~Aqk~g?#D93oi$7r1rIfHv9ay&&eH%ZRmo`I{VMMdZL3~Z&2T2v=*aL=(Ts7(cf zdyc(AZ7LhwbL=N-Q`P)l9MJwHpE0=S*a&LVsKGtQwy=m=RAg7nGqjB{%2cT`!zhDE-ix9rilVfHY}uMp~lLJvXH{Yj+BKO8(SL9JnuD%l_;f&u);JG zD~T!d3{#>>DKb3j-Dc)~WnuA7b6=hN&N=s_xIePU0!fW!=Xd1u~ z@>4AhU^yTB1_-CfOZUKsZYtx`Ctxbnu<5`@@Fx0O2B8cL`|)&`FIf z`^xIp0K#Rm>@?CUpqq%SddG^^0m2AbbqFy5*vtZ1G>(`72)A)x%r0|)BJR6`m;(q8 zaa+Vjvw%Ig?FwQRAdKU#A9R`r^y98`hs1;7r# zrvaFxSyf$tp*;=2PnuQN1!(UDz$DF@GW-fnnFkEgtRY>1#-1U-Ru)MrrcW1OYES2p zLnJkfsAqpqAJE1Vmg*^OhZDOACaP!D#z% zaW&ryPR)c5|J7Rd^IIhP&0uExDY}q-j)Bsb21mO+d>n=kTDmSU`>o)xebA!#u=%%) z!HM-CHc*fcB-j9F^aU{UWpJuKXqacfe&+M@7R!WL_6mE-3Sh@!jsZ=xL%(B3Pp0dhX z-}~8iZ8}x7Ytw3rb>8!WW(!>8gvbX|%(L8Tt*W)tr*fOTz|BoEzye!(!h`TZ_e}N3skx-zO^SrDC=DhC~uf&}^G5v}!gXYN5%q_RG?K zPiu;S&}4-UdC*~nsZkB%J*i!u9I)K+(H{o8+irQZ%Ob}_@65JIo^3QMdSZ;{3 zoUKhB?R7@g|C@k_K<9 zr#Cf3vW&J_y=-)BB+se#sE1uZ$~V%o!N^FT zlkJj*9i~NnxI&ktb($OX<9dwfP*VN*b$eIz>8>lY~kL+o(!$ACDO5}q}b*ff}*^wXaQms28U%aXsFGb!M z?o0i(&alWIQ*F~9txk-5a)}OoX?J1dmqq$=TjZOU^=3unonb!LlQo7${+X=R(fnpo z6oZR(I9$66qBz{{aJNLUcu{xHMbQ{)t$%!GNEDAres|EXCPp!trQHGNM{&8;!YGwW erBbQ%zx6lP^mn*@UzW7(f;~xz0z-riNSWW)Ue$38e6fNb`mhi=YvXD+X69B5oqhDn8-Rz~oV6^pW&_H6jd-knoINp&YN(pC8Z(g4z5~xr{cr4}K9K-o$F`?4% zwO>dRa>CpQesl?!&_t$?7xKYo!g?VyxVT_3A<>I)TF4GPDYuPKMt31U4CSjWgc@l# z`Fs|bEpapZBb1XNtP6QyF(FP^8DcnRHNoQ0gD1KZ(y7&*FjMMgRzlbSS`&!jjP8VZ z9_da<WO<47-n{QM{10f@f&D_Y8 z*5jjqu+}tSmDlmP>Io-J)j#ZWylMhnO*|@fm(B(gloOIed{pf{iGC(1C(Ph?(E7gu zJTxX)YCyPR=T2A?6tE)5g!Y75CI~yj@qqOULzvK(V0V2X=)yg}^^1)*ufdF#2cGS( zFN6>z@xieEI|hY3BL@#(7%$X>sWkf3Z(yRS8QIF}3n2uneE`daUg4Nm{ezm-7lJrS zTkGGJ^3M#m6Wmk-@RorZ))zturqD=(l8-`Zgq7rKT;GWkCr+F=aS|kd0RQV?fCTWH Q#sB~S07*qoM6N<$f`@##u>b%7 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4796c33bf648921e59c587f973ed86ef4e90d9ac GIT binary patch literal 107 zcmeAS@N?(olHy`uVBq!ia0vp^2_Vb}Bp6OT_L>T$m`Z~Df*BafCZDwc@>D!s978G? zlNE$}nEw3__|VukM^TVXKI63HpTrN1(`393XfQC$nblutbM5;IkSU(7elF{r5}E*i CZy%EY literal 0 HcmV?d00001 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +