Skip to content

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 extends Exception).
  • Asynchronous methods (updateAssets, updateSubscription, …) deliver errors through the typed callback. For SubscriptionCallback.onSubscriptionError(String) you receive a plain string.
  • LeafListener.onLeafError(String) and CoreListener.onCoreError(String) fire for runtime errors that originate inside the Rust code.

7. Packaging recipes

  • Uber / shaded JAR — include org.questdb:jar-jni so JarJniLoader can extract the native library from a combined artifact.
  • Modular (JPMS)jar-jni writes temp files, so the SDK module needs requires java.base; and a writable java.io.tmpdir.
  • macOS notarization — the bundled leafjni.dylib is 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 so pkexec can elevate leaf-ipc non-interactively if desired.

8. Next steps