Merge pull request #869 from majestrate/vpn-api-2019-10-03

android jni shim and vpn api for mobile
pull/872/head
Jeff 5 years ago committed by GitHub
commit 175e9f1324
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -167,8 +167,8 @@ endif()
if (NOT MSVC OR NOT MSVC_VERSION)
add_compile_options(${OPTIMIZE_FLAGS} ${CRYPTO_FLAGS})
endif()
add_subdirectory(${ABSEIL_DIR})
add_subdirectory(vendor/gtest)
add_subdirectory(${ABSEIL_DIR})
if (FS_LIB STREQUAL "cppbackport")
add_subdirectory(vendor)
@ -233,9 +233,6 @@ enable_testing()
if (NOT SHADOW)
add_subdirectory(test)
if(ANDROID)
add_library(${ANDROID_LIB} SHARED jni/lokinet_android.cpp)
set_property(TARGET ${ANDROID_LIB} PROPERTY CXX_STANDARD 14)
add_log_tag(${ANDROID_LIB})
target_link_libraries(${ANDROID_LIB} ${STATIC_LIB} ${LIBS})
add_subdirectory(jni)
endif(ANDROID)
endif()

@ -33,20 +33,16 @@ import android.widget.TextView;
public class LokiNetActivity extends Activity {
private static final String TAG = "lokinet-activity";
private TextView textView;
private static final String DefaultBootstrapURL = "https://i2p.rocks/bootstrap.signed";
private static final String DefaultBootstrapURL = "https://seed.lokinet.org/bootstrap.signed";
private AsyncBootstrap bootstrapper;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// copy assets
//String conf = copyFileAsset("daemon.ini");
textView = new TextView(this);
setContentView(textView);
Lokinet_JNI.loadLibraries();
System.loadLibrary("lokinetandroid");
}

@ -5,4 +5,5 @@ import android.net.VpnService;
public class LokinetService extends VpnService
{
}

@ -1,44 +0,0 @@
package network.loki.lokinet;
public class Lokinet_JNI {
public static final String STATUS_OK = "ok";
public static native String getABICompiledWith();
/**
* returns error info if failed
* returns "ok" if daemon initialized and started okay
*/
public static native String startLokinet(String config);
/**
* stop daemon if running
*/
public static native void stopLokinet();
/** get interface address we want */
public static native String getIfAddr();
/** get interface address range we want */
public static native int getIfRange();
/**
* change network status
*/
public static native void onNetworkStateChanged(boolean isConnected);
/**
* set vpn network interface fd pair
* @param rfd the file descriptor of read end
* @param wfd the file descriptor of the write end
*/
public static native void setVPNFileDescriptor(int rfd, int wfd);
/**
* load jni libraries
*/
public static void loadLibraries() {
System.loadLibrary("lokinetandroid");
}
}

@ -21,8 +21,6 @@ public class NetworkStateChangeReceiver extends BroadcastReceiver {
boolean isConnected = activeNetworkInfo!=null && activeNetworkInfo.isConnected();
// https://developer.android.com/training/monitoring-device-state/connectivity-monitoring.html?hl=ru
// boolean isWiFi = activeNetworkInfo!=null && (activeNetworkInfo.getType() == ConnectivityManager.TYPE_WIFI);
Lokinet_JNI.onNetworkStateChanged(isConnected);
} catch (Throwable tr) {
Log.d(TAG,"",tr);
}

