Desktop SDK (Java)¶
leaf-sdk-java is a Maven / Gradle artifact that wraps the native Rust crate with JNI. It lets any JVM application (Swing, JavaFX, Compose Desktop, Spring, plain CLI) drive the Leaf VPN on Windows, macOS, and Linux.
The native library (leafjni) and the leaf-ipc sidecar binary are packaged inside the JAR and extracted at runtime by QuestDB's jar-jni — you do not have to ship them separately.
1. Gradle setup¶
repositories {
mavenCentral()
maven { url 'https://repo.surfshield.org/repository/maven-public' }
}
dependencies {
implementation 'com.github.shiroedev2024:leaf-java-sdk:1.3.8'
// Provided transitively:
// com.google.code.gson:gson
// org.apache.httpcomponents.client5:httpclient5
// org.slf4j:slf4j-api
// org.questdb:jar-jni
}
If your artifact repository requires authentication, configure credentials through nexus.properties as demonstrated in the sample build.gradle.
2. LeafWrapper singleton¶
Everything is routed through LeafWrapper.getInstance(). On first call it loads leafjni out of the JAR using JarJniLoader.
import com.github.shiroedev2024.leaf.desktop.LeafWrapper;
import com.github.shiroedev2024.leaf.desktop.model.LogLevel;
LeafWrapper leaf = LeafWrapper.getInstance();
leaf.initLogger(LogLevel.INFO, /*output=*/1 /*Stderr*/, /*filePath=*/"", /*formatJson=*/false);
System.out.println("Leaf core: " + leaf.getVersion());
Listeners¶
Unlike the C FFI (which uses a single callback per call), the Java SDK uses delegates that you register once:
leaf.addCoreListener(new CoreListener() {
@Override public void onCoreStarting() { /* UI: connecting */ }
@Override public void onCoreStarted() { /* UI: core ready */ }
@Override public void onCoreStopped() { /* UI: stopped */ }
@Override public void onCoreError(String error) { /* UI: show error */ }
});
leaf.addLeafListener(new LeafListener() {
@Override public void onLeafStarted() { /* VPN up */ }
@Override public void onLeafStopped() { /* VPN down */ }
@Override public void onLeafReloaded() { /* config reloaded */ }
@Override public void onLeafError(String error) { /* VPN error */ }
});
API summary¶
| Method | Throws | Purpose |
|---|---|---|
getVersion() |
— | Leaf core version string. |
initLogger(level, output, filePath, formatJson) |
— | Initialise the native logger once per process. |
startCore(boolean daemon) |
LeafException |
Extract leaf-ipc from the JAR and launch it with root / UAC. |
isCoreRunning() |
— | IPC ping. |
stopCore(boolean daemon) |
— | Graceful shutdown. |
forceStopCore() |
— | Graceful + hard kill. |
testConfig() |
LeafException |
Validate the generated Leaf config. |
startLeaf() |
LeafException |
Start the VPN. |
stopLeaf() |
— | Stop the VPN. |
isLeafRunning() |
LeafException |
Check running status. |
reloadLeaf() |
LeafException |
Hot reload. |
autoUpdateSubscription(cb) |
— | Refresh with last-known flags. |
updateSubscription(tls, fragment, clientId, enableSpeedtest, enableTryAll, cb) |
LeafException |
Full control (each flag: -1 auto / 0 off / 1 on). |
updateCustomConfig(config, cb) |
LeafException |
Load a raw .conf string. |
importOfflineSubscription(path, passphrase, keyringJson, cb) |
LeafException |
Import a .leafsub bundle. |
updateAssets(major, minor, patch, cb) |
LeafException |
Refresh geoip.dat / geosite.dat asynchronously. |
verifyFileIntegrity() |
LeafException |
SHA-256 check before start. |
setupWintun(String path) |
LeafException |
Windows-only; extract Wintun driver. |
getPreferences() |
LeafException |
Read persistent preferences. |
setLeafPreferences(UpdateLeafPreferences p) |
LeafException |
Write preferences. |
ping() |
LeafException |
IPC liveness. |
All asynchronous calls that deliver a SubscriptionCallback / AssetsCallback marshal to a dedicated executor and invoke your callback off the native thread.
3. Quick start¶
import com.github.shiroedev2024.leaf.desktop.*;
import com.github.shiroedev2024.leaf.desktop.delegate.*;
import com.github.shiroedev2024.leaf.desktop.model.*;
public class VpnApp {
public static void main(String[] args) throws Exception {
String clientId = args[0];
LeafWrapper leaf = LeafWrapper.getInstance();
leaf.initLogger(LogLevel.INFO, 1, "", false);
CountDownLatch coreUp = new CountDownLatch(1);
leaf.addCoreListener(new CoreListener() {
public void onCoreStarting() {}
public void onCoreStarted() { coreUp.countDown(); }
public void onCoreStopped() {}
public void onCoreError(String e) { throw new RuntimeException(e); }
});
leaf.startCore(true); // extracts leaf-ipc, asks for root / UAC
coreUp.await();
CountDownLatch subOk = new CountDownLatch(1);
leaf.updateSubscription(-1, -1, clientId, 1, 1, new SubscriptionCallback() {
public void onSubscriptionUpdating() {}
public void onSubscriptionSuccess() { subOk.countDown(); }
public void onSubscriptionError(String e) { throw new RuntimeException(e); }
});
subOk.await();
leaf.verifyFileIntegrity();
CountDownLatch leafUp = new CountDownLatch(1);
leaf.addLeafListener(new LeafListener() {
public void onLeafStarted() { leafUp.countDown(); }
public void onLeafStopped() {}
public void onLeafReloaded() {}
public void onLeafError(String e) { throw new RuntimeException(e); }
});
leaf.startLeaf();
leafUp.await();
System.out.println("VPN up. Press Ctrl+C to stop.");
Thread.currentThread().join();
}
}
4. Preferences¶
LeafPreferences p = leaf.getPreferences();
UpdateLeafPreferences u = new UpdateLeafPreferences();
u.setEnableIpv6(p.isEnableIpv6());
u.setPreferIpv6(p.isPreferIpv6());
u.setMemoryLogger(p.isMemoryLogger());
u.setLogLevel(p.getLogLevel());
u.setApiPort(p.getApiPort());
u.setAutoReload(p.isAutoReload());
u.setCustomUserAgent(p.getUserAgent() == null ? "" : p.getUserAgent());
u.setBypassLan(p.isBypassLan());
u.setBypassLanInCore(p.isBypassLanInCore());
u.setFakeIp(true);
u.setForceResolveDomain(p.isForceResolveDomain());
u.setBypassGeoipList(List.of("cn", "ir", "ru"));
u.setBypassGeositeList(p.getBypassGeositeList());
u.setRejectGeoipList(p.getRejectGeoipList());
u.setRejectGeositeList(p.getRejectGeositeList());
u.setInternalDnsServer(p.isInternalDnsServer());
leaf.setLeafPreferences(u);
if (leaf.isLeafRunning()) leaf.reloadLeaf();
Full field descriptions are in the shared Preferences Reference. UpdateLeafPreferences is the write-side DTO; LeafPreferences is the read-side (annotated with Lombok @Data).
5. ApiClient¶
The SDK ships an Apache HttpClient-based API client that mirrors the Android and Rust versions. Use it to read live stats from the local core:
import com.github.shiroedev2024.leaf.desktop.api.ApiClient;
import com.github.shiroedev2024.leaf.desktop.model.*;
LeafPreferences prefs = leaf.getPreferences();
ApiClient api = new ApiClient("http://127.0.0.1:" + prefs.getApiPort());
UsageReply usage = api.getUsage("default");
OutboundListReply ol = api.getOutboundList();
OutboundHealthReply hr = api.getOutboundHealth("OUT");
api.setSelectOutboundItem("OUT", "us-ams-1");
api.forceFailoverHealthCheckAll();
See the Runtime HTTP API for the full endpoint contract.
6. Error handling¶
- All synchronous methods that can fail throw
LeafException(it extendsException). - Asynchronous methods (
updateAssets,updateSubscription, …) deliver errors through the typed callback. ForSubscriptionCallback.onSubscriptionError(String)you receive a plain string. LeafListener.onLeafError(String)andCoreListener.onCoreError(String)fire for runtime errors that originate inside the Rust code.
7. Packaging recipes¶
- Uber / shaded JAR — include
org.questdb:jar-jnisoJarJniLoadercan extract the native library from a combined artifact. - Modular (JPMS) —
jar-jniwrites temp files, so the SDK module needsrequires java.base;and a writablejava.io.tmpdir. - macOS notarization — the bundled
leafjni.dylibis signed with the upstream identity. You must resign with your own developer certificate during notarization. - Linux — shipping as a
.deb/.rpm? Install a polkit policy sopkexeccan elevateleaf-ipcnon-interactively if desired.
8. Next steps¶
- Consume live metrics: Runtime HTTP API.
- Switch to the Rust API for more granular control: Desktop SDK (Rust).
- Reference implementation (Tauri): Sample Projects and
github.com/shiroedev2024/leaf-desktop.