Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e14e900276 | ||
|
|
6d8a923475 |
No files matched your search
@@ -5,6 +5,24 @@ Versions follow [semver](https://semver.org/): MAJOR.MINOR.PATCH
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [3.3.6] — 2026-10-08
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **VPN service notification icon.** The ongoing notification now uses the
|
||||||
|
bundled `notification_icon` drawable instead of `notification_status_icon`,
|
||||||
|
and the vendored runtime resolves that drawable by name at runtime (falling
|
||||||
|
back to the platform icon only if it is missing), so the foreground-service
|
||||||
|
notification no longer shows the generic Android info icon.
|
||||||
|
- **"Test Local SOCKS" no longer claims an outbound it never tested.** Xray
|
||||||
|
answers a SOCKS `CONNECT` before it dials upstream, so trusting the `0x00`
|
||||||
|
reply reported success for unroutable addresses and for hostnames that can
|
||||||
|
never resolve — meaning the test said OK while the server was down, which is
|
||||||
|
exactly when it gets run. It now forces a request/response round trip through
|
||||||
|
the tunnel and reports distinctly when the outbound never replies, drops the
|
||||||
|
connection, or returns unexpected data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [3.3.5] — 2026-09-06
|
## [3.3.5] — 2026-09-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -172,13 +172,13 @@ class MainActivity : FlutterActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val publicVersion = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
val publicVersion = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||||
.setSmallIcon(R.drawable.notification_status_icon)
|
.setSmallIcon(R.drawable.notification_icon)
|
||||||
.setContentTitle("Revolt VPN")
|
.setContentTitle("Revolt VPN")
|
||||||
.setContentText("VPN is active")
|
.setContentText("VPN is active")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||||
.setSmallIcon(R.drawable.notification_status_icon)
|
.setSmallIcon(R.drawable.notification_icon)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.setContentText(text)
|
.setContentText(text)
|
||||||
.setColorized(true)
|
.setColorized(true)
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
@@ -86,15 +86,22 @@ abstract final class LocalSocksTester {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Full user-facing test: authenticated local SOCKS handshake plus an
|
/// Full user-facing test: authenticated local SOCKS handshake, an outbound
|
||||||
/// outbound TCP CONNECT through the active ReVolt route.
|
/// CONNECT through the active ReVolt route, and — critically — a real
|
||||||
|
/// request/response exchange over that CONNECT.
|
||||||
|
///
|
||||||
|
/// The exchange is not optional politeness. Xray answers a SOCKS CONNECT
|
||||||
|
/// before it dials upstream, so the reply code alone says nothing about the
|
||||||
|
/// outbound: it returns success for unroutable addresses and for hostnames
|
||||||
|
/// that can never resolve. Without a round trip this test reports OK while
|
||||||
|
/// the server is down, which is precisely when a user runs it.
|
||||||
static Future<LocalSocksTestResult> test({
|
static Future<LocalSocksTestResult> test({
|
||||||
String host = '127.0.0.1',
|
String host = '127.0.0.1',
|
||||||
required int port,
|
required int port,
|
||||||
required String username,
|
required String username,
|
||||||
required String password,
|
required String password,
|
||||||
String targetHost = 'example.com',
|
String targetHost = 'example.com',
|
||||||
int targetPort = 443,
|
int targetPort = 80,
|
||||||
}) async {
|
}) async {
|
||||||
const totalTimeout = Duration(seconds: 8);
|
const totalTimeout = Duration(seconds: 8);
|
||||||
final stopwatch = Stopwatch()..start();
|
final stopwatch = Stopwatch()..start();
|
||||||
@@ -171,6 +178,43 @@ abstract final class LocalSocksTester {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _consumeAddress(reader, replyHead[3], timeout: remaining());
|
await _consumeAddress(reader, replyHead[3], timeout: remaining());
|
||||||
|
|
||||||
|
// The CONNECT reply above is not evidence of an outbound. Force a round
|
||||||
|
// trip and require the peer's answer before claiming the route works.
|
||||||
|
socket.add(
|
||||||
|
utf8.encode(
|
||||||
|
'GET / HTTP/1.1\r\nHost: $targetHost\r\nConnection: close\r\n\r\n',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await socket.flush();
|
||||||
|
|
||||||
|
List<int> banner;
|
||||||
|
try {
|
||||||
|
banner = await reader.readExactly(5, timeout: remaining());
|
||||||
|
} on TimeoutException {
|
||||||
|
return const LocalSocksTestResult(
|
||||||
|
ok: false,
|
||||||
|
latencyMs: null,
|
||||||
|
message: 'Local SOCKS5 is up, but the ReVolt outbound never replied.',
|
||||||
|
);
|
||||||
|
} on SocketException {
|
||||||
|
return const LocalSocksTestResult(
|
||||||
|
ok: false,
|
||||||
|
latencyMs: null,
|
||||||
|
message:
|
||||||
|
'Local SOCKS5 is up, but the ReVolt outbound dropped the connection.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String.fromCharCodes(banner) != 'HTTP/') {
|
||||||
|
return const LocalSocksTestResult(
|
||||||
|
ok: false,
|
||||||
|
latencyMs: null,
|
||||||
|
message:
|
||||||
|
'Local SOCKS5 is up, but the ReVolt outbound returned unexpected data.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
stopwatch.stop();
|
stopwatch.stop();
|
||||||
return LocalSocksTestResult(
|
return LocalSocksTestResult(
|
||||||
ok: true,
|
ok: true,
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class VpnConnection extends ChangeNotifier {
|
|||||||
await _vless.initializeVless(
|
await _vless.initializeVless(
|
||||||
providerBundleIdentifier: 'com.paladinvpn.app',
|
providerBundleIdentifier: 'com.paladinvpn.app',
|
||||||
notificationIconResourceType: 'drawable',
|
notificationIconResourceType: 'drawable',
|
||||||
notificationIconResourceName: 'notification_status_icon',
|
notificationIconResourceName: 'notification_icon',
|
||||||
);
|
);
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
|
|||||||
+2
-1
@@ -655,7 +655,8 @@ class XrayVPNService : VpnService() {
|
|||||||
android.app.Notification.Builder(this)
|
android.app.Notification.Builder(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
val icon = android.R.drawable.ic_dialog_info
|
val icon = resources.getIdentifier("notification_icon", "drawable", packageName)
|
||||||
|
.takeIf { it != 0 } ?: android.R.drawable.ic_dialog_info
|
||||||
|
|
||||||
val notification = builder
|
val notification = builder
|
||||||
.setContentTitle("VPN Service")
|
.setContentTitle("VPN Service")
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 3.3.5+31
|
version: 3.3.6+32
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
|
|||||||
Reference in new issue
Block a user