@ -1,6 +1,6 @@
set(CMAKE_SYSTEM_NAME Windows)
set(TOOLCHAIN_PREFIX x86_64-w64-mingw32)
set(TOOLCHAIN_SUFFIX "")
set(TOOLCHAIN_SUFFIX "-posix")
set(WIN64_CROSS_COMPILE ON)
set(TOOLCHAIN_PATHS

@ -14,6 +14,7 @@
#include <cxxopts.hpp>
#include <string>
#include <iostream>
#include <future>
#ifdef _WIN32
#define wmin(x, y) (((x) < (y)) ? (x) : (y))
@ -85,12 +86,12 @@ resolvePath(std::string conffname)
/// this sets up, configures and runs the main context
static void
run_main_context(std::string conffname, bool multiThreaded, bool backgroundMode)
run_main_context(std::string conffname, llarp_main_runtime_opts opts)
{
// this is important, can downgrade from Info though
llarp::LogDebug("Running from: ", fs::current_path().string());
llarp::LogInfo("Using config file: ", conffname);
ctx = llarp_main_init(conffname.c_str(), multiThreaded);
ctx = llarp_main_init(conffname.c_str());
int code = 1;
if(ctx)
{
@ -102,7 +103,7 @@ run_main_context(std::string conffname, bool multiThreaded, bool backgroundMode)
code = llarp_main_setup(ctx);
llarp::util::SetThreadName("llarp-mainloop");
if(code == 0)
code = llarp_main_run(ctx, backgroundMode);
code = llarp_main_run(ctx, opts);
llarp_main_free(ctx);
}
exit_code.set_value(code);
@ -111,11 +112,11 @@ run_main_context(std::string conffname, bool multiThreaded, bool backgroundMode)
int
main(int argc, char *argv[])
{
bool multiThreaded = true;
llarp_main_runtime_opts opts;
const char *singleThreadVar = getenv("LLARP_SHADOW");
if(singleThreadVar && std::string(singleThreadVar) == "1")
{
multiThreaded = false;
opts.singleThreaded = true;
}
#ifdef _WIN32
@ -148,12 +149,10 @@ main(int argc, char *argv[])
options.parse_positional("config");
// clang-format on
bool genconfigOnly = false;
bool asRouter = false;
bool overWrite = false;
bool backgroundMode = false;
bool genconfigOnly = false;
bool asRouter = false;
bool overWrite = false;
std::string conffname; // suggestions: confFName? conf_fname?
try
{
auto result = options.parse(argc, argv);
@ -178,7 +177,7 @@ main(int argc, char *argv[])
if(result.count("version"))
{
std::cout << LLARP_VERSION << std::endl;
std::cout << llarp_version() << std::endl;
return 0;
}
@ -189,7 +188,7 @@ main(int argc, char *argv[])
if(result.count("background") > 0)
{
backgroundMode = true;
opts.background = true;
}
if(result.count("force") > 0)
@ -315,8 +314,7 @@ main(int argc, char *argv[])
return 0;
}
std::thread main_thread{
std::bind(&run_main_context, conffname, multiThreaded, backgroundMode)};
std::thread main_thread{std::bind(&run_main_context, conffname, opts)};
auto ftr = exit_code.get_future();
do
{
@ -324,11 +322,9 @@ main(int argc, char *argv[])
} while(ftr.wait_for(std::chrono::seconds(1)) != std::future_status::ready);
main_thread.join();
const auto code = ftr.get();
#ifdef _WIN32
::WSACleanup();
#endif
const auto code = ftr.get();
exit(code);
return code;
}

@ -1,15 +1,8 @@
#ifndef LLARP_H_
#define LLARP_H_
#include <stdint.h>
#include <unistd.h>
#ifdef __cplusplus
#include <constants/version.hpp>
#include <ev/ev.h>
#include <handlers/tun.hpp> // for handlers
#include <service/address.hpp> // for service::address
#include <service/endpoint.hpp>
#include <util/thread/logic.hpp>
#include <util/mem.h>
extern "C"
{
#endif
@ -27,106 +20,211 @@ extern "C"
/// llarp application context for C api
struct llarp_main;
/// initialize application context and load config
struct llarp_main *
llarp_main_init(const char *fname, bool multiProcess);
/// handle signal for main context
void
llarp_main_signal(struct llarp_main *ptr, int sig);
/// give main context a vpn file descriptor pair (android/ios)
void
llarp_main_inject_vpn_fd(struct llarp_main *m, int rfd, int wfd);
/// runtime options for main context from cli
struct llarp_main_runtime_opts
{
bool background = false;
bool debug = false;
bool singleThreaded = false;
};
/// setup main context, returns 0 on success
int
llarp_main_setup(struct llarp_main *ptr);
/// llarp_application config
struct llarp_config;
/// run main context, returns 0 on success, blocks until program end
int
llarp_main_run(struct llarp_main *ptr, bool backgroundMode);
/// get default config for current platform
struct llarp_config *
llarp_default_config();
/// free main context and end all operations
/// free previously allocated configuration
void
llarp_main_free(struct llarp_main *ptr);
llarp_config_free(struct llarp_config *);
/// packet writer to send packets to lokinet internals
struct llarp_vpn_writer_pipe;
/// packet reader to recv packets from lokinet internals
struct llarp_vpn_reader_pipe;
/// vpn io api
/// all hooks called in native code
/// for use with a vpn interface managed by external code
/// the external vpn interface MUST be up and have addresses set
struct llarp_vpn_io
{
/// private implementation
void *impl;
/// user data
void *user;
/// hook set by user called by lokinet core when lokinet is done with the
/// vpn io
void (*closed)(struct llarp_vpn_io *);
/// hook set by user called from lokinet core after attempting to inject
/// into endpoint passed a bool set to true if we were injected otherwise
/// set to false
void (*injected)(struct llarp_vpn_io *, bool);
/// hook set by user called every event loop tick
void (*tick)(struct llarp_vpn_io *);
};
#ifdef __cplusplus
/// info about the network interface that we give to lokinet core
struct llarp_vpn_ifaddr_info
{
/// name of the network interface
char ifname[64];
/// interface's address as string
char ifaddr[128];
/// netmask number of bits set
uint8_t netmask;
};
/// initialize llarp_vpn_io private implementation
/// returns false if either parameter is nullptr
bool
llarp_vpn_io_init(struct llarp_main *m, struct llarp_vpn_io *io);
/// get the packet pipe for writing IP packets to lokinet internals
/// returns nullptr if llarp_vpn_io is nullptr or not initialized
struct llarp_vpn_pkt_writer *
llarp_vpn_io_packet_writer(struct llarp_vpn_io *io);
/// get the packet pipe for reading IP packets from lokinet internals
/// returns nullptr if llarp_vpn_io is nullptr or not initialized
struct llarp_vpn_pkt_reader *
llarp_vpn_io_packet_reader(struct llarp_vpn_io *io);
/// blocking read on packet reader from lokinet internals
/// returns -1 on error, returns size of packet read
/// thread safe
ssize_t
llarp_vpn_io_readpkt(struct llarp_vpn_pkt_reader *r, unsigned char *dst,
size_t dstlen);
/// blocking write on packet writer to lokinet internals
/// returns false if we can't write this packet
/// return true if we wrote this packet
/// thread safe
bool
llarp_vpn_io_writepkt(struct llarp_vpn_pkt_writer *w, unsigned char *pktbuf,
size_t pktlen);
/// close vpn io and free private implementation after done
/// operation is async and calls llarp_vpn_io.closed after fully closed
/// after fully closed the llarp_vpn_io MUST be re-initialized by
/// llarp_vpn_io_init if it is to be used again
void
llarp_main_abort(struct llarp_main *ptr);
llarp_vpn_io_close_async(struct llarp_vpn_io *io);
/// get the default endpoint's name for injection
const char *
handleBaseCmdLineArgs(int argc, char *argv[]);
llarp_main_get_default_endpoint_name(struct llarp_main *m);
/// load nodeDB into memory
int
llarp_main_loadDatabase(struct llarp_main *ptr);
/// iterator on nodedb entries
int
llarp_main_iterateDatabase(struct llarp_main *ptr,
struct llarp_nodedb_iter i);
/// give main context a vpn io for mobile when it is reader to do io with
/// associated info tries to give the vpn io to endpoint with name epName a
/// deferred call to llarp_vpn_io.injected is queued unconditionally
/// thread safe
bool
llarp_main_inject_vpn_by_name(struct llarp_main *m, const char *epName,
struct llarp_vpn_io *io,
struct llarp_vpn_ifaddr_info info);
/// give main context a vpn io on its default endpoint
static bool
llarp_main_inject_default_vpn(struct llarp_main *m, struct llarp_vpn_io *io,
struct llarp_vpn_ifaddr_info info)
{
return llarp_main_inject_vpn_by_name(
m, llarp_main_get_default_endpoint_name(m), io, info);
}
/// put RC into nodeDB
/// load config from file by name
/// allocates new config and puts it into c
/// return false on failure
bool
llarp_main_putDatabase(struct llarp_main *ptr,
struct llarp::RouterContact &rc);
llarp_config_load_file(const char *fname, struct llarp_config **c);
// fwd declr
struct check_online_request;
/// loads config from file by name
/// uses already allocated config
/// return false on failure
bool
llarp_config_read_file(struct llarp_config *c, const char *f);
/// check_online_request hook definition
typedef void (*check_online_request_hook_func)(struct check_online_request *);
/// make a main context from configuration
/// copies config contents
struct llarp_main *
llarp_main_init_from_config(struct llarp_config *conf);
struct check_online_request
/// initialize application context and load config
static struct llarp_main *
llarp_main_init(const char *fname)
{
struct llarp_main *ptr;
struct llarp_router_lookup_job *job;
bool online;
size_t nodes;
bool first;
check_online_request_hook_func hook;
};
struct llarp_main *m = 0;
struct llarp_config *conf = 0;
if(!llarp_config_load_file(fname, &conf))
return 0;
if(conf == NULL)
return 0;
m = llarp_main_init_from_config(conf);
llarp_config_free(conf);
return m;
}
/// initialize applicatin context with all defaults
static struct llarp_main *
llarp_main_default_init()
{
struct llarp_main *m;
struct llarp_config *conf;
conf = llarp_default_config();
if(conf == 0)
return 0;
m = llarp_main_init_from_config(conf);
llarp_config_free(conf);
return m;
}
/// (re)configure main context
/// return true if (re)configuration was successful
bool
llarp_main_configure(struct llarp_main *ptr, struct llarp_config *conf);
/// get RC from DHT but wait until online
void
llarp_main_queryDHT(struct check_online_request *request);
/// return true if this main context is running
/// return false otherwise
bool
llarp_main_is_running(struct llarp_main *ptr);
/// get RC from DHT
/// handle signal for main context
void
llarp_main_queryDHT_RC(struct llarp_main *ptr,
struct llarp_router_lookup_job *job);
llarp_main_signal(struct llarp_main *ptr, int sig);
/// set up DNS libs with a context
bool
llarp_main_init_dnsd(struct llarp_main *ptr, struct dnsd_context *dnsd,
const llarp::Addr &dnsd_sockaddr,
const llarp::Addr &dnsc_sockaddr);
/// setup main context, returns 0 on success
int
llarp_main_setup(struct llarp_main *ptr);
/// set up dotLokiLookup with logic for setting timers
bool
llarp_main_init_dotLokiLookup(struct llarp_main *ptr,
struct dotLokiLookup *dll);
/// run main context, returns 0 on success, blocks until program end
int
llarp_main_run(struct llarp_main *ptr, struct llarp_main_runtime_opts opts);
/// tell main context to stop and wait for complete stop
/// after calling this you can call llarp_main_free safely
void
llarp_main_stop(struct llarp_main *ptr);
/// free main context and end all operations
void
llarp_main_free(struct llarp_main *ptr);
llarp::RouterContact *
llarp_main_getLocalRC(struct llarp_main *ptr);
/// get RC from nodeDB
llarp::RouterContact *
llarp_main_getDatabase(struct llarp_main *ptr, byte_t *pk);
/// get version string
const char *
llarp_version();
llarp_tun_io *
main_router_getRange(struct llarp_main *ptr);
/// return sizeof(llarp_main); for jni
size_t
llarp_main_size();
/// map an (host byte order) ip to a hidden service address
bool
main_router_mapAddress(struct llarp_main *ptr,
const llarp::service::Address &addr, uint32_t ip);
/// return sizeof(llarp_config); for jni
size_t
llarp_config_size();
/// info of possible path usage
bool
main_router_prefetch(struct llarp_main *ptr,
const llarp::service::Address &addr);
#ifdef __cplusplus
}
#endif
#endif

@ -1,6 +1,6 @@
#ifndef LLARP_HPP
#define LLARP_HPP
#include <llarp.h>
#include <util/fs.hpp>
#include <util/types.hpp>
#include <ev/ev.hpp>
@ -43,6 +43,10 @@ namespace llarp
struct Context
{
/// get context from main pointer
static Context *
Get(llarp_main *);
Context();
~Context();
@ -71,24 +75,32 @@ namespace llarp
int
LoadDatabase();
int
IterateDatabase(llarp_nodedb_iter &i);
bool
PutDatabase(struct llarp::RouterContact &rc);
llarp::RouterContact *
GetDatabase(const byte_t *pk);
int
Setup();
int
Run(bool daemonMode);
Run(llarp_main_runtime_opts opts);
void
HandleSignal(int sig);
bool
Configure();
/// close async
void
CloseAsync();
/// wait until closed and done
void
Wait();
/// call a function in logic thread
/// return true if queued for calling
/// return false if not queued for calling
bool
CallSafe(std::function< void(void) > f);
private:
void
SetPIDFile(const std::string &fname);
@ -99,9 +111,6 @@ namespace llarp
void
RemovePIDFile() const;
bool
Configure();
void
SigINT();
@ -116,10 +125,8 @@ namespace llarp
std::string configfile;
std::string pidfile;
std::unique_ptr< std::promise< void > > closeWaiter;
};
} // namespace llarp
llarp::Context *
llarp_main_get_context(llarp_main *m);
#endif

@ -0,0 +1,10 @@
set(ANDROID_SRC
lokinet_config.cpp
lokinet_daemon.cpp
lokinet_vpn.cpp
)
add_library(${ANDROID_LIB} SHARED ${ANDROID_SRC})
set_property(TARGET ${ANDROID_LIB} PROPERTY CXX_STANDARD 14)
add_log_tag(${ANDROID_LIB})
target_link_libraries(${ANDROID_LIB} ${STATIC_LIB} ${LIBS})

