From 87a65b14bd36fbe1837dc7a537c27ac8706cde70 Mon Sep 17 00:00:00 2001 From: brightsunshine54 Date: Tue, 21 Apr 2026 23:51:03 +0700 Subject: [PATCH 1/3] add IPv6 support --- .../org/fptn/vpn/enums/ConnectionSubnets.java | 49 ++++--- .../fptn/vpn/services/vpn/FptnConnection.java | 121 ++++++++++++------ .../fptn/vpn/services/vpn/FptnService.java | 6 +- .../websocket/WebSocketClientWrapper.java | 19 +++ .../java/org/fptn/vpn/utils/NetworkUtils.java | 4 +- 5 files changed, 138 insertions(+), 61 deletions(-) diff --git a/app/src/main/java/org/fptn/vpn/enums/ConnectionSubnets.java b/app/src/main/java/org/fptn/vpn/enums/ConnectionSubnets.java index 02c07eda..19d2aa45 100644 --- a/app/src/main/java/org/fptn/vpn/enums/ConnectionSubnets.java +++ b/app/src/main/java/org/fptn/vpn/enums/ConnectionSubnets.java @@ -12,29 +12,44 @@ @Getter public enum ConnectionSubnets { - TUN_ADDRESS("10.10.0.1", 32), - TUN_INTERFACE_SUBNET("10.10.0.0", 16), - FPTN_SUBNET("172.16.0.0", 12), - LOCAL_SUBNET("192.168.0.0", 16), - ALL_SUBNET("0.0.0.0", 0), - - // todo: rename me! - HZ_WHAT_IS_THIS_IP("172.20.0.1", 32); + // todo: fix IPv6 + TUN_ADDRESS("10.10.0.1", 32, "::::", 128), + TUN_INTERFACE_SUBNET("10.10.0.0", 16, "::::", 128), + FPTN_SUBNET("172.16.0.0", 12, "::::", 128), + LOCAL_SUBNET("192.168.0.0", 16, "::::", 128), + ALL_SUBNET("0.0.0.0", 0, "::::", 128), + + // todo: rename me! STAS WHAT IS THIS ADDRESS? + HZ_WHAT_IS_THIS_IP("172.20.0.1", 32, "::::", 128); + + private final String ipV4Address; + private final int v4prefix; + + private final String ipV6Address; + private final int v6prefix; + + ConnectionSubnets(String ipV4Address, int v4prefix, String ipV6Address, int v6prefix) { + this.ipV4Address = ipV4Address; + this.v4prefix = v4prefix; + this.ipV6Address = ipV6Address; + this.v6prefix = v6prefix; + } - private final String ipAddress; - private final int prefix; + @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) + public IpPrefix getAsIpV4Prefix() throws UnknownHostException { + return new IpPrefix(InetAddress.getByName(ipV4Address), v4prefix); + } - ConnectionSubnets(String ipAddress, int prefix) { - this.ipAddress = ipAddress; - this.prefix = prefix; + public String getAsIpV4PrefixAsString() { + return ipV4Address + "/" + v4prefix; } @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) - public IpPrefix getAsIpPrefix() throws UnknownHostException { - return new IpPrefix(InetAddress.getByName(ipAddress), prefix); + public IpPrefix getAsIpV6Prefix() throws UnknownHostException { + return new IpPrefix(InetAddress.getByName(ipV6Address), v6prefix); } - public String getAsIPWithPrefix() { - return ipAddress + "/" + prefix; + public String getAsIpV6PrefixAsString() { + return ipV6Address + "/" + v6prefix; } } diff --git a/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java b/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java index fabba69f..3890ac12 100644 --- a/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java +++ b/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java @@ -31,7 +31,10 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; import java.net.InetAddress; +import java.net.UnknownHostException; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -61,6 +64,8 @@ public class FptnConnection extends Thread { * Maximum packet size is constrained by the MTU */ private static final int MAX_PACKET_SIZE = 1500; + public static final int IP_V4_PREFIX_LENGTH = 32; + public static final int IP_V6_PREFIX_LENGTH = 128; @Getter private final int connectionId; @@ -112,7 +117,7 @@ public FptnConnection(final FptnService service, final String sniHostName, final BypassCensorshipMethod censorshipStrategy, final PerAppVpnMode perAppVpnMode, - final List appInfos) { + final List appInfos) throws UnknownHostException { this.service = service; this.connectionId = connectionId; this.serverEntity = serverEntity; @@ -122,15 +127,18 @@ public FptnConnection(final FptnService service, this.censorshipStrategy = censorshipStrategy; this.perAppVpnMode = perAppVpnMode; this.appInfos = appInfos; + + InetAddress inetAddress = InetAddress.getByName(serverEntity.getHost()); this.webSocketClient = new WebSocketClientWrapper( this.serverEntity, - TUN_ADDRESS.getIpAddress(), + inetAddress instanceof Inet4Address ? TUN_ADDRESS.getIpV4Address() : TUN_ADDRESS.getIpV6Address(), this::onConnectionOpen, this::onMessageReceived, this::onConnectionFailure, this.sniHostName, this.censorshipStrategy ); + this.maxReconnectCount = maxReconnectCount; this.delayBetweenAttempts = delayBetweenAttempts; } @@ -143,16 +151,13 @@ public void run() { sendConnectionStateToService(ConnectionState.CONNECTING); VpnService.Builder builder = service.new Builder(); - builder.addAddress(TUN_ADDRESS.getIpAddress(), TUN_ADDRESS.getPrefix()); - builder.addRoute(HZ_WHAT_IS_THIS_IP.getIpAddress(), HZ_WHAT_IS_THIS_IP.getPrefix()); - builder.setMtu(MAX_PACKET_SIZE); - // enable blocking reading - builder.setBlocking(true); - - /* - From documentation: You can create either an allowed list, or, a disallowed list, but not both - */ - if (perAppVpnMode == PerAppVpnMode.ONLY_ALLOWED){ + builder.setBlocking(true) // enable blocking reading EXTREMELY IMPORTANT + .setSession(serverEntity.getName()) + .setConfigureIntent(configureVpnIntent) + .setMtu(MAX_PACKET_SIZE); + + // From documentation: You can create either an allowed list, or, a disallowed list, but not both + if (perAppVpnMode == PerAppVpnMode.ONLY_ALLOWED) { for (AppInfo appInfo : appInfos) { String packageName = appInfo.getPackageName(); try { @@ -161,7 +166,7 @@ public void run() { Log.d(TAG, "Package not found: " + packageName); } } - } else if (perAppVpnMode == PerAppVpnMode.EXCEPT_DISALLOWED){ + } else if (perAppVpnMode == PerAppVpnMode.EXCEPT_DISALLOWED) { for (AppInfo appInfo : appInfos) { String packageName = appInfo.getPackageName(); try { @@ -172,37 +177,71 @@ public void run() { } } - final String dnsServer = webSocketClient.getDnsServerIPv4(); - builder.addDnsServer(dnsServer); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - builder.excludeRoute(new IpPrefix(InetAddress.getByName(serverEntity.getHost()), 32)); - builder.excludeRoute(TUN_INTERFACE_SUBNET.getAsIpPrefix()); - builder.excludeRoute(FPTN_SUBNET.getAsIpPrefix()); - builder.excludeRoute(LOCAL_SUBNET.getAsIpPrefix()); - builder.addRoute(ALL_SUBNET.getIpAddress(), ALL_SUBNET.getPrefix()); - } else { - IPAddress rootSubnet = new IPAddressString(ALL_SUBNET.getAsIPWithPrefix()).getAddress(); - List subnetsToExclude = Stream.of( - serverEntity.getHost() + "/32", - TUN_INTERFACE_SUBNET.getAsIPWithPrefix(), - FPTN_SUBNET.getAsIPWithPrefix(), - LOCAL_SUBNET.getAsIPWithPrefix() - ) - .map(sub -> new IPAddressString(sub).getAddress()) - .collect(Collectors.toList()); - - List subnetsToInclude = new ArrayList<>(); - IPUtils.exclude(rootSubnet, subnetsToExclude, subnetsToInclude); - for (IPAddress ipAddress : subnetsToInclude) { - String hostIp = ipAddress.getLower().toAddressString().getHostAddress().toString(); - Integer networkPrefixLength = ipAddress.getLower().toAddressString().getNetworkPrefixLength(); - Log.d(getTag(), "subnetsToInclude.ipAddress: " + hostIp + "/" + networkPrefixLength); - builder.addRoute(hostIp, networkPrefixLength != null ? networkPrefixLength : 32); + InetAddress inetAddress = InetAddress.getByName(serverEntity.getHost()); + if (inetAddress instanceof Inet4Address) { + builder.addDnsServer(webSocketClient.getDnsServerIPv4()); + builder.addAddress(TUN_ADDRESS.getIpV4Address(), TUN_ADDRESS.getV4prefix()); + builder.addRoute(HZ_WHAT_IS_THIS_IP.getIpV4Address(), HZ_WHAT_IS_THIS_IP.getV4prefix()); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + builder.excludeRoute(new IpPrefix(inetAddress, IP_V4_PREFIX_LENGTH)); + builder.excludeRoute(TUN_INTERFACE_SUBNET.getAsIpV4Prefix()); + builder.excludeRoute(FPTN_SUBNET.getAsIpV4Prefix()); + builder.excludeRoute(LOCAL_SUBNET.getAsIpV4Prefix()); + builder.addRoute(ALL_SUBNET.getIpV4Address(), ALL_SUBNET.getV4prefix()); + } else { + IPAddress rootSubnet = new IPAddressString(ALL_SUBNET.getAsIpV4PrefixAsString()).getAddress(); + List subnetsToExclude = Stream.of( + String.format("%s/%s", serverEntity.getHost(), IP_V4_PREFIX_LENGTH), + TUN_INTERFACE_SUBNET.getAsIpV4PrefixAsString(), + FPTN_SUBNET.getAsIpV4PrefixAsString(), + LOCAL_SUBNET.getAsIpV4PrefixAsString() + ) + .map(sub -> new IPAddressString(sub).getAddress()) + .collect(Collectors.toList()); + + List subnetsToInclude = new ArrayList<>(); + IPUtils.exclude(rootSubnet, subnetsToExclude, subnetsToInclude); + for (IPAddress ipAddress : subnetsToInclude) { + String hostIp = ipAddress.getLower().toAddressString().getHostAddress().toString(); + Integer networkPrefixLength = ipAddress.getLower().toAddressString().getNetworkPrefixLength(); + Log.d(getTag(), "subnetsToInclude.ipAddress: " + hostIp + "/" + networkPrefixLength); + builder.addRoute(hostIp, networkPrefixLength != null ? networkPrefixLength : IP_V4_PREFIX_LENGTH); + } + } + } else if (inetAddress instanceof Inet6Address) { + builder.addDnsServer(webSocketClient.getDnsServerIPv6()); + builder.addAddress(TUN_ADDRESS.getIpV6Address(), TUN_ADDRESS.getV6prefix()); + builder.addRoute(HZ_WHAT_IS_THIS_IP.getIpV6Address(), HZ_WHAT_IS_THIS_IP.getV6prefix()); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + builder.excludeRoute(new IpPrefix(inetAddress, IP_V6_PREFIX_LENGTH)); + builder.excludeRoute(TUN_INTERFACE_SUBNET.getAsIpV6Prefix()); + builder.excludeRoute(FPTN_SUBNET.getAsIpV6Prefix()); + builder.excludeRoute(LOCAL_SUBNET.getAsIpV6Prefix()); + builder.addRoute(ALL_SUBNET.getIpV6Address(), ALL_SUBNET.getV6prefix()); + } else { + IPAddress rootSubnet = new IPAddressString(ALL_SUBNET.getAsIpV6PrefixAsString()).getAddress(); + List subnetsToExclude = Stream.of( + String.format("%s/%s", serverEntity.getHost(), IP_V6_PREFIX_LENGTH), + TUN_INTERFACE_SUBNET.getAsIpV6PrefixAsString(), + FPTN_SUBNET.getAsIpV6PrefixAsString(), + LOCAL_SUBNET.getAsIpV6PrefixAsString() + ) + .map(sub -> new IPAddressString(sub).getAddress()) + .collect(Collectors.toList()); + + List subnetsToInclude = new ArrayList<>(); + IPUtils.exclude(rootSubnet, subnetsToExclude, subnetsToInclude); + for (IPAddress ipAddress : subnetsToInclude) { + String hostIp = ipAddress.getLower().toAddressString().getHostAddress().toString(); + Integer networkPrefixLength = ipAddress.getLower().toAddressString().getNetworkPrefixLength(); + Log.d(getTag(), "subnetsToInclude.ipAddress: " + hostIp + "/" + networkPrefixLength); + builder.addRoute(hostIp, networkPrefixLength != null ? networkPrefixLength : IP_V6_PREFIX_LENGTH); + } } } - builder.setSession(serverEntity.getName()).setConfigureIntent(configureVpnIntent); - synchronized (service) { vpnInterface = builder.establish(); } diff --git a/app/src/main/java/org/fptn/vpn/services/vpn/FptnService.java b/app/src/main/java/org/fptn/vpn/services/vpn/FptnService.java index e0513854..f7216f94 100644 --- a/app/src/main/java/org/fptn/vpn/services/vpn/FptnService.java +++ b/app/src/main/java/org/fptn/vpn/services/vpn/FptnService.java @@ -46,6 +46,7 @@ import org.fptn.vpn.vpnclient.exception.ErrorCode; import org.fptn.vpn.vpnclient.exception.PVNClientException; +import java.net.UnknownHostException; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -284,7 +285,8 @@ public int onStartCommand(Intent intent, int flags, int startId) { connect(server, sniHostname); } - } catch (ExecutionException | InterruptedException | RuntimeException e) { + } catch (ExecutionException | InterruptedException | RuntimeException | + UnknownHostException e) { disconnect(new PVNClientException(e.getMessage())); } }); @@ -414,7 +416,7 @@ private void setConnectionState(ConnectionState connectionState, PVNClientExcept .build()); } - private void connect(ServerEntity serverEntity, String sniHostname) { + private void connect(ServerEntity serverEntity, String sniHostname) throws UnknownHostException { // Moving VPNService to foreground to give it higher priority in system updateNotificationWithMessage(getString(R.string.connecting_to) + serverEntity.getServerInfo(), ""); diff --git a/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java b/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java index 18aa0c8f..9b27cb3b 100644 --- a/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java +++ b/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java @@ -160,6 +160,25 @@ public String getDnsServerIPv4() throws PVNClientException { throw new PVNClientException(ErrorCode.CONNECT_TO_SERVER_ERROR); } + public String getDnsServerIPv6() throws PVNClientException { + // todo: fix to IPv6 + NativeResponse response = nativeHttpsClient.Get(DNS_URL, 15); + if (response != null) { + if (response.code == 200) { + try { + JSONObject jsonResponse = new JSONObject(response.body); + String dnsServer = jsonResponse.getString("dns"); + Log.i(getTag(), "DNS " + dnsServer + " retrieval successful."); + return dnsServer; + } catch (JSONException e) { + Log.e(getTag(), "Some error occurs on receiving DNS response: " + e); + throw new PVNClientException(ErrorCode.CONNECT_TO_SERVER_ERROR); + } + } + } + throw new PVNClientException(ErrorCode.CONNECT_TO_SERVER_ERROR); + } + private String getTag() { return this.getClass().getCanonicalName(); } diff --git a/app/src/main/java/org/fptn/vpn/utils/NetworkUtils.java b/app/src/main/java/org/fptn/vpn/utils/NetworkUtils.java index 834c62d7..5d2ccad5 100644 --- a/app/src/main/java/org/fptn/vpn/utils/NetworkUtils.java +++ b/app/src/main/java/org/fptn/vpn/utils/NetworkUtils.java @@ -8,6 +8,7 @@ import org.fptn.vpn.enums.NetworkType; import java.net.Inet4Address; +import java.net.Inet6Address; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; @@ -28,7 +29,8 @@ public static String getCurrentIPAddress() { while (inetAddressEnumeration.hasMoreElements()) { InetAddress inetAddress = inetAddressEnumeration.nextElement(); if (!inetAddress.isLoopbackAddress() - && inetAddress instanceof Inet4Address) { //!inetAddress.getHostAddress().contains(":") + && (inetAddress instanceof Inet4Address || inetAddress instanceof Inet6Address)) { + Log.d(TAG, "getCurrentIPAddress(): " + inetAddress.getHostAddress()); return inetAddress.getHostAddress(); } } From 706bd171c1b15be3a4f670bd9705976d6df49e7f Mon Sep 17 00:00:00 2001 From: Stas Skokov <7090stas@gmail.com> Date: Wed, 22 Apr 2026 18:09:13 +1000 Subject: [PATCH 2/3] fix websocket params --- app/src/main/cpp/src/websocket_client.cpp | 18 ++++++++++-------- .../wrapper_websocket_client.cpp | 5 +++-- .../wrapper_websocket_client.h | 2 ++ .../org/fptn/vpn/enums/ConnectionSubnets.java | 2 +- .../fptn/vpn/services/vpn/FptnConnection.java | 3 ++- .../websocket/NativeWebSocketClientImpl.java | 7 +++++-- .../websocket/WebSocketClientWrapper.java | 12 ++++++++---- 7 files changed, 31 insertions(+), 18 deletions(-) diff --git a/app/src/main/cpp/src/websocket_client.cpp b/app/src/main/cpp/src/websocket_client.cpp index 4034f8bd..55abc625 100644 --- a/app/src/main/cpp/src/websocket_client.cpp +++ b/app/src/main/cpp/src/websocket_client.cpp @@ -67,6 +67,7 @@ Java_org_fptn_vpn_services_websocket_NativeWebSocketClientImpl_nativeCreate( jstring server_ip_param, jint server_port_param, jstring tun_ipv4_param, + jstring tun_ipv6_param, jstring sni_param, jstring access_token_param, jstring expected_md5_fingerprint_param, @@ -76,24 +77,25 @@ Java_org_fptn_vpn_services_websocket_NativeWebSocketClientImpl_nativeCreate( auto server_ip = fptn::wrapper::ConvertToCString(env, server_ip_param); int server_port = server_port_param; auto tun_ipv4 = fptn::wrapper::ConvertToCString(env, tun_ipv4_param); + auto tun_ipv6 = fptn::wrapper::ConvertToCString(env, tun_ipv6_param); auto sni = fptn::wrapper::ConvertToCString(env, sni_param); auto access_token = fptn::wrapper::ConvertToCString(env, access_token_param); auto expected_md5_fingerprint = fptn::wrapper::ConvertToCString(env, expected_md5_fingerprint_param); - const auto censorship_strategy_name = fptn::wrapper::ConvertToCString( + const auto censorship_strategy_name = fptn::wrapper::ConvertToCString( env,censorship_strategy_name_param); - fptn::protocol::https::CensorshipStrategy censorship_strategy = + fptn::protocol::https::CensorshipStrategy censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSni; - if (censorship_strategy_name == "OBFUSCATION") { - censorship_strategy = fptn::protocol::https::CensorshipStrategy::kTlsObfuscator; - } else if (censorship_strategy_name == "SNI-REALITY") { - censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityMode; - } + if (censorship_strategy_name == "OBFUSCATION") { + censorship_strategy = fptn::protocol::https::CensorshipStrategy::kTlsObfuscator; + } else if (censorship_strategy_name == "SNI-REALITY") { + censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityMode; + } jobject global_object_ref = env->NewWeakGlobalRef(thiz); auto* websocket_client = new WrapperWebsocketClient(global_object_ref, - std::move(server_ip), server_port, std::move(tun_ipv4), std::move(sni), + std::move(server_ip), server_port, std::move(tun_ipv4), std::move(tun_ipv6), std::move(sni), std::move(access_token), std::move(expected_md5_fingerprint), censorship_strategy); auto jobj_client = reinterpret_cast(websocket_client); diff --git a/app/src/main/cpp/src/wrappers/wrapper_websocket_client/wrapper_websocket_client.cpp b/app/src/main/cpp/src/wrappers/wrapper_websocket_client/wrapper_websocket_client.cpp index 9439fc87..7e4f7414 100644 --- a/app/src/main/cpp/src/wrappers/wrapper_websocket_client/wrapper_websocket_client.cpp +++ b/app/src/main/cpp/src/wrappers/wrapper_websocket_client/wrapper_websocket_client.cpp @@ -23,6 +23,7 @@ WrapperWebsocketClient::WrapperWebsocketClient(jobject wrapper, std::string server_ip, int server_port, std::string tun_ipv4, + std::string tun_ipv6, std::string sni, std::string access_token, std::string expected_md5_fingerprint, @@ -33,6 +34,7 @@ WrapperWebsocketClient::WrapperWebsocketClient(jobject wrapper, server_ip_(std::move(server_ip)), server_port_(server_port), tun_ipv4_(std::move(tun_ipv4)), + tun_ipv6_(std::move(tun_ipv6)), sni_(std::move(sni)), access_token_(std::move(access_token)), expected_md5_fingerprint_(std::move(expected_md5_fingerprint)), @@ -97,8 +99,7 @@ void WrapperWebsocketClient::Run() { try { const auto server_ip_addr = fptn::common::network::IPv4Address::Create(server_ip_); const auto tun_ipv4_addr = fptn::common::network::IPv4Address::Create(tun_ipv4_); - const auto tun_ipv6_addr = fptn::common::network::IPv6Address::Create( - FPTN_CLIENT_DEFAULT_ADDRESS_IP6); + const auto tun_ipv6_addr = fptn::common::network::IPv6Address::Create(tun_ipv6_); if (!server_ip_addr.IsValid() || !tun_ipv4_addr.IsValid() || !tun_ipv6_addr.IsValid()) { SPDLOG_ERROR( diff --git a/app/src/main/cpp/src/wrappers/wrapper_websocket_client/wrapper_websocket_client.h b/app/src/main/cpp/src/wrappers/wrapper_websocket_client/wrapper_websocket_client.h index e9add55c..d912025a 100644 --- a/app/src/main/cpp/src/wrappers/wrapper_websocket_client/wrapper_websocket_client.h +++ b/app/src/main/cpp/src/wrappers/wrapper_websocket_client/wrapper_websocket_client.h @@ -20,6 +20,7 @@ class WrapperWebsocketClient final { std::string server_ip, int server_port, std::string tun_ipv4, + std::string tun_ipv6, std::string sni, std::string access_token, std::string expected_md5_fingerprint, @@ -55,6 +56,7 @@ class WrapperWebsocketClient final { const std::string server_ip_; const int server_port_; const std::string tun_ipv4_; + const std::string tun_ipv6_; const std::string sni_; const std::string access_token_; const std::string expected_md5_fingerprint_; diff --git a/app/src/main/java/org/fptn/vpn/enums/ConnectionSubnets.java b/app/src/main/java/org/fptn/vpn/enums/ConnectionSubnets.java index 19d2aa45..e6d5e77f 100644 --- a/app/src/main/java/org/fptn/vpn/enums/ConnectionSubnets.java +++ b/app/src/main/java/org/fptn/vpn/enums/ConnectionSubnets.java @@ -20,7 +20,7 @@ public enum ConnectionSubnets { ALL_SUBNET("0.0.0.0", 0, "::::", 128), // todo: rename me! STAS WHAT IS THIS ADDRESS? - HZ_WHAT_IS_THIS_IP("172.20.0.1", 32, "::::", 128); + HZ_WHAT_IS_THIS_IP("fd00::1", 32, "::::", 126); private final String ipV4Address; private final int v4prefix; diff --git a/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java b/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java index 3890ac12..425dc9c6 100644 --- a/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java +++ b/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java @@ -131,7 +131,8 @@ public FptnConnection(final FptnService service, InetAddress inetAddress = InetAddress.getByName(serverEntity.getHost()); this.webSocketClient = new WebSocketClientWrapper( this.serverEntity, - inetAddress instanceof Inet4Address ? TUN_ADDRESS.getIpV4Address() : TUN_ADDRESS.getIpV6Address(), + TUN_ADDRESS.getIpV4Address(), + TUN_ADDRESS.getIpV6Address(), this::onConnectionOpen, this::onMessageReceived, this::onConnectionFailure, diff --git a/app/src/main/java/org/fptn/vpn/services/websocket/NativeWebSocketClientImpl.java b/app/src/main/java/org/fptn/vpn/services/websocket/NativeWebSocketClientImpl.java index 44fbdb39..63af9e0c 100644 --- a/app/src/main/java/org/fptn/vpn/services/websocket/NativeWebSocketClientImpl.java +++ b/app/src/main/java/org/fptn/vpn/services/websocket/NativeWebSocketClientImpl.java @@ -31,7 +31,8 @@ public class NativeWebSocketClientImpl { public NativeWebSocketClientImpl( String host, int port, - String tunAddress, + String tunAddressIPv4, + String tunAddressIPv6, String accessToken, String md5ServerFingerprint, OnOpenCallback onOpenCallback, @@ -53,7 +54,8 @@ public NativeWebSocketClientImpl( this.nativeHandle = nativeCreate( host, port, - tunAddress, + tunAddressIPv4, + tunAddressIPv6, sniHostName, accessToken, md5ServerFingerprint, @@ -134,6 +136,7 @@ public void onMessageImpl(byte[] msg) { private native long nativeCreate(String server_ip, int server_port, String tun_ipv4, + String tun_ipv6, String sni, String access_token, String expected_md5_fingerprint, diff --git a/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java b/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java index 9b27cb3b..af5fc89a 100644 --- a/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java +++ b/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java @@ -21,7 +21,8 @@ public class WebSocketClientWrapper { private static final String LOGIN_URL = "/api/v1/login"; private final ServerEntity serverEntity; - private final String tunAddress; + private final String tunAddressIPv4; + private final String tunAddressIPv6; private final OnOpenCallback onOpenCallback; private final OnMessageReceivedCallback onMessageReceivedCallback; private final OnFailureCallback onFailureCallback; @@ -35,14 +36,16 @@ public class WebSocketClientWrapper { private boolean shutdown = false; public WebSocketClientWrapper(ServerEntity serverEntity, - String tunAddress, + String tunAddressIPv4, + String tunAddressIPv6, OnOpenCallback onOpenCallback, OnMessageReceivedCallback onMessageReceivedCallback, OnFailureCallback onFailureCallback, String sniHostName, BypassCensorshipMethod censorshipStrategy) { this.serverEntity = serverEntity; - this.tunAddress = tunAddress; + this.tunAddressIPv4 = tunAddressIPv4; + this.tunAddressIPv6 = tunAddressIPv6; this.onOpenCallback = onOpenCallback; this.onMessageReceivedCallback = onMessageReceivedCallback; this.onFailureCallback = onFailureCallback; @@ -71,7 +74,8 @@ public synchronized void startWebSocket() throws PVNClientException, WebSocketAl nativeWebSocketClient = new NativeWebSocketClientImpl( serverEntity.getHost(), serverEntity.getPort(), - tunAddress, + tunAddressIPv4, + tunAddressIPv6, accessToken, serverEntity.getMd5ServerFingerprint(), onOpenCallback, From 4f0dabcbba4e4240b361774885628e5567a0e92d Mon Sep 17 00:00:00 2001 From: Stas Skokov <7090stas@gmail.com> Date: Wed, 22 Apr 2026 20:33:26 +1000 Subject: [PATCH 3/3] IPv6 routing --- .../org/fptn/vpn/enums/ConnectionSubnets.java | 28 +++-- .../fptn/vpn/services/vpn/FptnConnection.java | 113 +++++++----------- .../vpn/services/websocket/DnsServers.java | 14 +++ .../websocket/WebSocketClientWrapper.java | 41 ++----- 4 files changed, 89 insertions(+), 107 deletions(-) create mode 100644 app/src/main/java/org/fptn/vpn/services/websocket/DnsServers.java diff --git a/app/src/main/java/org/fptn/vpn/enums/ConnectionSubnets.java b/app/src/main/java/org/fptn/vpn/enums/ConnectionSubnets.java index e6d5e77f..5185dd96 100644 --- a/app/src/main/java/org/fptn/vpn/enums/ConnectionSubnets.java +++ b/app/src/main/java/org/fptn/vpn/enums/ConnectionSubnets.java @@ -13,14 +13,11 @@ @Getter public enum ConnectionSubnets { // todo: fix IPv6 - TUN_ADDRESS("10.10.0.1", 32, "::::", 128), - TUN_INTERFACE_SUBNET("10.10.0.0", 16, "::::", 128), - FPTN_SUBNET("172.16.0.0", 12, "::::", 128), + LOCAL_TUN_ADDRESS("10.10.0.1", 32, "fd00::1", 128), + LOCAL_TUN_INTERFACE_SUBNET("10.10.0.0", 16, "fd00:::", 64), + FPTN_SERVER_SUBNET("172.16.0.0", 12, "fc00:1::", 64), LOCAL_SUBNET("192.168.0.0", 16, "::::", 128), - ALL_SUBNET("0.0.0.0", 0, "::::", 128), - - // todo: rename me! STAS WHAT IS THIS ADDRESS? - HZ_WHAT_IS_THIS_IP("fd00::1", 32, "::::", 126); + ALL_SUBNET("0.0.0.0", 0, "::::", 128); private final String ipV4Address; private final int v4prefix; @@ -52,4 +49,21 @@ public IpPrefix getAsIpV6Prefix() throws UnknownHostException { public String getAsIpV6PrefixAsString() { return ipV6Address + "/" + v6prefix; } + + public String getIpV4Address() { + return ipV4Address; + } + + public String getIpV6Address() { + return ipV6Address; + } + + public int getIpV4Prefix() { + return v4prefix; + } + + public int getIpV6Prefix() { + return v6prefix; + } + } diff --git a/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java b/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java index 425dc9c6..5b881675 100644 --- a/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java +++ b/app/src/main/java/org/fptn/vpn/services/vpn/FptnConnection.java @@ -1,11 +1,10 @@ package org.fptn.vpn.services.vpn; import static org.fptn.vpn.enums.ConnectionSubnets.ALL_SUBNET; -import static org.fptn.vpn.enums.ConnectionSubnets.FPTN_SUBNET; -import static org.fptn.vpn.enums.ConnectionSubnets.HZ_WHAT_IS_THIS_IP; +import static org.fptn.vpn.enums.ConnectionSubnets.FPTN_SERVER_SUBNET; import static org.fptn.vpn.enums.ConnectionSubnets.LOCAL_SUBNET; -import static org.fptn.vpn.enums.ConnectionSubnets.TUN_ADDRESS; -import static org.fptn.vpn.enums.ConnectionSubnets.TUN_INTERFACE_SUBNET; +import static org.fptn.vpn.enums.ConnectionSubnets.LOCAL_TUN_ADDRESS; +import static org.fptn.vpn.enums.ConnectionSubnets.LOCAL_TUN_INTERFACE_SUBNET; import android.app.PendingIntent; import android.content.pm.PackageManager; @@ -20,6 +19,7 @@ import org.fptn.vpn.enums.ConnectionState; import org.fptn.vpn.enums.NetworkType; import org.fptn.vpn.enums.PerAppVpnMode; +import org.fptn.vpn.services.websocket.DnsServers; import org.fptn.vpn.services.websocket.WebSocketAlreadyShutdownException; import org.fptn.vpn.services.websocket.WebSocketClientWrapper; import org.fptn.vpn.utils.DataRateCalculator; @@ -31,8 +31,6 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.net.Inet4Address; -import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.time.Duration; @@ -131,8 +129,8 @@ public FptnConnection(final FptnService service, InetAddress inetAddress = InetAddress.getByName(serverEntity.getHost()); this.webSocketClient = new WebSocketClientWrapper( this.serverEntity, - TUN_ADDRESS.getIpV4Address(), - TUN_ADDRESS.getIpV6Address(), + LOCAL_TUN_ADDRESS.getIpV4Address(), + LOCAL_TUN_ADDRESS.getIpV6Address(), this::onConnectionOpen, this::onMessageReceived, this::onConnectionFailure, @@ -177,69 +175,44 @@ public void run() { } } } - InetAddress inetAddress = InetAddress.getByName(serverEntity.getHost()); - if (inetAddress instanceof Inet4Address) { - builder.addDnsServer(webSocketClient.getDnsServerIPv4()); - builder.addAddress(TUN_ADDRESS.getIpV4Address(), TUN_ADDRESS.getV4prefix()); - builder.addRoute(HZ_WHAT_IS_THIS_IP.getIpV4Address(), HZ_WHAT_IS_THIS_IP.getV4prefix()); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - builder.excludeRoute(new IpPrefix(inetAddress, IP_V4_PREFIX_LENGTH)); - builder.excludeRoute(TUN_INTERFACE_SUBNET.getAsIpV4Prefix()); - builder.excludeRoute(FPTN_SUBNET.getAsIpV4Prefix()); - builder.excludeRoute(LOCAL_SUBNET.getAsIpV4Prefix()); - builder.addRoute(ALL_SUBNET.getIpV4Address(), ALL_SUBNET.getV4prefix()); - } else { - IPAddress rootSubnet = new IPAddressString(ALL_SUBNET.getAsIpV4PrefixAsString()).getAddress(); - List subnetsToExclude = Stream.of( - String.format("%s/%s", serverEntity.getHost(), IP_V4_PREFIX_LENGTH), - TUN_INTERFACE_SUBNET.getAsIpV4PrefixAsString(), - FPTN_SUBNET.getAsIpV4PrefixAsString(), - LOCAL_SUBNET.getAsIpV4PrefixAsString() - ) - .map(sub -> new IPAddressString(sub).getAddress()) - .collect(Collectors.toList()); - - List subnetsToInclude = new ArrayList<>(); - IPUtils.exclude(rootSubnet, subnetsToExclude, subnetsToInclude); - for (IPAddress ipAddress : subnetsToInclude) { - String hostIp = ipAddress.getLower().toAddressString().getHostAddress().toString(); - Integer networkPrefixLength = ipAddress.getLower().toAddressString().getNetworkPrefixLength(); - Log.d(getTag(), "subnetsToInclude.ipAddress: " + hostIp + "/" + networkPrefixLength); - builder.addRoute(hostIp, networkPrefixLength != null ? networkPrefixLength : IP_V4_PREFIX_LENGTH); - } - } - } else if (inetAddress instanceof Inet6Address) { - builder.addDnsServer(webSocketClient.getDnsServerIPv6()); - builder.addAddress(TUN_ADDRESS.getIpV6Address(), TUN_ADDRESS.getV6prefix()); - builder.addRoute(HZ_WHAT_IS_THIS_IP.getIpV6Address(), HZ_WHAT_IS_THIS_IP.getV6prefix()); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - builder.excludeRoute(new IpPrefix(inetAddress, IP_V6_PREFIX_LENGTH)); - builder.excludeRoute(TUN_INTERFACE_SUBNET.getAsIpV6Prefix()); - builder.excludeRoute(FPTN_SUBNET.getAsIpV6Prefix()); - builder.excludeRoute(LOCAL_SUBNET.getAsIpV6Prefix()); - builder.addRoute(ALL_SUBNET.getIpV6Address(), ALL_SUBNET.getV6prefix()); - } else { - IPAddress rootSubnet = new IPAddressString(ALL_SUBNET.getAsIpV6PrefixAsString()).getAddress(); - List subnetsToExclude = Stream.of( - String.format("%s/%s", serverEntity.getHost(), IP_V6_PREFIX_LENGTH), - TUN_INTERFACE_SUBNET.getAsIpV6PrefixAsString(), - FPTN_SUBNET.getAsIpV6PrefixAsString(), - LOCAL_SUBNET.getAsIpV6PrefixAsString() - ) - .map(sub -> new IPAddressString(sub).getAddress()) - .collect(Collectors.toList()); - - List subnetsToInclude = new ArrayList<>(); - IPUtils.exclude(rootSubnet, subnetsToExclude, subnetsToInclude); - for (IPAddress ipAddress : subnetsToInclude) { - String hostIp = ipAddress.getLower().toAddressString().getHostAddress().toString(); - Integer networkPrefixLength = ipAddress.getLower().toAddressString().getNetworkPrefixLength(); - Log.d(getTag(), "subnetsToInclude.ipAddress: " + hostIp + "/" + networkPrefixLength); - builder.addRoute(hostIp, networkPrefixLength != null ? networkPrefixLength : IP_V6_PREFIX_LENGTH); - } + + DnsServers dns_server = webSocketClient.getDnsServers(); + + // IPv4 + builder.addDnsServer(dns_server.getIpv4()); + builder.addAddress(LOCAL_TUN_ADDRESS.getIpV4Address(), LOCAL_TUN_ADDRESS.getIpV4Prefix()); + builder.addRoute(dns_server.getIpv4(), 32); + + // IPv6 + builder.addDnsServer(dns_server.getIpv6()); + builder.addAddress(LOCAL_TUN_ADDRESS.getIpV6Address(), LOCAL_TUN_ADDRESS.getIpV6Prefix()); + builder.addRoute(dns_server.getIpv6(), 128); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + builder.excludeRoute(new IpPrefix(inetAddress, IP_V4_PREFIX_LENGTH)); + builder.excludeRoute(LOCAL_TUN_INTERFACE_SUBNET.getAsIpV4Prefix()); + builder.excludeRoute(FPTN_SERVER_SUBNET.getAsIpV4Prefix()); + builder.excludeRoute(LOCAL_SUBNET.getAsIpV4Prefix()); + builder.addRoute(ALL_SUBNET.getIpV4Address(), ALL_SUBNET.getV4prefix()); + } else { + IPAddress rootSubnet = new IPAddressString(ALL_SUBNET.getAsIpV4PrefixAsString()).getAddress(); + List subnetsToExclude = Stream.of( + String.format("%s/%s", serverEntity.getHost(), IP_V4_PREFIX_LENGTH), + LOCAL_TUN_INTERFACE_SUBNET.getAsIpV4PrefixAsString(), + FPTN_SERVER_SUBNET.getAsIpV4PrefixAsString(), + LOCAL_SUBNET.getAsIpV4PrefixAsString() + ) + .map(sub -> new IPAddressString(sub).getAddress()) + .collect(Collectors.toList()); + + List subnetsToInclude = new ArrayList<>(); + IPUtils.exclude(rootSubnet, subnetsToExclude, subnetsToInclude); + for (IPAddress ipAddress : subnetsToInclude) { + String hostIp = ipAddress.getLower().toAddressString().getHostAddress().toString(); + Integer networkPrefixLength = ipAddress.getLower().toAddressString().getNetworkPrefixLength(); + Log.d(getTag(), "subnetsToInclude.ipAddress: " + hostIp + "/" + networkPrefixLength); + builder.addRoute(hostIp, networkPrefixLength != null ? networkPrefixLength : IP_V4_PREFIX_LENGTH); } } diff --git a/app/src/main/java/org/fptn/vpn/services/websocket/DnsServers.java b/app/src/main/java/org/fptn/vpn/services/websocket/DnsServers.java new file mode 100644 index 00000000..8937a1ff --- /dev/null +++ b/app/src/main/java/org/fptn/vpn/services/websocket/DnsServers.java @@ -0,0 +1,14 @@ +package org.fptn.vpn.services.websocket; + +public class DnsServers { + private final String ipv4; + private final String ipv6; + + public DnsServers(String ipv4, String ipv6) { + this.ipv4 = ipv4; + this.ipv6 = ipv6; + } + + public String getIpv4() { return ipv4; } + public String getIpv6() { return ipv6; } +} diff --git a/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java b/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java index af5fc89a..d3b33ed4 100644 --- a/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java +++ b/app/src/main/java/org/fptn/vpn/services/websocket/WebSocketClientWrapper.java @@ -146,42 +146,23 @@ private String getAccessToken() throws PVNClientException { throw new PVNClientException(ErrorCode.CONNECT_TO_SERVER_ERROR); } - public String getDnsServerIPv4() throws PVNClientException { + public DnsServers getDnsServers() throws PVNClientException { NativeResponse response = nativeHttpsClient.Get(DNS_URL, 15); - if (response != null) { - if (response.code == 200) { - try { - JSONObject jsonResponse = new JSONObject(response.body); - String dnsServer = jsonResponse.getString("dns"); - Log.i(getTag(), "DNS " + dnsServer + " retrieval successful."); - return dnsServer; - } catch (JSONException e) { - Log.e(getTag(), "Some error occurs on receiving DNS response: " + e); - throw new PVNClientException(ErrorCode.CONNECT_TO_SERVER_ERROR); - } + if (response != null && response.code == 200) { + try { + JSONObject jsonResponse = new JSONObject(response.body); + String ipv4 = jsonResponse.getString("dns"); + String ipv6 = jsonResponse.getString("dns_ipv6"); + Log.i(getTag(), "DNS_IPv4: " + ipv4 + " DNS_IPv6: " + ipv6); + return new DnsServers(ipv4, ipv6); + } catch (JSONException e) { + Log.e(getTag(), "Error parsing DNS response: " + e); + throw new PVNClientException(ErrorCode.CONNECT_TO_SERVER_ERROR); } } throw new PVNClientException(ErrorCode.CONNECT_TO_SERVER_ERROR); } - public String getDnsServerIPv6() throws PVNClientException { - // todo: fix to IPv6 - NativeResponse response = nativeHttpsClient.Get(DNS_URL, 15); - if (response != null) { - if (response.code == 200) { - try { - JSONObject jsonResponse = new JSONObject(response.body); - String dnsServer = jsonResponse.getString("dns"); - Log.i(getTag(), "DNS " + dnsServer + " retrieval successful."); - return dnsServer; - } catch (JSONException e) { - Log.e(getTag(), "Some error occurs on receiving DNS response: " + e); - throw new PVNClientException(ErrorCode.CONNECT_TO_SERVER_ERROR); - } - } - } - throw new PVNClientException(ErrorCode.CONNECT_TO_SERVER_ERROR); - } private String getTag() { return this.getClass().getCanonicalName();