@ -1,237 +0,0 @@
#include <llarp.h>
#include <config/config.hpp>
#include <util/fs.hpp>
#include <llarp.hpp>
#include <router/router.hpp>
#include <jni.h>
#include <signal.h>
#include <memory>
#include <thread>
struct AndroidMain
{
llarp_main* m_impl = nullptr;
std::thread* m_thread = nullptr;
std::string configFile;
/// set configuration and ensure files
bool
Configure(const char* conf, const char* basedir)
{
configFile = conf;
return llarp_ensure_config(conf, basedir, false, false);
}
/// reload config on runtime
bool
ReloadConfig()
{
if(!m_impl)
return false;
llarp_main_signal(m_impl, SIGHUP);
return true;
}
/// start daemon thread
bool
Start()
{
if(m_impl || m_thread)
return true;
m_impl = llarp_main_init(configFile.c_str(), true);
if(m_impl == nullptr)
return false;
if(llarp_main_setup(m_impl, false))
{
llarp_main_free(m_impl);
m_impl = nullptr;
return false;
}
m_thread = new std::thread(std::bind(&AndroidMain::Run, this));
return true;
}
/// return true if we are running
bool
Running() const
{
return m_impl != nullptr && m_thread != nullptr;
}
/// blocking run
void
Run()
{
if(llarp_main_run(m_impl))
{
// on error
llarp::LogError("daemon run fail");
llarp_main* ptr = m_impl;
m_impl = nullptr;
llarp_main_signal(ptr, SIGINT);
llarp_main_free(ptr);
}
}
const char*
GetIfAddr()
{
std::string addr;
if(m_impl)
{
auto* ctx = llarp_main_get_context(m_impl);
if(!ctx)
return "";
ctx->router->hiddenServiceContext().ForEachService(
[&addr](const std::string&,
const llarp::service::Endpoint_ptr& ep) -> bool {
if(addr.empty())
{
if(ep->HasIfAddr())
{
// TODO: v4
const auto ip = ep->GetIfAddr();
if(ip.h)
{
addr = ip.ToString();
return false;
}
}
}
return true;
});
}
return addr.c_str();
}
int
GetIfRange() const
{
if(m_impl)
{
auto* ctx = llarp_main_get_context(m_impl);
if(!ctx)
return -1;
}
return -1;
}
void
SetVPN_FD(int rfd, int wfd)
{
(void)rfd;
(void)wfd;
// if(m_impl)
// llarp_main_inject_vpn_fd(m_impl, rfd, wfd);
}
/// stop daemon thread
void
Stop()
{
if(m_impl)
llarp_main_signal(m_impl, SIGINT);
m_thread->join();
delete m_thread;
m_thread = nullptr;
if(m_impl)
llarp_main_free(m_impl);
m_impl = nullptr;
}
typedef std::unique_ptr< AndroidMain > Ptr;
};
static AndroidMain::Ptr daemon_ptr(new AndroidMain());
extern "C"
{
JNIEXPORT jstring JNICALL
Java_network_loki_lokinet_Lokinet_1JNI_getABICompiledWith(JNIEnv* env, jclass)
{
// TODO: fixme
return env->NewStringUTF("android");
}
JNIEXPORT jstring JNICALL
Java_network_loki_lokinet_Lokinet_1JNI_startLokinet(JNIEnv* env, jclass,
jstring configfile)
{
if(daemon_ptr->Running())
return env->NewStringUTF("already running");
std::string conf;
fs::path basepath;
{
const char* nativeString = env->GetStringUTFChars(configfile, JNI_FALSE);
conf += std::string(nativeString);
env->ReleaseStringUTFChars(configfile, nativeString);
basepath = fs::path(conf).parent_path();
}
if(daemon_ptr->Configure(conf.c_str(), basepath.string().c_str()))
{
if(daemon_ptr->Start())
return env->NewStringUTF("ok");
else
return env->NewStringUTF("failed to start daemon");
}
else
return env->NewStringUTF("failed to configure daemon");
}
JNIEXPORT void JNICALL
Java_network_loki_lokinet_Lokinet_1JNI_stopLokinet(JNIEnv*, jclass)
{
if(daemon_ptr->Running())
{
daemon_ptr->Stop();
}
}
JNIEXPORT void JNICALL
Java_network_loki_lokinet_Lokinet_1JNI_setVPNFileDescriptor(JNIEnv*, jclass,
jint rfd,
jint wfd)
{
daemon_ptr->SetVPN_FD(rfd, wfd);
}
JNIEXPORT jstring JNICALL
Java_network_loki_lokinet_Lokinet_1JNI_getIfAddr(JNIEnv* env, jclass)
{
if(daemon_ptr)
return env->NewStringUTF(daemon_ptr->GetIfAddr());
else
return env->NewStringUTF("");
}
JNIEXPORT jint JNICALL
Java_network_loki_lokinet_Lokinet_1JNI_getIfRange(JNIEnv*, jclass)
{
if(daemon_ptr)
return daemon_ptr->GetIfRange();
else
return -1;
}
JNIEXPORT void JNICALL
Java_network_loki_lokinet_Lokinet_1JNI_onNetworkStateChanged(
JNIEnv*, jclass, jboolean isConnected)
{
if(isConnected)
{
if(!daemon_ptr->Running())
{
if(!daemon_ptr->Start())
{
// TODO: do some kind of callback here
}
}
}
else if(daemon_ptr->Running())
{
daemon_ptr->Stop();
}
}
}

@ -0,0 +1,37 @@
#include "network_loki_lokinet_LokinetConfig.h"
#include <llarp.hpp>
#include "lokinet_jni_common.hpp"
extern "C"
{
JNIEXPORT jobject JNICALL
Java_network_loki_lokinet_LokinetConfig_Obtain(JNIEnv* env, jclass)
{
llarp_config* conf = llarp_default_config();
if(conf == nullptr)
return nullptr;
return env->NewDirectByteBuffer(conf, llarp_config_size());
}
JNIEXPORT void JNICALL
Java_network_loki_lokinet_LokinetConfig_Free(JNIEnv* env, jclass, jobject buf)
{
llarp_config_free(FromBuffer< llarp_config >(env, buf));
}
JNIEXPORT jboolean JNICALL
Java_network_loki_lokinet_LokinetConfig_Load(JNIEnv* env, jobject self,
jstring fname)
{
llarp_config* conf = GetImpl< llarp_config >(env, self);
if(conf == nullptr)
return JNI_FALSE;
return VisitStringAsStringView< jboolean >(
env, fname, [conf](llarp::string_view val) -> jboolean {
const auto filename = llarp::string_view_string(val);
if(llarp_config_read_file(conf, filename.c_str()))
return JNI_TRUE;
return JNI_FALSE;
});
}
}

@ -0,0 +1,83 @@
#include "network_loki_lokinet_LokinetDaemon.h"
#include "lokinet_jni_common.hpp"
#include "lokinet_jni_vpnio.hpp"
#include <llarp.h>
extern "C"
{
JNIEXPORT jobject JNICALL
Java_network_loki_lokinet_LokinetDaemon_Obtain(JNIEnv *env, jclass)
{
llarp_main *ptr = llarp_main_default_init();
if(ptr == nullptr)
return nullptr;
return env->NewDirectByteBuffer(ptr, llarp_main_size());
}
JNIEXPORT void JNICALL
Java_network_loki_lokinet_LokinetDaemon_Free(JNIEnv *env, jclass, jobject buf)
{
llarp_main *ptr = FromBuffer< llarp_main >(env, buf);
llarp_main_free(ptr);
}
JNIEXPORT jboolean JNICALL
Java_network_loki_lokinet_LokinetDaemon_Configure(JNIEnv *env, jobject self,
jobject conf)
{
llarp_main *ptr = GetImpl< llarp_main >(env, self);
llarp_config *config = GetImpl< llarp_config >(env, conf);
if(ptr == nullptr || config == nullptr)
return JNI_FALSE;
if(llarp_main_configure(ptr, config))
return JNI_TRUE;
return llarp_main_setup(ptr) == 0 ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jint JNICALL
Java_network_loki_lokinet_LokinetDaemon_Mainloop(JNIEnv *env, jobject self)
{
static llarp_main_runtime_opts opts;
llarp_main *ptr = GetImpl< llarp_main >(env, self);
if(ptr == nullptr)
return -1;
return llarp_main_run(ptr, opts);
}
JNIEXPORT jboolean JNICALL
Java_network_loki_lokinet_LokinetDaemon_IsRunning(JNIEnv *env, jobject self)
{
llarp_main *ptr = GetImpl< llarp_main >(env, self);
return (ptr != nullptr && llarp_main_is_running(ptr)) ? JNI_TRUE
: JNI_FALSE;
}
JNIEXPORT jboolean JNICALL
Java_network_loki_lokinet_LokinetDaemon_Stop(JNIEnv *env, jobject self)
{
llarp_main *ptr = GetImpl< llarp_main >(env, self);
if(ptr == nullptr)
return JNI_FALSE;
if(not llarp_main_is_running(ptr))
return JNI_FALSE;
llarp_main_stop(ptr);
return llarp_main_is_running(ptr) ? JNI_FALSE : JNI_TRUE;
}
JNIEXPORT jboolean JNICALL
Java_network_loki_lokinet_LokinetDaemon_InjectVPN(JNIEnv *env, jobject self,
jobject vpn)
{
llarp_main *ptr = GetImpl< llarp_main >(env, self);
lokinet_jni_vpnio *impl = GetImpl< lokinet_jni_vpnio >(env, vpn);
if(ptr == nullptr || impl == nullptr)
return JNI_FALSE;
if(impl->info.netmask == 0)
return JNI_FALSE;
if(not impl->Init(ptr))
return JNI_FALSE;
return llarp_main_inject_default_vpn(ptr, &impl->io, impl->info)
? JNI_TRUE
: JNI_FALSE;
}
}

@ -0,0 +1,85 @@
#ifndef LOKINET_JNI_COMMON_HPP
#define LOKINET_JNI_COMMON_HPP
#include <jni.h>
#include <util/string_view.hpp>
#include <functional>
/// visit string as native bytes
/// jvm uses some unholy encoding internally so we convert it to utf-8
template < typename T, typename V >
static T
VisitStringAsStringView(JNIEnv* env, jobject str, V visit)
{
const jclass stringClass = env->GetObjectClass(str);
const jmethodID getBytes =
env->GetMethodID(stringClass, "getBytes", "(Ljava/lang/String;)[B");
const jstring charsetName = env->NewStringUTF("UTF-8");
const jbyteArray stringJbytes =
(jbyteArray)env->CallObjectMethod(str, getBytes, charsetName);
env->DeleteLocalRef(charsetName);
const size_t length = env->GetArrayLength(stringJbytes);
jbyte* pBytes = env->GetByteArrayElements(stringJbytes, NULL);
T result = visit(llarp::string_view((const char*)pBytes, length));
env->ReleaseByteArrayElements(stringJbytes, pBytes, JNI_ABORT);
env->DeleteLocalRef(stringJbytes);
return std::move(result);
}
/// cast jni buffer to T *
template < typename T >
static T*
FromBuffer(JNIEnv* env, jobject o)
{
if(o == nullptr)
return nullptr;
return static_cast< T* >(env->GetDirectBufferAddress(o));
}
/// get T * from object member called membername
template < typename T >
static T*
FromObjectMember(JNIEnv* env, jobject self, const char* membername)
{
jclass cl = env->GetObjectClass(self);
jfieldID name = env->GetFieldID(cl, membername, "Ljava/nio/Buffer;");
jobject buffer = env->GetObjectField(self, name);
return FromBuffer< T >(env, buffer);
}
/// visit object string member called membername as bytes
template < typename T, typename V >
static T
VisitObjectMemberStringAsStringView(JNIEnv* env, jobject self,
const char* membername, V v)
{
jclass cl = env->GetObjectClass(self);
jfieldID name = env->GetFieldID(cl, membername, "Ljava/lang/String;");
jobject str = env->GetObjectField(self, name);
return VisitStringAsStringView< T, V >(env, str, v);
}
/// get object member int called membername
template < typename Int_t >
Int_t
GetObjectMemberAsInt(JNIEnv* env, jobject self, const char* membername)
{
jclass cl = env->GetObjectClass(self);
jfieldID name = env->GetFieldID(cl, membername, "I");
return env->GetIntField(self, name);
}
/// get implementation on jni type
template < typename T >
T*
GetImpl(JNIEnv* env, jobject self)
{
return FromObjectMember< T >(env, self, "impl");
}
#endif

@ -0,0 +1,150 @@
#ifndef LOKINET_JNI_VPNIO_HPP
#define LOKINET_JNI_VPNIO_HPP
#include <llarp.h>
#include <memory>
#include <future>
#include <util/string_view.hpp>
#include <algorithm>
#include <jni.h>
namespace lokinet
{
struct VPNIO
{
static VPNIO *
Get(llarp_vpn_io *vpn)
{
return static_cast< VPNIO * >(vpn->user);
}
virtual ~VPNIO() = default;
llarp_vpn_io io;
llarp_vpn_ifaddr_info info{{0}, {0}, 0};
std::unique_ptr< std::promise< void > > closeWaiter;
void
Closed()
{
if(closeWaiter)
closeWaiter->set_value();
}
virtual void
InjectSuccess() = 0;
virtual void
InjectFail() = 0;
virtual void
Tick() = 0;
VPNIO()
{
io.impl = nullptr;
io.user = this;
io.closed = [](llarp_vpn_io *vpn) { VPNIO::Get(vpn)->Closed(); };
io.injected = [](llarp_vpn_io *vpn, bool good) {
VPNIO *ptr = VPNIO::Get(vpn);
if(good)
ptr->InjectSuccess();
else
ptr->InjectFail();
};
io.tick = [](llarp_vpn_io *vpn) { VPNIO::Get(vpn)->Tick(); };
}
bool
Init(llarp_main *ptr)
{
if(Ready())
return false;
return llarp_vpn_io_init(ptr, &io);
}
bool
Ready() const
{
return io.impl != nullptr;
}
void
Close()
{
if(not Ready())
return;
if(closeWaiter)
return;
closeWaiter = std::make_unique< std::promise< void > >();
llarp_vpn_io_close_async(&io);
closeWaiter->get_future().wait();
closeWaiter.reset();
io.impl = nullptr;
}
llarp_vpn_pkt_reader *
Reader()
{
return llarp_vpn_io_packet_reader(&io);
}
llarp_vpn_pkt_writer *
Writer()
{
return llarp_vpn_io_packet_writer(&io);
}
ssize_t
ReadPacket(void *dst, size_t len)
{
if(not Ready())
return -1;
unsigned char *buf = (unsigned char *)dst;
return llarp_vpn_io_readpkt(Reader(), buf, len);
}
bool
WritePacket(void *pkt, size_t len)
{
if(not Ready())
return false;
unsigned char *buf = (unsigned char *)pkt;
return llarp_vpn_io_writepkt(Writer(), buf, len);
}
void
SetIfName(llarp::string_view val)
{
const auto sz = std::min(val.size(), sizeof(info.ifname));
std::copy_n(val.data(), sz, info.ifname);
}
void
SetIfAddr(llarp::string_view val)
{
const auto sz = std::min(val.size(), sizeof(info.ifaddr));
std::copy_n(val.data(), sz, info.ifaddr);
}
};
} // namespace lokinet
struct lokinet_jni_vpnio : public lokinet::VPNIO
{
void
InjectSuccess() override
{
}
void
InjectFail() override
{
}
void
Tick() override
{
}
};
#endif

@ -0,0 +1,86 @@
#include "network_loki_lokinet_LokinetVPN.h"
#include "lokinet_jni_vpnio.hpp"
#include "lokinet_jni_common.hpp"
#include <net/ip.hpp>
extern "C"
{
JNIEXPORT jint JNICALL
Java_network_loki_lokinet_LokinetVPN_PacketSize(JNIEnv *, jclass)
{
return llarp::net::IPPacket::MaxSize;
}
JNIEXPORT jobject JNICALL
Java_network_loki_lokinet_LokinetVPN_Alloc(JNIEnv *env, jclass)
{
lokinet_jni_vpnio *vpn = new lokinet_jni_vpnio();
return env->NewDirectByteBuffer(vpn, sizeof(lokinet_jni_vpnio));
}
JNIEXPORT void JNICALL
Java_network_loki_lokinet_LokinetVPN_Free(JNIEnv *env, jclass, jobject buf)
{
lokinet_jni_vpnio *vpn = FromBuffer< lokinet_jni_vpnio >(env, buf);
if(vpn == nullptr)
return;
delete vpn;
}
JNIEXPORT void JNICALL
Java_network_loki_lokinet_LokinetVPN_Stop(JNIEnv *env, jobject self)
{
lokinet_jni_vpnio *vpn = GetImpl< lokinet_jni_vpnio >(env, self);
if(vpn)
{
vpn->Close();
}
}
JNIEXPORT jint JNICALL
Java_network_loki_lokinet_LokinetVPN_ReadPkt(JNIEnv *env, jobject self,
jobject pkt)
{
lokinet_jni_vpnio *vpn = GetImpl< lokinet_jni_vpnio >(env, self);
if(vpn == nullptr)
return -1;
void *pktbuf = env->GetDirectBufferAddress(pkt);
auto pktlen = env->GetDirectBufferCapacity(pkt);
if(pktbuf == nullptr)
return -1;
return vpn->ReadPacket(pktbuf, pktlen);
}
JNIEXPORT jboolean JNICALL
Java_network_loki_lokinet_LokinetVPN_WritePkt(JNIEnv *env, jobject self,
jobject pkt)
{
lokinet_jni_vpnio *vpn = GetImpl< lokinet_jni_vpnio >(env, self);
if(vpn == nullptr)
return false;
void *pktbuf = env->GetDirectBufferAddress(pkt);
auto pktlen = env->GetDirectBufferCapacity(pkt);
if(pktbuf == nullptr)
return false;
return vpn->WritePacket(pktbuf, pktlen);
}
JNIEXPORT void JNICALL
Java_network_loki_lokinet_LokinetVPN_SetInfo(JNIEnv *env, jobject self,
jobject info)
{
lokinet_jni_vpnio *vpn = GetImpl< lokinet_jni_vpnio >(env, self);
if(vpn == nullptr)
return;
VisitObjectMemberStringAsStringView< bool >(
env, info, "ifaddr", [vpn](llarp::string_view val) -> bool {
vpn->SetIfAddr(val);
return true;
});
VisitObjectMemberStringAsStringView< bool >(
env, info, "ifname", [vpn](llarp::string_view val) -> bool {
vpn->SetIfName(val);
return true;
});
vpn->info.netmask = GetObjectMemberAsInt< uint8_t >(env, info, "netmask");
}
}

@ -0,0 +1,38 @@
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class network_loki_lokinet_LokinetConfig */
#ifndef _Included_network_loki_lokinet_LokinetConfig
#define _Included_network_loki_lokinet_LokinetConfig
#ifdef __cplusplus
extern "C"
{
#endif
/*
* Class: network_loki_lokinet_LokinetConfig
* Method: Obtain
* Signature: ()Ljava/nio/Buffer;
*/
JNIEXPORT jobject JNICALL
Java_network_loki_lokinet_LokinetConfig_Obtain(JNIEnv *, jclass);
/*
* Class: network_loki_lokinet_LokinetConfig
* Method: Free
* Signature: (Ljava/nio/Buffer;)V
*/
JNIEXPORT void JNICALL
Java_network_loki_lokinet_LokinetConfig_Free(JNIEnv *, jclass, jobject);
/*
* Class: network_loki_lokinet_LokinetConfig
* Method: Load
* Signature: (Ljava/lang/String;)Z
*/
JNIEXPORT jboolean JNICALL
Java_network_loki_lokinet_LokinetConfig_Load(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif

@ -0,0 +1,70 @@
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class network_loki_lokinet_LokinetDaemon */
#ifndef _Included_network_loki_lokinet_LokinetDaemon
#define _Included_network_loki_lokinet_LokinetDaemon
#ifdef __cplusplus
extern "C"
{
#endif
/*
* Class: network_loki_lokinet_LokinetDaemon
* Method: Obtain
* Signature: ()Ljava/nio/Buffer;
*/
JNIEXPORT jobject JNICALL
Java_network_loki_lokinet_LokinetDaemon_Obtain(JNIEnv *, jclass);
/*
* Class: network_loki_lokinet_LokinetDaemon
* Method: Free
* Signature: (Ljava/nio/Buffer;)V
*/
JNIEXPORT void JNICALL
Java_network_loki_lokinet_LokinetDaemon_Free(JNIEnv *, jclass, jobject);
/*
* Class: network_loki_lokinet_LokinetDaemon
* Method: Configure
* Signature: (Lnetwork/loki/lokinet/LokinetConfig;)Z
*/
JNIEXPORT jboolean JNICALL
Java_network_loki_lokinet_LokinetDaemon_Configure(JNIEnv *, jobject, jobject);
/*
* Class: network_loki_lokinet_LokinetDaemon
* Method: Mainloop
* Signature: ()I
*/
JNIEXPORT jint JNICALL
Java_network_loki_lokinet_LokinetDaemon_Mainloop(JNIEnv *, jobject);
/*
* Class: network_loki_lokinet_LokinetDaemon
* Method: IsRunning
* Signature: ()Z
*/
JNIEXPORT jboolean JNICALL
Java_network_loki_lokinet_LokinetDaemon_IsRunning(JNIEnv *, jobject);
/*
* Class: network_loki_lokinet_LokinetDaemon
* Method: Stop
* Signature: ()Z
*/
JNIEXPORT jboolean JNICALL
Java_network_loki_lokinet_LokinetDaemon_Stop(JNIEnv *, jobject);
/*
* Class: network_loki_lokinet_LokinetDaemon
* Method: InjectVPN
* Signature: (Lnetwork/loki/lokinet/LokinetVPN;)Z
*/
JNIEXPORT jboolean JNICALL
Java_network_loki_lokinet_LokinetDaemon_InjectVPN(JNIEnv *, jobject, jobject);
#ifdef __cplusplus
}
#endif
#endif

@ -0,0 +1,69 @@
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class network_loki_lokinet_LokinetVPN */
#ifndef _Included_network_loki_lokinet_LokinetVPN
#define _Included_network_loki_lokinet_LokinetVPN
#ifdef __cplusplus
extern "C"
{
#endif
/*
* Class: network_loki_lokinet_LokinetVPN
* Method: PacketSize
* Signature: ()I
*/
JNIEXPORT jint JNICALL
Java_network_loki_lokinet_LokinetVPN_PacketSize(JNIEnv *, jclass);
/*
* Class: network_loki_lokinet_LokinetVPN
* Method: Alloc
* Signature: ()Ljava/nio/Buffer;
*/
JNIEXPORT jobject JNICALL
Java_network_loki_lokinet_LokinetVPN_Alloc(JNIEnv *, jclass);
/*
* Class: network_loki_lokinet_LokinetVPN
* Method: Free
* Signature: (Ljava/nio/Buffer;)V
*/
JNIEXPORT void JNICALL
Java_network_loki_lokinet_LokinetVPN_Free(JNIEnv *, jclass, jobject);
/*
* Class: network_loki_lokinet_LokinetVPN
* Method: Stop
* Signature: ()V
*/
JNIEXPORT void JNICALL
Java_network_loki_lokinet_LokinetVPN_Stop(JNIEnv *, jobject);
/*
* Class: network_loki_lokinet_LokinetVPN
* Method: ReadPkt
* Signature: (Ljava/nio/ByteBuffer;)I
*/
JNIEXPORT jint JNICALL
Java_network_loki_lokinet_LokinetVPN_ReadPkt(JNIEnv *, jobject, jobject);
/*
* Class: network_loki_lokinet_LokinetVPN
* Method: WritePkt
* Signature: (Ljava/nio/ByteBuffer;)Z
*/
JNIEXPORT jboolean JNICALL
Java_network_loki_lokinet_LokinetVPN_WritePkt(JNIEnv *, jobject, jobject);
/*
* Class: network_loki_lokinet_LokinetVPN
* Method: SetInfo
* Signature: (Lnetwork/loki/lokinet/LokinetVPN/VPNInfo;)V
*/
JNIEXPORT void JNICALL
Java_network_loki_lokinet_LokinetVPN_SetInfo(JNIEnv *, jobject, jobject);
#ifdef __cplusplus
}
#endif
#endif

@ -0,0 +1,14 @@
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class network_loki_lokinet_LokinetVPN_VPNInfo */
#ifndef _Included_network_loki_lokinet_LokinetVPN_VPNInfo
#define _Included_network_loki_lokinet_LokinetVPN_VPNInfo
#ifdef __cplusplus
extern "C"
{
#endif
#ifdef __cplusplus
}
#endif
#endif

@ -78,6 +78,7 @@ set(LIB_PLATFORM_SRC
# for networking
ev/ev.cpp
ev/pipe.cpp
ev/vpnio.cpp
net/ip.cpp
net/net.cpp
net/net_addr.cpp

@ -1,5 +1,5 @@
#include <llarp.hpp>
#include <llarp.h>
#include <constants/version.hpp>
#include <config/config.hpp>
#include <crypto/crypto_libsodium.hpp>
@ -7,8 +7,10 @@
#include <dht/context.hpp>
#include <dnsd.hpp>
#include <ev/ev.hpp>
#include <ev/vpnio.hpp>
#include <nodedb.hpp>
#include <router/router.hpp>
#include <service/context.hpp>
#include <util/logging/logger.h>
#include <util/meta/memfn.hpp>
#include <util/metrics/json_publisher.hpp>
@ -34,6 +36,12 @@ namespace llarp
m_scheduler->stop();
}
bool
Context::CallSafe(std::function< void(void) > f)
{
return logic && logic->queue_func(std::move(f));
}
void
Context::progress()
{
@ -45,11 +53,14 @@ namespace llarp
{
logic = std::make_shared< Logic >();
// llarp::LogInfo("loading config at ", configfile);
if(!config->Load(configfile.c_str()))
if(configfile.size())
{
config.release();
llarp::LogError("failed to load config file ", configfile);
return false;
if(!config->Load(configfile.c_str()))
{
config.release();
llarp::LogError("failed to load config file ", configfile);
return false;
}
}
// System config
@ -176,22 +187,6 @@ __ ___ ____ _ _ ___ _ _ ____
return 1;
}
bool
Context::PutDatabase(__attribute__((unused)) struct llarp::RouterContact &rc)
{
// FIXME
// return llarp_nodedb_put_rc(nodedb, rc);
return false;
}
llarp::RouterContact *
Context::GetDatabase(__attribute__((unused)) const byte_t *pk)
{
// FIXME
// return llarp_nodedb_get_rc(nodedb, pk);
return nullptr;
}
int
Context::Setup()
{
@ -222,7 +217,7 @@ __ ___ ____ _ _ ___ _ _ ____
}
int
Context::Run(bool backgroundMode)
Context::Run(llarp_main_runtime_opts opts)
{
if(router == nullptr)
{
@ -236,7 +231,7 @@ __ ___ ____ _ _ ___ _ _ ____
if(!router->StartJsonRpc())
return 1;
if(!backgroundMode)
if(!opts.background)
{
if(!router->Run())
return 2;
@ -245,10 +240,34 @@ __ ___ ____ _ _ ___ _ _ ____
// run net io thread
llarp::LogInfo("running mainloop");
llarp_ev_loop_run_single_process(mainloop, logic);
// waits for router graceful stop
if(closeWaiter)
{
// inform promise if called by CloseAsync
closeWaiter->set_value();
}
return 0;
}
void
Context::CloseAsync()
{
/// already closing
if(closeWaiter)
return;
if(CallSafe(std::bind(&Context::HandleSignal, this, SIGTERM)))
closeWaiter = std::make_unique< std::promise< void > >();
}
void
Context::Wait()
{
if(closeWaiter)
{
closeWaiter->get_future().wait();
closeWaiter.reset();
}
}
bool
Context::WritePIDFile() const
{
@ -377,37 +396,87 @@ __ ___ ____ _ _ ___ _ _ ____
struct llarp_main
{
llarp_main(llarp_config *conf);
~llarp_main() = default;
std::unique_ptr< llarp::Context > ctx;
};
llarp::Context *
llarp_main_get_context(llarp_main *m)
struct llarp_config
{
return m->ctx.get();
}
llarp::Config impl;
llarp_config() = default;
llarp_config(const llarp_config *other) : impl(other->impl)
{
}
};
extern "C"
{
size_t
llarp_main_size()
{
return sizeof(llarp_main);
}
size_t
llarp_config_size()
{
return sizeof(llarp_config);
}
struct llarp_config *
llarp_default_config()
{
llarp_config *conf = new llarp_config();
#ifdef ANDROID
// put andrid config overrides here
#endif
#ifdef IOS
// put IOS config overrides here
#endif
return conf;
}
void
llarp_config_free(struct llarp_config *conf)
{
if(conf)
delete conf;
}
struct llarp_main *
llarp_main_init(const char *fname, bool multiProcess)
llarp_main_init_from_config(struct llarp_config *conf)
{
(void)multiProcess;
if(!fname)
fname = "daemon.ini";
char *var = getenv("LLARP_DEBUG");
if(var && *var == '1')
{
cSetLogLevel(eLogDebug);
}
auto *m = new llarp_main;
m->ctx = std::make_unique< llarp::Context >();
if(!m->ctx->LoadConfig(fname))
{
m->ctx->Close();
delete m;
if(conf == nullptr)
return nullptr;
llarp_main *m = new llarp_main(conf);
if(m->ctx->Configure())
return m;
delete m;
return nullptr;
}
bool
llarp_config_read_file(struct llarp_config *conf, const char *fname)
{
if(conf == nullptr)
return false;
return conf->impl.Load(fname);
}
bool
llarp_config_load_file(const char *fname, struct llarp_config **conf)
{
llarp_config *c = new llarp_config();
if(c->impl.Load(fname))
{
*conf = c;
return true;
}
return m;
delete c;
*conf = nullptr;
return false;
}
void
@ -424,47 +493,97 @@ extern "C"
}
int
llarp_main_run(struct llarp_main *ptr, bool backgroundMode)
llarp_main_run(struct llarp_main *ptr, struct llarp_main_runtime_opts opts)
{
if(!ptr)
{
llarp::LogError("No ptr passed in");
return 1;
}
return ptr->ctx->Run(backgroundMode);
return ptr->ctx->Run(opts);
}
void
llarp_main_abort(struct llarp_main *ptr)
const char *
llarp_version()
{
ptr->ctx->router->logic()->stop_timer();
return LLARP_VERSION;
}
void
llarp_main_queryDHT_RC(struct llarp_main *ptr,
struct llarp_router_lookup_job *job)
ssize_t
llarp_vpn_io_readpkt(struct llarp_vpn_pkt_reader *r, unsigned char *dst,
size_t dstlen)
{
llarp_dht_lookup_router(ptr->ctx->router->dht(), job);
if(r == nullptr)
return -1;
if(not r->queue.enabled())
return -1;
auto pkt = r->queue.popFront();
ManagedBuffer mbuf = pkt.ConstBuffer();
const llarp_buffer_t &buf = mbuf;
if(buf.sz > dstlen || buf.sz == 0)
return -1;
std::copy_n(buf.base, buf.sz, dst);
return buf.sz;
}
bool
llarp_main_init_dnsd(struct llarp_main *ptr, struct dnsd_context *dnsd,
const llarp::Addr &dnsd_sockaddr,
const llarp::Addr &dnsc_sockaddr)
llarp_vpn_io_writepkt(struct llarp_vpn_pkt_writer *w, unsigned char *pktbuf,
size_t pktlen)
{
return llarp_dnsd_init(dnsd, ptr->ctx->logic.get(),
ptr->ctx->mainloop.get(), dnsd_sockaddr,
dnsc_sockaddr);
if(pktlen == 0 || pktbuf == nullptr)
return false;
if(w == nullptr)
return false;
llarp_vpn_pkt_queue::Packet_t pkt;
llarp_buffer_t buf(pktbuf, pktlen);
if(not pkt.Load(buf))
return false;
return w->queue.pushBack(std::move(pkt))
== llarp::thread::QueueReturn::Success;
}
bool
llarp_main_init_dotLokiLookup(struct llarp_main *ptr,
struct dotLokiLookup *dll)
llarp_main_inject_vpn_by_name(struct llarp_main *ptr, const char *name,
struct llarp_vpn_io *io,
struct llarp_vpn_ifaddr_info info)
{
(void)ptr;
(void)dll;
// TODO: gut me
return false;
if(name == nullptr || io == nullptr)
return false;
if(ptr == nullptr || ptr->ctx == nullptr || ptr->ctx->router == nullptr)
return false;
auto ep = ptr->ctx->router->hiddenServiceContext().GetEndpointByName(name);
return ep && ep->InjectVPN(io, info);
}
void
llarp_vpn_io_close_async(struct llarp_vpn_io *io)
{
if(io == nullptr || io->impl == nullptr)
return;
static_cast< llarp_vpn_io_impl * >(io->impl)->AsyncClose();
}
bool
llarp_vpn_io_init(struct llarp_main *ptr, struct llarp_vpn_io *io)
{
if(io == nullptr || ptr == nullptr)
return false;
llarp_vpn_io_impl *impl = new llarp_vpn_io_impl(ptr, io);
io->impl = impl;
return true;
}
struct llarp_vpn_pkt_writer *
llarp_vpn_io_packet_writer(struct llarp_vpn_io *io)
{
if(io == nullptr || io->impl == nullptr)
return nullptr;
llarp_vpn_io_impl *vpn = static_cast< llarp_vpn_io_impl * >(io->impl);
return &vpn->writer;
}
struct llarp_vpn_pkt_reader *
llarp_vpn_io_packet_reader(struct llarp_vpn_io *io)
{
if(io == nullptr || io->impl == nullptr)
return nullptr;
llarp_vpn_io_impl *vpn = static_cast< llarp_vpn_io_impl * >(io->impl);
return &vpn->reader;
}
void
@ -473,49 +592,52 @@ extern "C"
delete ptr;
}
int
llarp_main_loadDatabase(struct llarp_main *ptr)
const char *
llarp_main_get_default_endpoint_name(struct llarp_main *)
{
return ptr->ctx->LoadDatabase();
return "default";
}
const char *
handleBaseCmdLineArgs(int argc, char *argv[])
{
// clang-format off
cxxopts::Options options(
"lokinet",
"Lokinet is a private, decentralized and IP based overlay network for the internet"
);
options.add_options()
("c,config", "Config file", cxxopts::value< std::string >()->default_value("daemon.ini"))
("o,logLevel", "logging level");
// clang-format on
auto result = options.parse(argc, argv);
std::string logLevel = result["logLevel"].as< std::string >();
if(logLevel == "debug")
{
cSetLogLevel(eLogDebug);
}
else if(logLevel == "info")
{
cSetLogLevel(eLogInfo);
}
else if(logLevel == "warn")
{
cSetLogLevel(eLogWarn);
}
else if(logLevel == "error")
{
cSetLogLevel(eLogError);
}
void
llarp_main_stop(struct llarp_main *ptr)
{
if(ptr == nullptr)
return;
ptr->ctx->CloseAsync();
ptr->ctx->Wait();
}
// this isn't thread safe, but reconfiguring during run is likely unsafe
// either way
static std::string confname = result["config"].as< std::string >();
bool
llarp_main_configure(struct llarp_main *ptr, struct llarp_config *conf)
{
if(ptr == nullptr || conf == nullptr)
return false;
// give new config
ptr->ctx->config.reset(new llarp::Config(conf->impl));
return ptr->ctx->Configure();
}
return confname.c_str();
bool
llarp_main_is_running(struct llarp_main *ptr)
{
return ptr && ptr->ctx->router && ptr->ctx->router->IsRunning();
}
}
llarp_main::llarp_main(llarp_config *conf)
: ctx(new llarp::Context())
{
ctx->config.reset(new llarp::Config(conf->impl));
}
namespace llarp
{
Context *
Context::Get(llarp_main *m)
{
if(m == nullptr || m->ctx == nullptr)
return nullptr;
return m->ctx.get();
}
} // namespace llarp

@ -0,0 +1,31 @@
#include <ev/vpnio.hpp>
#include <llarp.hpp>
#include <router/abstractrouter.hpp>
#include <util/thread/logic.hpp>
void
llarp_vpn_io_impl::AsyncClose()
{
reader.queue.disable();
writer.queue.disable();
CallSafe(std::bind(&llarp_vpn_io_impl::Expunge, this));
}
void
llarp_vpn_io_impl::CallSafe(std::function< void(void) > f)
{
llarp::Context* ctx = llarp::Context::Get(ptr);
if(ctx && ctx->CallSafe(f))
return;
else if(ctx == nullptr || ctx->logic == nullptr)
f();
}
void
llarp_vpn_io_impl::Expunge()
{
parent->impl = nullptr;
if(parent->closed)
parent->closed(parent);
delete this;
}

@ -0,0 +1,51 @@
#ifndef LLARP_EV_VPNIO_HPP
#define LLARP_EV_VPNIO_HPP
#include <net/ip.hpp>
#include <util/thread/queue.hpp>
#include <functional>
struct llarp_main;
struct llarp_vpn_io;
struct llarp_vpn_pkt_queue
{
using Packet_t = llarp::net::IPPacket;
llarp::thread::Queue< Packet_t > queue;
llarp_vpn_pkt_queue() : queue(1024){};
~llarp_vpn_pkt_queue() = default;
};
struct llarp_vpn_pkt_writer : public llarp_vpn_pkt_queue
{
};
struct llarp_vpn_pkt_reader : public llarp_vpn_pkt_queue
{
};
struct llarp_vpn_io_impl
{
llarp_vpn_io_impl(llarp_main* p, llarp_vpn_io* io) : ptr(p), parent(io)
{
}
~llarp_vpn_io_impl() = default;
llarp_main* ptr;
llarp_vpn_io* parent;
llarp_vpn_pkt_writer writer;
llarp_vpn_pkt_reader reader;
void
AsyncClose();
private:
void
CallSafe(std::function< void(void) > f);
void
Expunge();
};
#endif

@ -24,10 +24,12 @@ namespace llarp
{
namespace handlers
{
static llarp_fd_promise *
get_tun_fd_promise(llarp_tun_io *tun)
void
TunEndpoint::FlushToUser(std::function< bool(net::IPPacket &) > send)
{
return static_cast< TunEndpoint * >(tun->user)->Promise.get();
m_ExitMap.ForEachValue([](const auto &exit) { exit->FlushDownstream(); });
// flush network to user
m_NetworkToUserPktQueue.Process(send);
}
static void
@ -38,7 +40,7 @@ namespace llarp
}
TunEndpoint::TunEndpoint(const std::string &nickname, AbstractRouter *r,
service::Context *parent)
service::Context *parent, bool lazyVPN)
: service::Endpoint(nickname, r, parent)
, m_UserToNetworkPktQueue(nickname + "_sendq", r->netloop(),
r->netloop())
@ -47,22 +49,19 @@ namespace llarp
, m_Resolver(std::make_shared< dns::Proxy >(
r->netloop(), r->logic(), r->netloop(), r->logic(), this))
{
std::fill(tunif.ifaddr, tunif.ifaddr + sizeof(tunif.ifaddr), 0);
std::fill(tunif.ifname, tunif.ifname + sizeof(tunif.ifname), 0);
tunif.netmask = 0;
#ifdef ANDROID
tunif.get_fd_promise = &get_tun_fd_promise;
Promise.reset(new llarp_fd_promise(&m_VPNPromise));
#else
tunif.get_fd_promise = nullptr;
#endif
tunif.user = this;
// eh this shouldn't do anything on windows anyway
tunif.tick = &tunifTick;
tunif.before_write = &tunifBeforeWrite;
tunif.recvpkt = &tunifRecvPkt;
if(not lazyVPN)
{
tunif.reset(new llarp_tun_io());
std::fill(tunif->ifaddr, tunif->ifaddr + sizeof(tunif->ifaddr), 0);
std::fill(tunif->ifname, tunif->ifname + sizeof(tunif->ifname), 0);
tunif->netmask = 0;
tunif->get_fd_promise = nullptr;
tunif->user = this;
// eh this shouldn't do anything on windows anyway
tunif->tick = &tunifTick;
tunif->before_write = &tunifBeforeWrite;
tunif->recvpkt = &tunifRecvPkt;
}
}
util::StatusObject
@ -241,18 +240,18 @@ namespace llarp
}
return MapAddress(addr, ipv6, false);
}
if(k == "ifname")
if(k == "ifname" && tunif)
{
if(v.length() >= sizeof(tunif.ifname))
if(v.length() >= sizeof(tunif->ifname))
{
llarp::LogError(Name() + " ifname '", v, "' is too long");
return false;
}
strncpy(tunif.ifname, v.c_str(), sizeof(tunif.ifname) - 1);
llarp::LogInfo(Name() + " setting ifname to ", tunif.ifname);
strncpy(tunif->ifname, v.c_str(), sizeof(tunif->ifname) - 1);
llarp::LogInfo(Name() + " setting ifname to ", tunif->ifname);
return true;
}
if(k == "ifaddr")
if(k == "ifaddr" && tunif)
{
std::string addr;
m_UseV6 = addr.find(":") != std::string::npos;
@ -268,8 +267,8 @@ namespace llarp
#endif
if(num > 0)
{
tunif.netmask = num;
addr = v.substr(0, pos);
tunif->netmask = num;
addr = v.substr(0, pos);
}
else
{
@ -280,14 +279,14 @@ namespace llarp
else
{
if(m_UseV6)
tunif.netmask = 128;
tunif->netmask = 128;
else
tunif.netmask = 32;
tunif->netmask = 32;
addr = v;
}
llarp::LogInfo(Name() + " set ifaddr to ", addr, " with netmask ",
tunif.netmask);
strncpy(tunif.ifaddr, addr.c_str(), sizeof(tunif.ifaddr) - 1);
tunif->netmask);
strncpy(tunif->ifaddr, addr.c_str(), sizeof(tunif->ifaddr) - 1);
return true;
}
return Endpoint::SetOption(k, v);
@ -585,80 +584,120 @@ namespace llarp
bool
TunEndpoint::SetupTun()
{
lazy_vpn vpn;
huint32_t ip;
auto loop = EndpointNetLoop();
if(!llarp_ev_add_tun(loop.get(), &tunif))
{
llarp::LogError(Name(),
" failed to set up tun interface: ", tunif.ifaddr,
" on ", tunif.ifname);
return false;
}
struct addrinfo hint, *res = nullptr;
int ret;
memset(&hint, 0, sizeof hint);
hint.ai_family = PF_UNSPEC;
hint.ai_flags = AI_NUMERICHOST;
ret = getaddrinfo(tunif.ifaddr, nullptr, &hint, &res);
if(ret)
{
llarp::LogError(Name(),
" failed to set up tun interface, cant determine "
"family from ",
tunif.ifaddr);
return false;
}
/*
// output is in network byte order
unsigned char buf[sizeof(struct in6_addr)];
int s = inet_pton(res->ai_family, tunif.ifaddr, buf);
if (s <= 0)
{
llarp::LogError(Name(), " failed to set up tun interface, cant parse
", tunif.ifaddr); return false;
}
*/
if(res->ai_family == AF_INET6)
if(tunif == nullptr)
{
m_UseV6 = true;
}
llarp::LogInfo(Name(), " waiting for vpn to start");
vpn = m_LazyVPNPromise.get_future().get();
vpnif = vpn.io;
if(vpnif == nullptr)
{
llarp::LogError(Name(), " failed to recieve vpn interface");
return false;
}
llarp::LogInfo(Name(), " got vpn interface");
auto self = shared_from_this();
// function to queue a packet to send to vpn interface
auto sendpkt = [self](net::IPPacket &pkt) -> bool {
// drop if no endpoint
auto impl = self->GetVPNImpl();
// drop if no vpn interface
if(impl == nullptr)
return true;
// drop if queue to vpn not enabled
if(not impl->reader.queue.enabled())
return true;
// drop if queue to vpn full
if(impl->reader.queue.full())
return true;
// queue to reader
impl->reader.queue.pushBack(pkt);
return false;
};
// event loop ticker
auto ticker = [self, sendpkt]() {
TunEndpoint *ep = self.get();
const bool running = not ep->IsStopped();
auto impl = ep->GetVPNImpl();
if(impl)
{
/// get packets from vpn
while(not impl->writer.queue.empty())
{
// queue it to be sent over lokinet
auto pkt = impl->writer.queue.popFront();
if(running)
ep->m_UserToNetworkPktQueue.Emplace(pkt);
}
}
freeaddrinfo(res);
if(m_UseV6)
{
llarp::LogInfo(Name(), " using IPV6");
// process packets queued from vpn
if(running)
{
ep->Flush();
ep->FlushToUser(sendpkt);
}
// if impl has a tick function call it
if(impl && impl->parent && impl->parent->tick)
impl->parent->tick(impl->parent);
};
if(not loop->add_ticker(ticker))
{
llarp::LogError(Name(), " failed to add vpn to event loop");
if(vpnif->injected)
vpnif->injected(vpnif, false);
return false;
}
}
else
{
struct in_addr addr; // network byte order
if(inet_aton(tunif.ifaddr, &addr) == 0)
if(!llarp_ev_add_tun(loop.get(), tunif.get()))
{
llarp::LogError(Name(),
" failed to set up tun interface, cant parse ",
tunif.ifaddr);
" failed to set up tun interface: ", tunif->ifaddr,
" on ", tunif->ifname);
return false;
}
}
huint32_t ip;
if(ip.FromString(tunif.ifaddr))
const char *ifname;
const char *ifaddr;
unsigned char netmask;
if(tunif)
{
ifname = tunif->ifname;
ifaddr = tunif->ifaddr;
netmask = tunif->netmask;
}
else
{
ifname = vpn.info.ifname;
ifaddr = vpn.info.ifaddr;
netmask = vpn.info.netmask;
}
if(ip.FromString(ifaddr))
{
m_OurIP = net::IPPacket::ExpandV4(ip);
m_OurRange.netmask_bits = netmask_ipv6_bits(tunif.netmask + 96);
m_OurRange.netmask_bits = netmask_ipv6_bits(netmask + 96);
}
else if(m_OurIP.FromString(tunif.ifaddr))
else if(m_OurIP.FromString(ifaddr))
{
m_OurRange.netmask_bits = netmask_ipv6_bits(tunif.netmask);
m_OurRange.netmask_bits = netmask_ipv6_bits(netmask);
m_UseV6 = true;
}
else
{
LogError(Name(), " invalid interface address given, ifaddr=", ifaddr);
if(vpnif && vpnif->injected)
vpnif->injected(vpnif, false);
return false;
}
m_NextIP = m_OurIP;
m_OurRange.addr = m_OurIP;
m_MaxIP = m_OurRange.HighestAddr();
llarp::LogInfo(Name(), " set ", tunif.ifname, " to have address ",
m_OurIP);
llarp::LogInfo(Name(), " set ", ifname, " to have address ", m_OurIP);
llarp::LogInfo(Name(), " allocated up to ", m_MaxIP, " on range ",
m_OurRange);
@ -667,6 +706,10 @@ namespace llarp
{
m_OnUp->NotifyAsync(NotifyParams());
}
if(vpnif && vpnif->injected)
{
vpnif->injected(vpnif, true);
}
return true;
}
@ -676,7 +719,8 @@ namespace llarp
auto env = Endpoint::NotifyParams();
env.emplace("IP_ADDR", m_OurIP.ToString());
env.emplace("IF_ADDR", m_OurRange.ToString());
env.emplace("IF_NAME", tunif.ifname);
if(tunif)
env.emplace("IF_NAME", tunif->ifname);
std::string strictConnect;
for(const auto &addr : m_StrictConnectAddrs)
strictConnect += addr.ToString() + " ";
@ -929,20 +973,17 @@ namespace llarp
TunEndpoint::tunifBeforeWrite(llarp_tun_io *tun)
{
// called in the isolated network thread
auto *self = static_cast< TunEndpoint * >(tun->user);
// flush user to network
self->EndpointLogic()->queue_func(
std::bind(&TunEndpoint::FlushSend, self));
// flush exit traffic queues if it's there
self->EndpointLogic()->queue_func([self] {
self->m_ExitMap.ForEachValue(
[](const auto &exit) { exit->FlushDownstream(); });
});
// flush network to user
self->m_NetworkToUserPktQueue.Process([tun](net::IPPacket &pkt) {
auto *self = static_cast< TunEndpoint * >(tun->user);
auto sendpkt = [self, tun](net::IPPacket &pkt) -> bool {
if(!llarp_ev_tun_async_write(tun, pkt.Buffer()))
llarp::LogWarn("packet dropped");
});
{
llarp::LogWarn(self->Name(), " packet dropped");
return true;
}
return false;
};
self->EndpointLogic()->queue_func(std::bind(
&TunEndpoint::FlushToUser, self->shared_from_this(), sendpkt));
}
void

@ -3,6 +3,7 @@
#include <dns/server.hpp>
#include <ev/ev.h>
#include <ev/vpnio.hpp>
#include <net/ip.hpp>
#include <net/net.hpp>
#include <service/endpoint.hpp>
@ -20,7 +21,7 @@ namespace llarp
public std::enable_shared_from_this< TunEndpoint >
{
TunEndpoint(const std::string& nickname, AbstractRouter* r,
llarp::service::Context* parent);
llarp::service::Context* parent, bool lazyVPN = false);
~TunEndpoint() override;
path::PathSet_ptr
@ -114,8 +115,17 @@ namespace llarp
bool
HasLocalIP(const huint128_t& ip) const;
llarp_tun_io tunif;
std::unique_ptr< llarp_fd_promise > Promise;
std::unique_ptr< llarp_tun_io > tunif;
llarp_vpn_io* vpnif = nullptr;
bool
InjectVPN(llarp_vpn_io* io, llarp_vpn_ifaddr_info info)
{
if(tunif)
return false;
m_LazyVPNPromise.set_value(lazy_vpn{info, io});
return true;
}
/// called before writing to tun interface
static void
@ -206,6 +216,14 @@ namespace llarp
m_SNodes;
private:
llarp_vpn_io_impl*
GetVPNImpl()
{
if(vpnif && vpnif->impl)
return static_cast< llarp_vpn_io_impl* >(vpnif->impl);
return nullptr;
}
bool
QueueInboundPacketForExit(const llarp_buffer_t& buf)
{
@ -255,12 +273,6 @@ namespace llarp
reply(*query);
delete query;
}
#ifndef WIN32
/// handles fd injection force android
std::promise< std::pair< int, int > > m_VPNPromise;
#endif
/// our dns resolver
std::shared_ptr< dns::Proxy > m_Resolver;
@ -283,6 +295,18 @@ namespace llarp
std::vector< llarp::Addr > m_StrictConnectAddrs;
/// use v6?
bool m_UseV6;
struct lazy_vpn
{
llarp_vpn_ifaddr_info info;
llarp_vpn_io* io;
};
std::promise< lazy_vpn > m_LazyVPNPromise;
/// send packets on endpoint to user using send function
/// send function returns true to indicate stop iteration and do codel
/// drop
void
FlushToUser(std::function< bool(net::IPPacket&) > sendfunc);
};
} // namespace handlers
} // namespace llarp

@ -90,20 +90,24 @@ namespace llarp
if(pkt.sz > sizeof(buf))
return false;
sz = pkt.sz;
memcpy(buf, pkt.base, sz);
std::copy_n(pkt.base, sz, buf);
return true;
}
llarp_buffer_t
ManagedBuffer
IPPacket::ConstBuffer() const
{
return {buf, sz};
const byte_t *ptr = buf;
llarp_buffer_t b(ptr, sz);
return ManagedBuffer(b);
}
llarp_buffer_t
ManagedBuffer
IPPacket::Buffer()
{
return {buf, sz};
byte_t *ptr = buf;
llarp_buffer_t b(ptr, sz);
return ManagedBuffer(b);
}
huint32_t

@ -116,10 +116,10 @@ namespace llarp
size_t sz;
byte_t buf[MaxSize];
llarp_buffer_t
ManagedBuffer
Buffer();
llarp_buffer_t
ManagedBuffer
ConstBuffer() const;
bool

@ -132,6 +132,9 @@ namespace llarp
virtual bool
Run() = 0;
virtual bool
IsRunning() const = 0;
/// stop running the router logic gracefully
virtual void
Stop() = 0;

@ -1040,6 +1040,12 @@ namespace llarp
return _running;
}
bool
Router::IsRunning() const
{
return _running;
}
llarp_time_t
Router::Uptime() const
{

@ -310,6 +310,9 @@ namespace llarp
bool
InitServiceNode();
bool
IsRunning() const override;
/// return true if we are running in service node mode
bool
IsServiceNode() const;

@ -20,23 +20,18 @@ namespace llarp
{"tun",
[](const std::string &nick, AbstractRouter *r,
service::Context *c) -> service::Endpoint_ptr {
return std::make_shared< handlers::TunEndpoint >(nick, r, c);
return std::make_shared< handlers::TunEndpoint >(nick, r, c,
false);
}},
{"android-tun",
[](const std::string &, AbstractRouter *,
service::Context *) -> service::Endpoint_ptr {
return nullptr;
/// SOOOOOOON (tm)
// return std::make_shared<handlers::AndroidTunEndpoint>(nick,
// r, c);
{"android",
[](const std::string &nick, AbstractRouter *r,
service::Context *c) -> service::Endpoint_ptr {
return std::make_shared< handlers::TunEndpoint >(nick, r, c, true);
}},
{"ios-tun",
[](const std::string &, AbstractRouter *,
service::Context *) -> service::Endpoint_ptr {
return nullptr;
/// SOOOOOOON (tm)
// return std::make_shared<handlers::IOSTunEndpoint>(nick, r,
// c);
{"ios",
[](const std::string &nick, AbstractRouter *r,
service::Context *c) -> service::Endpoint_ptr {
return std::make_shared< handlers::TunEndpoint >(nick, r, c, true);
}},
{"null",
[](const std::string &nick, AbstractRouter *r,
@ -133,12 +128,22 @@ namespace llarp
return m_Endpoints.size() ? true : false;
}
static const char *
DefaultEndpointType()
{
#ifdef ANDROID
return "android";
#else
return "tun";
#endif
}
bool
Context::AddDefaultEndpoint(
const std::unordered_multimap< std::string, std::string > &opts)
{
Config::section_values_t configOpts;
configOpts.push_back({"type", "tun"});
configOpts.push_back({"type", DefaultEndpointType()});
{
auto itr = opts.begin();
while(itr != opts.end())
@ -167,6 +172,15 @@ namespace llarp
return true;
}
Endpoint_ptr
Context::GetEndpointByName(const std::string &name)
{
auto itr = m_Endpoints.find(name);
if(itr != m_Endpoints.end())
return itr->second;
return nullptr;
}
bool
Context::AddEndpoint(const Config::section_t &conf, bool autostart)
{
@ -180,7 +194,7 @@ namespace llarp
}
}
// extract type
std::string endpointType = "tun";
std::string endpointType = DefaultEndpointType();
std::string keyfile;
for(const auto &option : conf.second)
{

@ -51,6 +51,9 @@ namespace llarp
bool
RemoveEndpoint(const std::string &name);
Endpoint_ptr
GetEndpointByName(const std::string &name);
bool
StartAll();

@ -1,6 +1,6 @@
#ifndef LLARP_SERVICE_ENDPOINT_HPP
#define LLARP_SERVICE_ENDPOINT_HPP
#include <llarp.h>
#include <dht/messages/gotrouter.hpp>
#include <ev/ev.h>
#include <exit/session.hpp>
@ -105,6 +105,14 @@ namespace llarp
return false;
}
/// inject vpn io
/// return false if not supported
virtual bool
InjectVPN(llarp_vpn_io*, llarp_vpn_ifaddr_info)
{
return false;
}
/// get our ifaddr if it is set
virtual huint128_t
GetIfAddr() const
@ -254,9 +262,6 @@ namespace llarp
bool
ShouldBundleRC() const override;
static void
HandlePathDead(void*);
/// return true if we have a convotag as an exit session
/// or as a hidden service session
/// set addr and issnode

Loading…
Cancel
Save