Initial: initial commit

This commit is contained in:
kr328
2021-05-15 00:51:08 +08:00
commit 07e8afa69a
483 changed files with 26328 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.kr328.clash.core">
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -0,0 +1,23 @@
cmake_minimum_required(VERSION 3.0)
project(clash-bridge C)
set(GO_OUTPUT_BASE ${GO_OUTPUT}/${FLAVOR_NAME})
if ("${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
set(GO_OUTPUT_BASE "${GO_OUTPUT_BASE}Debug")
elseif ("${CMAKE_BUILD_TYPE}" STREQUAL "Release")
set(GO_OUTPUT_BASE "${GO_OUTPUT_BASE}Release")
elseif ("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo")
set(GO_OUTPUT_BASE "${GO_OUTPUT_BASE}Release")
else ()
message(FATAL_ERROR "Unknown build type ${CMAKE_BUILD_TYPE}")
endif ()
include_directories("${GO_OUTPUT_BASE}/${CMAKE_ANDROID_ARCH_ABI}")
include_directories("${GO_SOURCE}")
link_directories("${GO_OUTPUT_BASE}/${CMAKE_ANDROID_ARCH_ABI}")
add_library(bridge SHARED main.c jni_helper.c bridge_helper.c)
target_link_libraries(bridge log clash)

View File

@@ -0,0 +1,12 @@
#include "bridge_helper.h"
uint64_t down_scale_traffic(uint64_t value) {
if (value > 1042 * 1024 * 1024)
return ((value * 100u / 1024u / 1024u / 1024u) & 0x3FFFFFFFu) | (3u << 30u);
if (value > 1024 * 1024)
return ((value * 100u / 1024u / 1024u) & 0x3FFFFFFFu) | (2u << 30u);
if (value > 1024)
return ((value * 100u / 1024u) & 0x3FFFFFFFu) | (1u << 30u);
return value & 0x3FFFFFFFu;
}

View File

@@ -0,0 +1,5 @@
#pragma once
#include <stdint.h>
uint64_t down_scale_traffic(uint64_t value);

View File

@@ -0,0 +1,75 @@
#include "jni_helper.h"
#include <malloc.h>
#include <string.h>
static JavaVM *global_vm;
static jclass c_string;
static jmethodID m_new_string;
static jmethodID m_get_bytes;
void initialize_jni(JavaVM *vm, JNIEnv *env) {
global_vm = vm;
c_string = (jclass) new_global(find_class("java/lang/String"));
m_new_string = find_method(c_string, "<init>", "([B)V");
m_get_bytes = find_method(c_string, "getBytes", "()[B");
}
JavaVM *global_java_vm() {
return global_vm;
}
char *jni_get_string(JNIEnv *env, jstring str) {
jbyteArray array = (*env)->CallObjectMethod(env, str, m_get_bytes);
int length = (*env)->GetArrayLength(env, array);
char *content = (char *) malloc(length + 1);
(*env)->GetByteArrayRegion(env, array, 0, length, (jbyte *) content);
content[length] = 0;
return content;
}
jstring jni_new_string(JNIEnv *env, const char *str) {
int length = strlen(str);
jbyteArray array = (*env)->NewByteArray(env, length);
(*env)->SetByteArrayRegion(env, array, 0, length, (const jbyte *) str);
return (jstring) (*env)->NewObject(env, c_string, m_new_string, array);
}
int jni_catch_exception(JNIEnv *env) {
int result = (*env)->ExceptionCheck(env);
if (result) {
(*env)->ExceptionDescribe(env);
(*env)->ExceptionClear(env);
}
return result;
}
void jni_attach_thread(JNIEnv **penv) {
JavaVM *vm = global_java_vm();
if ((*vm)->AttachCurrentThread(vm, penv, NULL) != JNI_OK) {
abort();
}
}
void jni_detach_thread(JNIEnv **env) {
(void) env;
JavaVM *vm = global_java_vm();
(*vm)->DetachCurrentThread(vm);
}
void release_string(char **str) {
free(*str);
}

View File

@@ -0,0 +1,32 @@
#pragma once
#include <jni.h>
#include <stdint.h>
#include <stdlib.h>
#include <malloc.h>
#include <android/log.h>
extern void initialize_jni(JavaVM *vm, JNIEnv *env);
extern jstring jni_new_string(JNIEnv *env, const char *str);
extern char *jni_get_string(JNIEnv *env, jstring str);
extern int jni_catch_exception(JNIEnv *env);
extern void jni_attach_thread(JNIEnv **penv);
extern void jni_detach_thread(JNIEnv **env);
extern void release_string(char **str);
#define ATTACH_JNI() __attribute__((unused, cleanup(jni_detach_thread))) JNIEnv *env = NULL; jni_attach_thread(&env)
#define scoped_string __attribute__((cleanup(release_string))) char*
#define find_class(name) (*env)->FindClass(env, name)
#define find_method(cls, name, signature) (*env)->GetMethodID(env, cls, name, signature)
#define new_global(obj) (*env)->NewGlobalRef(env, obj)
#define del_global(obj) (*env)->DeleteGlobalRef(env, obj)
#define get_string(jstr) jni_get_string(env, jstr)
#define new_string(cstr) jni_new_string(env, cstr)

528
core/src/main/cpp/main.c Normal file
View File

@@ -0,0 +1,528 @@
#include <jni.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "bridge_helper.h"
#include "libclash.h"
#include "jni_helper.h"
#include "trace.h"
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeInit(JNIEnv *env, jobject thiz,
jstring home,
jstring version_name, jint sdk_version) {
TRACE_METHOD();
scoped_string _home = get_string(home);
scoped_string _version_name = get_string(version_name);
coreInit(_home, _version_name, sdk_version);
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeReset(JNIEnv *env, jobject thiz) {
TRACE_METHOD();
reset();
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeForceGc(JNIEnv *env, jobject thiz) {
TRACE_METHOD();
forceGc();
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeSuspend(JNIEnv *env, jobject thiz,
jboolean suspended) {
TRACE_METHOD();
suspend((int) suspended);
}
JNIEXPORT jstring JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeQueryTunnelState(JNIEnv *env, jobject thiz) {
TRACE_METHOD();
scoped_string response = queryTunnelState();
return new_string(response);
}
JNIEXPORT jlong JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeQueryTrafficNow(JNIEnv *env, jobject thiz) {
TRACE_METHOD();
uint64_t upload = 0l, download = 0l;
queryNow(&upload, &download);
return (jlong) (down_scale_traffic(upload) << 32u | down_scale_traffic(download));
}
JNIEXPORT jlong JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeQueryTrafficTotal(JNIEnv *env, jobject thiz) {
TRACE_METHOD();
uint64_t upload = 0l, download = 0l;
queryTotal(&upload, &download);
return (jlong) (down_scale_traffic(upload) << 32u | down_scale_traffic(download));
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeNotifyDnsChanged(JNIEnv *env, jobject thiz,
jstring dns_list) {
TRACE_METHOD();
scoped_string _dns_list = get_string(dns_list);
notifyDnsChanged(_dns_list);
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeNotifyInstalledAppChanged(JNIEnv *env,
jobject thiz,
jstring uid_list) {
TRACE_METHOD();
scoped_string _uid_list = get_string(uid_list);
notifyInstalledAppsChanged(_uid_list);
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeStartTun(JNIEnv *env, jobject thiz, jint fd,
jint mtu, jstring gateway,
jstring mirror, jstring dns,
jobject cb) {
TRACE_METHOD();
scoped_string _gateway = get_string(gateway);
scoped_string _mirror = get_string(mirror);
scoped_string _dns = get_string(dns);
jobject _interface = new_global(cb);
startTun(fd, mtu, _gateway, _mirror, _dns, _interface);
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeStopTun(JNIEnv *env, jobject thiz) {
TRACE_METHOD();
stopTun();
}
JNIEXPORT jstring JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeStartHttp(JNIEnv *env, jobject thiz,
jstring listen_at) {
TRACE_METHOD();
scoped_string _listen_at = get_string(listen_at);
scoped_string listened = startHttp(_listen_at);
if (listened == NULL)
return NULL;
return new_string(listened);
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeStopHttp(JNIEnv *env, jobject thiz) {
TRACE_METHOD();
stopHttp();
}
JNIEXPORT jstring JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeQueryGroupNames(JNIEnv *env, jobject thiz,
jboolean exclude_not_selectable) {
TRACE_METHOD();
scoped_string response = queryGroupNames((int) exclude_not_selectable);
return new_string(response);
}
JNIEXPORT jstring JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeQueryGroup(JNIEnv *env, jobject thiz,
jstring name, jstring mode) {
TRACE_METHOD();
scoped_string _name = get_string(name);
scoped_string _mode = get_string(mode);
scoped_string response = queryGroup(_name, _mode);
if (response == NULL)
return NULL;
return new_string(response);
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeHealthCheck(JNIEnv *env, jobject thiz,
jobject completable,
jstring name) {
TRACE_METHOD();
jobject _completable = new_global(completable);
scoped_string _name = get_string(name);
healthCheck(_completable, _name);
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeHealthCheckAll(JNIEnv *env, jobject thiz) {
TRACE_METHOD();
healthCheckAll();
}
JNIEXPORT jboolean JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativePatchSelector(JNIEnv *env, jobject thiz,
jstring selector, jstring name) {
TRACE_METHOD();
scoped_string _selector = get_string(selector);
scoped_string _name = get_string(name);
return (jboolean) patchSelector(_selector, _name);
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeLoad(JNIEnv *env, jobject thiz,
jobject completable, jstring path) {
TRACE_METHOD();
jobject _completable = new_global(completable);
scoped_string _path = get_string(path);
load(_completable, _path);
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeFetchAndValid(JNIEnv *env, jobject thiz,
jobject callback,
jstring path,
jstring url, jboolean force) {
TRACE_METHOD();
jobject _completable = new_global(callback);
scoped_string _path = get_string(path);
scoped_string _url = get_string(url);
fetchAndValid(_completable, _path, _url, force);
}
JNIEXPORT jstring JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeQueryProviders(JNIEnv *env, jobject thiz) {
TRACE_METHOD();
scoped_string response = queryProviders();
return new_string(response);
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeUpdateProvider(JNIEnv *env, jobject thiz,
jobject completable,
jstring type,
jstring name) {
TRACE_METHOD();
jobject _completable = new_global(completable);
scoped_string _type = get_string(type);
scoped_string _name = get_string(name);
updateProvider(_completable, _type, _name);
}
JNIEXPORT jstring JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeReadOverride(JNIEnv *env, jobject thiz,
jint slot) {
TRACE_METHOD();
scoped_string response = readOverride(slot);
return new_string(response);
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeWriteOverride(JNIEnv *env, jobject thiz,
jint slot,
jstring content) {
TRACE_METHOD();
scoped_string _content = get_string(content);
writeOverride(slot, _content);
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeClearOverride(JNIEnv *env, jobject thiz,
jint slot) {
TRACE_METHOD();
clearOverride(slot);
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeInstallSideloadGeoip(JNIEnv *env, jobject thiz,
jbyteArray data) {
TRACE_METHOD();
if (data == NULL) {
installSideloadGeoip(NULL, 0);
return;
}
jbyte *bytes = (*env)->GetByteArrayElements(env, data, NULL);
int size = (*env)->GetArrayLength(env, data);
scoped_string err = installSideloadGeoip(bytes, size);
(*env)->ReleaseByteArrayElements(env, data, bytes, JNI_ABORT);
if (err != NULL) {
(*env)->ThrowNew(
env,
find_class("com/github/kr328/clash/core/bridge/ClashException"),
err
);
}
}
JNIEXPORT jstring JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeQueryConfiguration(JNIEnv *env, jobject thiz) {
TRACE_METHOD();
scoped_string response = queryConfiguration();
return new_string(response);
}
JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeSubscribeLogcat(JNIEnv *env, jobject thiz,
jobject callback) {
TRACE_METHOD();
jobject _callback = new_global(callback);
subscribeLogcat(_callback);
}
static jmethodID m_tun_interface_mark_socket;
static jmethodID m_tun_interface_query_socket_uid;
static jmethodID m_completable_complete;
static jmethodID m_completable_complete_exceptionally;
static jmethodID m_logcat_interface_received;
static jmethodID m_clash_exception;
static jmethodID m_fetch_callback_report;
static jmethodID m_fetch_callback_complete;
static jmethodID m_open;
static jmethodID m_get_message;
static jclass c_clash_exception;
static jclass c_content;
static jobject o_unit;
static void call_tun_interface_mark_socket_impl(void *tun_interface, int fd) {
TRACE_METHOD();
ATTACH_JNI();
(*env)->CallVoidMethod(env, (jobject) tun_interface,
(jmethodID) m_tun_interface_mark_socket,
(jint) fd);
}
static int call_tun_interface_query_socket_uid_impl(void *tun_interface, int protocol,
const char *source, const char *target) {
TRACE_METHOD();
ATTACH_JNI();
return (*env)->CallIntMethod(env, (jobject) tun_interface,
(jmethodID) m_tun_interface_query_socket_uid,
(jint) protocol,
(jstring) new_string(source),
(jstring) new_string(target));
}
static void call_completable_complete_impl(void *completable, const char *exception) {
TRACE_METHOD();
ATTACH_JNI();
if (exception == NULL) {
(*env)->CallBooleanMethod(env,
(jobject) completable,
(jmethodID) m_completable_complete,
(jobject) o_unit);
} else {
jthrowable _exception = (jthrowable)
(*env)->NewObject(env,
(jclass) c_clash_exception,
(jmethodID) m_clash_exception,
(jstring) new_string(exception)
);
(*env)->CallBooleanMethod(env,
(jobject) completable,
(jmethodID) m_completable_complete_exceptionally,
(jobject) _exception);
}
}
static void call_fetch_callback_report_impl(void *fetch_callback, const char *status_json) {
TRACE_METHOD();
ATTACH_JNI();
jstring _status_json = new_string(status_json);
(*env)->CallVoidMethod(env,
(jobject) fetch_callback,
(jmethodID) m_fetch_callback_report,
(jstring) _status_json);
}
static void call_fetch_callback_complete_impl(void *fetch_callback, const char *error) {
TRACE_METHOD();
ATTACH_JNI();
jstring _error = NULL;
if (error != NULL)
_error = new_string(error);
(*env)->CallVoidMethod(env,
(jobject) fetch_callback,
(jmethodID) m_fetch_callback_complete,
(jstring) _error);
}
static int call_logcat_interface_received_impl(void *callback, const char *payload) {
TRACE_METHOD();
ATTACH_JNI();
(*env)->CallVoidMethod(env,
(jobject) callback,
(jmethodID) m_logcat_interface_received,
(jstring) new_string(payload));
if (jni_catch_exception(env)) {
return 1;
}
return 0;
}
static int open_content_impl(const char *url, char *error, int error_length) {
TRACE_METHOD();
ATTACH_JNI();
int fd = (*env)->CallStaticIntMethod(env, c_content, m_open, new_string(url));
if ((*env)->ExceptionCheck(env)) {
jthrowable exception = (*env)->ExceptionOccurred(env);
(*env)->ExceptionClear(env);
jstring message = (jstring) (*env)->CallObjectMethod(
env,
(jthrowable) exception,
(jmethodID) m_get_message
);
if (message == NULL) {
strncpy(error, "unknown", error_length - 1);
} else {
scoped_string _message = get_string(message);
strncpy(error, _message, error_length - 1);
}
return -1;
}
return fd;
}
static void release_jni_object_impl(void *obj) {
TRACE_METHOD();
ATTACH_JNI();
del_global((jobject) obj);
}
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
TRACE_METHOD();
JNIEnv *env = NULL;
if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK)
return JNI_ERR;
initialize_jni(vm, env);
jclass c_tun_interface = find_class("com/github/kr328/clash/core/bridge/TunInterface");
jclass c_completable = find_class("kotlinx/coroutines/CompletableDeferred");
jclass c_fetch_callback = find_class("com/github/kr328/clash/core/bridge/FetchCallback");
jclass c_logcat_interface = find_class("com/github/kr328/clash/core/bridge/LogcatInterface");
jclass _c_clash_exception = find_class("com/github/kr328/clash/core/bridge/ClashException");
jclass _c_content = find_class("com/github/kr328/clash/core/bridge/Content");
jclass c_throwable = find_class("java/lang/Throwable");
jclass c_unit = find_class("kotlin/Unit");
m_tun_interface_mark_socket = find_method(c_tun_interface, "markSocket",
"(I)V");
m_tun_interface_query_socket_uid = find_method(c_tun_interface, "querySocketUid",
"(ILjava/lang/String;Ljava/lang/String;)I");
m_completable_complete = find_method(c_completable, "complete",
"(Ljava/lang/Object;)Z");
m_fetch_callback_report = find_method(c_fetch_callback, "report",
"(Ljava/lang/String;)V");
m_fetch_callback_complete = find_method(c_fetch_callback, "complete",
"(Ljava/lang/String;)V");
m_completable_complete_exceptionally = find_method(c_completable, "completeExceptionally",
"(Ljava/lang/Throwable;)Z");
m_logcat_interface_received = find_method(c_logcat_interface, "received",
"(Ljava/lang/String;)V");
m_clash_exception = find_method(_c_clash_exception, "<init>",
"(Ljava/lang/String;)V");
m_get_message = find_method(c_throwable, "getMessage",
"()Ljava/lang/String;");
m_open = (*env)->GetStaticMethodID(env, _c_content, "open",
"(Ljava/lang/String;)I");
o_unit = (*env)->GetStaticObjectField(env, c_unit,
(*env)->GetStaticFieldID(env, c_unit, "INSTANCE",
"Lkotlin/Unit;"));
c_clash_exception = (jclass) new_global(_c_clash_exception);
c_content = (jclass) new_global(_c_content);
o_unit = new_global(o_unit);
mark_socket_func = &call_tun_interface_mark_socket_impl;
query_socket_uid_func = &call_tun_interface_query_socket_uid_impl;
complete_func = &call_completable_complete_impl;
fetch_report_func = &call_fetch_callback_report_impl;
fetch_complete_func = &call_fetch_callback_complete_impl;
logcat_received_func = &call_logcat_interface_received_impl;
open_content_func = &open_content_impl;
release_object_func = &release_jni_object_impl;
return JNI_VERSION_1_6;
}

View File

@@ -0,0 +1,11 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<GoCodeStyleSettings>
<option name="IMPORT_SORTING" value="GOIMPORTS" />
<option name="MOVE_ALL_IMPORTS_IN_ONE_DECLARATION" value="true" />
<option name="MOVE_ALL_STDLIB_IMPORTS_IN_ONE_GROUP" value="true" />
<option name="GROUP_STDLIB_IMPORTS" value="true" />
<option name="GROUP_CURRENT_PROJECT_IMPORTS" value="true" />
</GoCodeStyleSettings>
</code_scheme>
</component>

View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

View File

@@ -0,0 +1,55 @@
package main
//#include "bridge.h"
import "C"
import (
"errors"
"unsafe"
"cfa/app"
"github.com/Dreamacro/clash/log"
)
func openRemoteContent(url string) (int, error) {
u := C.CString(url)
e := (*C.char)(C.malloc(1024))
log.Debugln("Open remote url: %s", url)
defer C.free(unsafe.Pointer(e))
fd := C.open_content(u, e, 1024)
if fd < 0 {
return -1, errors.New(C.GoString(e))
}
return int(fd), nil
}
//export notifyDnsChanged
func notifyDnsChanged(dnsList C.c_string) {
d := C.GoString(dnsList)
app.NotifyDnsChanged(d)
}
//export notifyInstalledAppsChanged
func notifyInstalledAppsChanged(uids C.c_string) {
u := C.GoString(uids)
app.NotifyInstallAppsChanged(u)
}
//export queryConfiguration
func queryConfiguration() *C.char {
response := &struct{}{}
return marshalJson(&response)
}
func init() {
app.ApplyContentContext(openRemoteContent)
}

View File

@@ -0,0 +1,48 @@
package app
import (
"strconv"
"strings"
)
var appVersionName string
var platformVersion int
var installedAppsUid = map[int]string{}
func ApplyVersionName(versionName string) {
appVersionName = versionName
}
func ApplyPlatformVersion(version int) {
platformVersion = version
}
func VersionName() string {
return appVersionName
}
func PlatformVersion() int {
return platformVersion
}
func NotifyInstallAppsChanged(uidList string) {
uids := map[int]string{}
for _, item := range strings.Split(uidList, ",") {
kv := strings.Split(item, ":")
if len(kv) == 2 {
uid, err := strconv.Atoi(kv[0])
if err != nil {
continue
}
uids[uid] = kv[1]
}
}
installedAppsUid = uids
}
func QueryAppByUid(uid int) string {
return installedAppsUid[uid]
}

View File

@@ -0,0 +1,27 @@
package app
import (
"errors"
"os"
"syscall"
)
var openContentImpl = func(url string) (int, error) {
return -1, errors.New("not implement")
}
func OpenContent(url string) (*os.File, error) {
fd, err := openContentImpl(url)
if err != nil {
return nil, err
}
_ = syscall.SetNonblock(fd, true)
return os.NewFile(uintptr(fd), "fd"), nil
}
func ApplyContentContext(openContent func(string) (int, error)) {
openContentImpl = openContent
}

View File

@@ -0,0 +1,15 @@
package app
import "strings"
var systemDns []string
func NotifyDnsChanged(dnsList string) {
dns := strings.Split(dnsList, ",")
systemDns = dns
}
func SystemDns() []string {
return systemDns
}

View File

@@ -0,0 +1,46 @@
package app
import (
"cfa/platform"
"net"
"strings"
"syscall"
)
var markSocketImpl func(fd int)
var querySocketUidImpl func(protocol int, source, target string) int
func MarkSocket(fd int) {
markSocketImpl(fd)
}
func QuerySocketUid(source, target net.Addr) int {
protocol := syscall.IPPROTO_TCP
if strings.HasPrefix(source.String(), "udp") {
protocol = syscall.IPPROTO_UDP
}
if PlatformVersion() < 29 {
return platform.QuerySocketUidFromProcFs(source, target)
}
return querySocketUidImpl(protocol, source.String(), target.String())
}
func ApplyTunContext(markSocket func(fd int), querySocketUid func(int, string, string) int) {
if markSocket == nil {
markSocket = func(fd int) {}
}
if querySocketUid == nil {
querySocketUid = func(int, string, string) int { return -1 }
}
markSocketImpl = markSocket
querySocketUidImpl = querySocketUid
}
func init() {
ApplyTunContext(nil, nil)
}

View File

@@ -0,0 +1,34 @@
package app
import (
"github.com/dlclark/regexp2"
"github.com/Dreamacro/clash/log"
)
var uiSubtitlePattern *regexp2.Regexp
func ApplySubtitlePattern(pattern string) {
if pattern == "" {
uiSubtitlePattern = nil
return
}
if o := uiSubtitlePattern; o != nil && o.String() == pattern {
return
}
reg, err := regexp2.Compile(pattern, regexp2.IgnoreCase|regexp2.Compiled)
if err == nil {
uiSubtitlePattern = reg
} else {
uiSubtitlePattern = nil
log.Warnln("Compile ui-subtitle-pattern: %s", err.Error())
}
}
func SubtitlePattern() *regexp2.Regexp {
return uiSubtitlePattern
}

View File

@@ -0,0 +1,115 @@
#include "bridge.h"
#include "trace.h"
void (*mark_socket_func)(void *tun_interface, int fd);
int (*query_socket_uid_func)(void *tun_interface, int protocol, const char *source, const char *target);
void (*complete_func)(void *completable, const char *exception);
void (*fetch_report_func)(void *fetch_callback, const char *status_json);
void (*fetch_complete_func)(void *fetch_callback, const char *error);
int (*logcat_received_func)(void *logcat_interface, const char *payload);
int (*open_content_func)(const char *url, char *error, int error_length);
void (*release_object_func)(void *obj);
void mark_socket(void *interface, int fd) {
TRACE_METHOD();
mark_socket_func(interface, fd);
}
int query_socket_uid(void *interface, int protocol, char *source, char *target) {
TRACE_METHOD();
int result = query_socket_uid_func(interface, protocol, source, target);
free(source);
free(target);
return result;
}
void complete(void *obj, char *error) {
TRACE_METHOD();
complete_func(obj, error);
free(error);
}
void fetch_complete(void *fetch_callback, char *exception) {
TRACE_METHOD();
fetch_complete_func(fetch_callback, exception);
free(exception);
}
void fetch_report(void *fetch_callback, char *json_status) {
TRACE_METHOD();
fetch_report_func(fetch_callback, json_status);
free(json_status);
}
int logcat_received(void *logcat_interface, char *payload) {
TRACE_METHOD();
int result = logcat_received_func(logcat_interface, payload);
free(payload);
return result;
}
int open_content(char *url, char *error, int error_length) {
TRACE_METHOD();
int result = open_content_func(url, error, error_length);
free(url);
return result;
}
void release_object(void *obj) {
TRACE_METHOD();
release_object_func(obj);
}
void log_info(char *msg) {
__android_log_write(ANDROID_LOG_INFO, TAG, msg);
free(msg);
}
void log_error(char *msg) {
__android_log_write(ANDROID_LOG_ERROR, TAG, msg);
free(msg);
}
void log_warn(char *msg) {
__android_log_write(ANDROID_LOG_WARN, TAG, msg);
free(msg);
}
void log_debug(char *msg) {
__android_log_write(ANDROID_LOG_DEBUG, TAG, msg);
free(msg);
}
void log_verbose(char *msg) {
__android_log_write(ANDROID_LOG_VERBOSE, TAG, msg);
free(msg);
}

View File

@@ -0,0 +1,53 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <malloc.h>
#include <android/log.h>
#define TAG "ClashForAndroid"
typedef const char *c_string;
extern void (*mark_socket_func)(void *tun_interface, int fd);
extern int (*query_socket_uid_func)(void *tun_interface, int protocol, const char *source, const char *target);
extern void (*complete_func)(void *completable, const char *exception);
extern void (*fetch_report_func)(void *fetch_callback, const char *status_json);
extern void (*fetch_complete_func)(void *fetch_callback, const char *error);
extern int (*logcat_received_func)(void *logcat_interface, const char *payload);
extern void (*release_object_func)(void *obj);
extern int (*open_content_func)(const char *url, char *error, int error_length);
// cgo
extern void mark_socket(void *interface, int fd);
extern int query_socket_uid(void *interface, int protocol, char *source, char *target);
extern void complete(void *obj, char *error);
extern void fetch_complete(void *completable, char *exception);
extern void fetch_report(void *fetch_callback, char *status_json);
extern int logcat_received(void *logcat_interface, char *payload);
extern void release_object(void *obj);
extern int open_content(char *url, char *error, int error_length);
extern void log_info(char *msg);
extern void log_error(char *msg);
extern void log_warn(char *msg);
extern void log_debug(char *msg);
extern void log_verbose(char *msg);

View File

@@ -0,0 +1,23 @@
package common
import "strings"
func ResolveAsRoot(path string) string {
directories := strings.Split(path, "/")
result := make([]string, 0, len(directories))
for _, directory := range directories {
switch directory {
case "", ".":
continue
case "..":
if len(result) > 0 {
result = result[:len(result)-1]
}
default:
result = append(result, directory)
}
}
return strings.Join(result, "/")
}

View File

@@ -0,0 +1,62 @@
package main
//#include "bridge.h"
import "C"
import (
"runtime"
"unsafe"
"cfa/config"
)
type remoteValidCallback struct {
callback unsafe.Pointer
}
func (r *remoteValidCallback) reportStatus(json string) {
C.fetch_report(r.callback, marshalString(json))
}
//export fetchAndValid
func fetchAndValid(callback unsafe.Pointer, path, url C.c_string, force C.int) {
go func(path, url string, callback unsafe.Pointer) {
cb := &remoteValidCallback{callback: callback}
err := config.FetchAndValid(path, url, force != 0, cb.reportStatus)
C.fetch_complete(callback, marshalString(err))
C.release_object(callback)
runtime.GC()
}(C.GoString(path), C.GoString(url), callback)
}
//export load
func load(completable unsafe.Pointer, path C.c_string) {
go func(path string) {
C.complete(completable, marshalString(config.Load(path)))
C.release_object(completable)
runtime.GC()
}(C.GoString(path))
}
//export readOverride
func readOverride(slot C.int) *C.char {
return C.CString(config.ReadOverride(config.OverrideSlot(slot)))
}
//export writeOverride
func writeOverride(slot C.int, content C.c_string) {
c := C.GoString(content)
config.WriteOverride(config.OverrideSlot(slot), c)
}
//export clearOverride
func clearOverride(slot C.int) {
config.ClearOverride(config.OverrideSlot(slot))
}

View File

@@ -0,0 +1,27 @@
package config
var (
defaultNameServers = []string{
"223.5.5.5",
"119.29.29.29",
"8.8.8.8",
"1.1.1.1",
}
defaultFakeIPFilter = []string{
// stun services
"+.stun.*.*",
"+.stun.*.*.*",
"+.stun.*.*.*.*",
// Google Voices
"lens.l.google.com",
"stun.l.google.com",
// Nintendo Switch
"*.n.n.srv.nintendo.net",
}
localNetwork = []string{
"0.0.0.0/32",
"127.0.0.0/8",
}
)

View File

@@ -0,0 +1,182 @@
package config
import (
"encoding/json"
"fmt"
"io"
"net/http"
U "net/url"
"os"
P "path"
"runtime"
"time"
"cfa/app"
"github.com/Dreamacro/clash/component/dialer"
)
type Status struct {
Action string `json:"action"`
Args []string `json:"args"`
Progress int `json:"progress"`
MaxProgress int `json:"max"`
}
var client = &http.Client{
Transport: &http.Transport{
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
DialContext: dialer.DefaultTunnelDialer,
},
}
func openUrl(url string) (io.ReadCloser, error) {
request, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
request.Header.Set("User-Agent", "ClashForAndroid/"+app.VersionName())
response, err := client.Do(request)
if err != nil {
return nil, err
}
return response.Body, nil
}
func openContent(url string) (io.ReadCloser, error) {
return app.OpenContent(url)
}
func fetch(url *U.URL, file string) error {
var reader io.ReadCloser
var err error
switch url.Scheme {
case "http", "https":
reader, err = openUrl(url.String())
case "content":
reader, err = openContent(url.String())
default:
err = fmt.Errorf("unsupported scheme %s of %s", url.Scheme, url)
}
if err != nil {
return err
}
defer reader.Close()
_ = os.MkdirAll(P.Dir(file), 0700)
f, err := os.OpenFile(file, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, reader)
if err != nil {
_ = os.Remove(file)
}
return err
}
func FetchAndValid(
path string,
url string,
force bool,
reportStatus func(string),
) error {
configPath := P.Join(path, "config.yaml")
if _, err := os.Stat(configPath); os.IsNotExist(err) || force {
url, err := U.Parse(url)
if err != nil {
return err
}
bytes, _ := json.Marshal(&Status{
Action: "FetchConfiguration",
Args: []string{url.Host},
Progress: -1,
MaxProgress: -1,
})
reportStatus(string(bytes))
if err := fetch(url, configPath); err != nil {
return err
}
}
defer runtime.GC()
rawCfg, err := UnmarshalAndPatch(path)
if err != nil {
return err
}
forEachProviders(rawCfg, func(index int, total int, name string, provider map[string]interface{}) {
bytes, _ := json.Marshal(&Status{
Action: "FetchProviders",
Args: []string{name},
Progress: index,
MaxProgress: total,
})
reportStatus(string(bytes))
u, uok := provider["url"]
p, pok := provider["path"]
if !uok || !pok {
return
}
us, uok := u.(string)
ps, pok := p.(string)
if !uok || !pok {
return
}
if _, err := os.Stat(ps); err == nil {
return
}
url, err := U.Parse(us)
if err != nil {
return
}
_ = fetch(url, ps)
})
bytes, _ := json.Marshal(&Status{
Action: "Verifying",
Args: []string{},
Progress: 0xffff,
MaxProgress: 0xffff,
})
reportStatus(string(bytes))
cfg, err := Parse(rawCfg)
if err != nil {
return err
}
destroyProviders(cfg)
return nil
}

View File

@@ -0,0 +1,102 @@
package config
import (
"io/ioutil"
P "path"
"strings"
"gopkg.in/yaml.v2"
"cfa/app"
"github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/hub/executor"
)
func logDns(cfg *config.RawConfig) {
bytes, err := yaml.Marshal(&cfg.DNS)
if err != nil {
log.Warnln("Marshal dns: %s", err.Error())
return
}
log.Infoln("dns:")
for _, line := range strings.Split(string(bytes), "\n") {
log.Infoln(" %s", line)
}
}
func UnmarshalAndPatch(profilePath string) (*config.RawConfig, error) {
configPath := P.Join(profilePath, "config.yaml")
configData, err := ioutil.ReadFile(configPath)
if err != nil {
return nil, err
}
rawConfig, err := config.UnmarshalRawConfig(configData)
if err != nil {
return nil, err
}
if err := process(rawConfig, profilePath); err != nil {
return nil, err
}
return rawConfig, nil
}
func Parse(rawConfig *config.RawConfig) (*config.Config, error) {
cfg, err := config.ParseRawConfig(rawConfig)
if err != nil {
return nil, err
}
return cfg, nil
}
func Load(path string) error {
rawCfg, err := UnmarshalAndPatch(path)
if err != nil {
log.Errorln("Load %s: %s", path, err.Error())
return err
}
logDns(rawCfg)
cfg, err := Parse(rawCfg)
if err != nil {
log.Errorln("Load %s: %s", path, err.Error())
return err
}
executor.ApplyConfig(cfg, true)
app.ApplySubtitlePattern(rawCfg.ClashForAndroid.UiSubtitlePattern)
return nil
}
func LoadDefault() {
rawConfig, _ := config.UnmarshalRawConfig([]byte{})
_ = patchDns(rawConfig, constant.Path.HomeDir())
cfg, err := config.ParseRawConfig(rawConfig)
if err != nil {
panic(err.Error())
}
executor.ApplyConfig(cfg, true)
}
func init() {
LoadDefault()
}

View File

@@ -0,0 +1,68 @@
package config
import (
"io/ioutil"
"os"
"github.com/Dreamacro/clash/constant"
)
type OverrideSlot int
const (
OverrideSlotPersist OverrideSlot = iota
OverrideSlotSession
)
const defaultPersistOverride = `{"dns":{"enable": false}}`
const defaultSessionOverride = `{}`
var sessionOverride = defaultSessionOverride
func overridePersistPath() string {
return constant.Path.Resolve("override.json")
}
func ReadOverride(slot OverrideSlot) string {
switch slot {
case OverrideSlotPersist:
file, err := os.OpenFile(overridePersistPath(), os.O_RDONLY, 0600)
if err != nil {
return defaultPersistOverride
}
buf, err := ioutil.ReadAll(file)
if err != nil {
return defaultPersistOverride
}
return string(buf)
case OverrideSlotSession:
return sessionOverride
}
return ""
}
func WriteOverride(slot OverrideSlot, content string) {
switch slot {
case OverrideSlotPersist:
file, err := os.OpenFile(overridePersistPath(), os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0600)
if err != nil {
return
}
_, err = file.Write([]byte(content))
case OverrideSlotSession:
sessionOverride = content
}
}
func ClearOverride(slot OverrideSlot) {
switch slot {
case OverrideSlotPersist:
_ = os.Remove(overridePersistPath())
case OverrideSlotSession:
sessionOverride = defaultSessionOverride
}
}

View File

@@ -0,0 +1,108 @@
package config
import (
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/Dreamacro/clash/log"
"github.com/dlclark/regexp2"
"cfa/app"
"cfa/common"
"github.com/Dreamacro/clash/config"
"github.com/Dreamacro/clash/dns"
)
var processors = []processor{
patchOverride,
patchGeneral,
patchProfile,
patchDns,
patchProviders,
validConfig,
}
type processor func(cfg *config.RawConfig, profileDir string) error
func patchOverride(cfg *config.RawConfig, _ string) error {
if err := json.NewDecoder(strings.NewReader(ReadOverride(OverrideSlotPersist))).Decode(cfg); err != nil {
log.Warnln("Apply persist override: %s", err.Error())
}
if err := json.NewDecoder(strings.NewReader(ReadOverride(OverrideSlotSession))).Decode(cfg); err != nil {
log.Warnln("Apply session override: %s", err.Error())
}
return nil
}
func patchGeneral(cfg *config.RawConfig, _ string) error {
cfg.Interface = ""
cfg.ExternalUI = ""
cfg.ExternalController = ""
return nil
}
func patchProfile(cfg *config.RawConfig, _ string) error {
cfg.Profile.StoreSelected = false
return nil
}
func patchDns(cfg *config.RawConfig, _ string) error {
if !cfg.DNS.Enable {
cfg.DNS.Enable = true
cfg.DNS.IPv6 = false
cfg.DNS.NameServer = defaultNameServers
cfg.DNS.Fallback = []string{}
cfg.DNS.FallbackFilter.GeoIP = false
cfg.DNS.FallbackFilter.IPCIDR = localNetwork
cfg.DNS.EnhancedMode = dns.FAKEIP
cfg.DNS.FakeIPRange = "198.18.0.0/16"
cfg.DNS.DefaultNameserver = defaultNameServers
cfg.DNS.FakeIPFilter = defaultFakeIPFilter
cfg.ClashForAndroid.AppendSystemDNS = true
}
if cfg.ClashForAndroid.AppendSystemDNS {
cfg.DNS.NameServer = append(cfg.DNS.NameServer, app.SystemDns()...)
}
return nil
}
func patchProviders(cfg *config.RawConfig, profileDir string) error {
forEachProviders(cfg, func(index int, total int, key string, provider map[string]interface{}) {
if path, ok := provider["path"].(string); ok {
provider["path"] = profileDir + "/providers/" + common.ResolveAsRoot(path)
}
})
return nil
}
func validConfig(cfg *config.RawConfig, _ string) error {
if len(cfg.Proxy) == 0 && len(cfg.ProxyProvider) == 0 {
return errors.New("profile does not contain `proxies` or `proxy-providers`")
}
if _, err := regexp2.Compile(cfg.ClashForAndroid.UiSubtitlePattern, 0); err != nil {
return fmt.Errorf("compile ui-subtitle-pattern: %s", err.Error())
}
return nil
}
func process(cfg *config.RawConfig, profileDir string) error {
for _, p := range processors {
if err := p(cfg, profileDir); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,22 @@
// +build !premium
package config
import "github.com/Dreamacro/clash/config"
func forEachProviders(rawCfg *config.RawConfig, fun func(index int, total int, key string, provider map[string]interface{})) {
total := len(rawCfg.ProxyProvider)
index := 0
for k, v := range rawCfg.ProxyProvider {
fun(index, total, k, v)
index++
}
}
func destroyProviders(cfg *config.Config) {
for _, p := range cfg.Providers {
_ = p.Destroy()
}
}

View File

@@ -0,0 +1,32 @@
// +build premium
package config
import "github.com/Dreamacro/clash/config"
func forEachProviders(rawCfg *config.RawConfig, fun func(index int, total int, key string, provider map[string]interface{})) {
total := len(rawCfg.ProxyProvider) + len(rawCfg.RuleProvider)
index := 0
for k, v := range rawCfg.ProxyProvider {
fun(index, total, k, v)
index++
}
for k, v := range rawCfg.RuleProvider {
fun(index, total, k, v)
index++
}
}
func destroyProviders(cfg *config.Config) {
for _, p := range cfg.ProxyProviders {
_ = p.Destroy()
}
for _, p := range cfg.RuleProviders {
_ = p.Destroy()
}
}

View File

@@ -0,0 +1,74 @@
package core
import (
"errors"
"net"
"syscall"
"cfa/blob"
"cfa/app"
"cfa/platform"
"github.com/Dreamacro/clash/component/process"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/component/dialer"
"github.com/Dreamacro/clash/component/mmdb"
"github.com/Dreamacro/clash/constant"
)
var errBlocked = errors.New("blocked")
func Init(home, versionName string, platformVersion int) {
mmdb.LoadFromBytes(blob.GeoipDatabase)
constant.SetHomeDir(home)
app.ApplyVersionName(versionName)
app.ApplyPlatformVersion(platformVersion)
process.DefaultPackageNameResolver = func(metadata *constant.Metadata) (string, error) {
src, dst := metadata.RawSrcAddr, metadata.RawDstAddr
if src == nil || dst == nil {
return "", process.ErrInvalidNetwork
}
uid := app.QuerySocketUid(metadata.RawSrcAddr, metadata.RawDstAddr)
pkg := app.QueryAppByUid(uid)
log.Debugln("[PKG] %s --> %s by %d[%s]", metadata.SourceAddress(), metadata.RemoteAddress(), uid, pkg)
return pkg, nil
}
dialer.DialerHook = func(dialer *net.Dialer) error {
dialer.Control = func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
app.MarkSocket(int(fd))
})
}
return nil
}
dialer.ListenPacketHook = func(lc *net.ListenConfig, address string) (string, error) {
lc.Control = func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
app.MarkSocket(int(fd))
})
}
if platform.ShouldBlockConnection() {
return "", errBlocked
}
return address, nil
}
dialer.DialHook = func(dialer *net.Dialer, network string, ip net.IP) error {
if platform.ShouldBlockConnection() {
return errBlocked
}
return nil
}
}

View File

@@ -0,0 +1,18 @@
// +build debug
package main
import (
"net/http"
_ "net/http/pprof"
"github.com/Dreamacro/clash/log"
)
func init() {
go func() {
log.Debugln("pprof service listen at: 0.0.0.0:8888")
_ = http.ListenAndServe("0.0.0.0:8888", nil)
}()
}

View File

@@ -0,0 +1,18 @@
module cfa
go 1.16
require (
cfa/blob v0.0.0 // local generated
github.com/Dreamacro/clash v0.0.0 // local
github.com/dlclark/regexp2 v1.4.0
github.com/kr328/tun2socket v0.0.0-20210412191540-3d56c47e2d99
github.com/miekg/dns v1.1.42
github.com/oschwald/geoip2-golang v1.5.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/yaml.v2 v2.4.0
)
replace github.com/Dreamacro/clash => ./clash
replace cfa/blob => ../../../build/intermediates/golang_blob

View File

@@ -0,0 +1,60 @@
github.com/Dreamacro/go-shadowsocks2 v0.1.7 h1:8CtbE1HoPPMfrQZGXmlluq6dO2lL31W6WRRE8fabc4Q=
github.com/Dreamacro/go-shadowsocks2 v0.1.7/go.mod h1:8p5G4cAj5ZlXwUR+Ww63gfSikr8kvw8uw3TDwLAJpUc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/go-chi/chi/v5 v5.0.3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.0/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kr328/tun2socket v0.0.0-20210412191540-3d56c47e2d99 h1:dkEFEnGUg2z/FAPywWr4yfR/sWDQK76qn3J4Y5H2hJs=
github.com/kr328/tun2socket v0.0.0-20210412191540-3d56c47e2d99/go.mod h1:FWfSixjrLgtK+dHkDoN6lHMNhvER24gnjUZd/wt8Z9o=
github.com/miekg/dns v1.1.42 h1:gWGe42RGaIqXQZ+r3WUGEKBEtvPHY2SXo4dqixDNxuY=
github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
github.com/oschwald/geoip2-golang v1.5.0 h1:igg2yQIrrcRccB1ytFXqBfOHCjXWIoMv85lVJ1ONZzw=
github.com/oschwald/geoip2-golang v1.5.0/go.mod h1:xdvYt5xQzB8ORWFqPnqMwZpCpgNagttWdoZLlJQzg7s=
github.com/oschwald/maxminddb-golang v1.8.0 h1:Uh/DSnGoxsyp/KYbY1AuP0tYEwfs0sCph9p/UMXK/Hk=
github.com/oschwald/maxminddb-golang v1.8.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o=
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210508051633-16afe75a6701 h1:lQVgcB3+FoAXOb20Dp6zTzAIrpj1k/yOOBN7s+Zv1rA=
golang.org/x/net v0.0.0-20210508051633-16afe75a6701/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210507161434-a76c4d0a0096 h1:5PbJGn5Sp3GEUjJ61aYbUP6RIo3Z3r2E4Tv9y2z8UHo=
golang.org/x/sys v0.0.0-20210507161434-a76c4d0a0096/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,79 @@
package main
//#include "bridge.h"
import "C"
import (
"strings"
"time"
"unsafe"
"github.com/Dreamacro/clash/log"
)
type message struct {
Level string `json:"level"`
Message string `json:"message"`
Time int64 `json:"time"`
}
func init() {
go func() {
sub := log.Subscribe()
defer log.UnSubscribe(sub)
for item := range sub {
msg := item.(*log.Event)
cPayload := C.CString(msg.Payload)
switch msg.LogLevel {
case log.INFO:
C.log_info(cPayload)
case log.ERROR:
C.log_error(cPayload)
case log.WARNING:
C.log_warn(cPayload)
case log.DEBUG:
C.log_debug(cPayload)
case log.SILENT:
C.log_verbose(cPayload)
}
}
}()
}
//export subscribeLogcat
func subscribeLogcat(remote unsafe.Pointer) {
go func(remote unsafe.Pointer) {
sub := log.Subscribe()
defer log.UnSubscribe(sub)
for i := range sub {
msg, ok := i.(*log.Event)
if !ok {
continue
}
if msg.LogLevel < log.Level() && !strings.HasPrefix(msg.Payload, "[APP]") {
continue
}
rMsg := &message{
Level: msg.LogLevel.String(),
Message: msg.Payload,
Time: time.Now().UnixNano() / 1000 / 1000,
}
if C.logcat_received(remote, marshalJson(rMsg)) != 0 {
C.release_object(remote)
log.Debugln("Logcat subscriber closed")
break
}
}
}(remote)
log.Infoln("[APP] Logcat level: %s", log.Level().String())
}

View File

@@ -0,0 +1,50 @@
package main
/*
#cgo LDFLAGS: -llog
#include "bridge.h"
*/
import "C"
import (
"runtime"
"cfa/config"
"cfa/core"
"cfa/tunnel"
"github.com/Dreamacro/clash/log"
)
func main() {
panic("Stub!")
}
//export coreInit
func coreInit(home, versionName C.c_string, sdkVersion C.int) {
h := C.GoString(home)
v := C.GoString(versionName)
s := int(sdkVersion)
core.Init(h, v, s)
reset()
}
//export reset
func reset() {
config.LoadDefault()
tunnel.ResetStatistic()
runtime.GC()
}
//export forceGc
func forceGc() {
go func() {
log.Infoln("[APP] request force GC")
runtime.GC()
}()
}

View File

@@ -0,0 +1,42 @@
// +build linux
package platform
import "syscall"
var nullFd int
var maxFdCount int
func init() {
fd, err := syscall.Open("/dev/null", syscall.O_WRONLY, 0644)
if err != nil {
panic(err.Error())
}
nullFd = fd
var limit syscall.Rlimit
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit); err != nil {
maxFdCount = 1024
} else {
maxFdCount = int(limit.Cur)
}
maxFdCount = maxFdCount / 4 * 3
}
func ShouldBlockConnection() bool {
fd, err := syscall.Dup(nullFd)
if err != nil {
return true
}
_ = syscall.Close(fd)
if fd > maxFdCount {
return true
}
return false
}

View File

@@ -0,0 +1,163 @@
// +build linux
package platform
import (
"bufio"
"encoding/binary"
"encoding/hex"
"fmt"
"net"
"os"
"strconv"
"strings"
"unsafe"
)
var netIndexOfLocal = -1
var netIndexOfUid = -1
var nativeEndian binary.ByteOrder
func QuerySocketUidFromProcFs(source, _ net.Addr) int {
if netIndexOfLocal < 0 || netIndexOfUid < 0 {
return -1
}
network := source.Network()
if strings.HasSuffix(network, "4") {
network = network[:len(network)-1]
}
path := "/proc/net/" + network
var sIP net.IP
var sPort int
switch s := source.(type) {
case *net.TCPAddr:
sIP = s.IP
sPort = s.Port
case *net.UDPAddr:
sIP = s.IP
sPort = s.Port
default:
return -1
}
if strings.HasSuffix(source.Network(), "6") {
sIP = sIP.To16()
} else {
sIP = sIP.To4()
}
if sIP == nil {
return -1
}
file, err := os.Open(path)
if err != nil {
return -1
}
defer file.Close()
reader := bufio.NewReader(file)
var bytes [2]byte
binary.BigEndian.PutUint16(bytes[:], uint16(sPort))
local := fmt.Sprintf("%s:%s", hex.EncodeToString(nativeEndianIP(sIP)), hex.EncodeToString(bytes[:]))
for {
row, _, err := reader.ReadLine()
if err != nil {
return -1
}
fields := strings.Fields(string(row))
if len(fields) <= netIndexOfLocal || len(fields) <= netIndexOfUid {
continue
}
if strings.EqualFold(local, fields[netIndexOfLocal]) {
uid, err := strconv.Atoi(fields[netIndexOfUid])
if err != nil {
return -1
}
return uid
}
}
}
func nativeEndianIP(ip net.IP) []byte {
result := make([]byte, len(ip))
for i := 0; i < len(ip); i += 4 {
value := binary.BigEndian.Uint32(ip[i:])
nativeEndian.PutUint32(result[i:], value)
}
return result
}
func init() {
file, err := os.Open("/proc/net/tcp")
if err != nil {
return
}
defer file.Close()
reader := bufio.NewReader(file)
header, _, err := reader.ReadLine()
if err != nil {
return
}
columns := strings.Fields(string(header))
var txQueue, rxQueue, tr, tmWhen bool
for idx, col := range columns {
offset := 0
if txQueue && rxQueue {
offset--
}
if tr && tmWhen {
offset--
}
switch col {
case "tx_queue":
txQueue = true
case "rx_queue":
rxQueue = true
case "tr":
tr = true
case "tm->when":
tmWhen = true
case "local_address":
netIndexOfLocal = idx + offset
case "uid":
netIndexOfUid = idx + offset
}
}
}
func init() {
var x uint32 = 0x01020304
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
nativeEndian = binary.BigEndian
} else {
nativeEndian = binary.LittleEndian
}
}

View File

@@ -0,0 +1,23 @@
package main
//#include "bridge.h"
import "C"
import "cfa/proxy"
//export startHttp
func startHttp(listenAt C.c_string) *C.char {
l := C.GoString(listenAt)
listen, err := proxy.Start(l)
if err != nil {
return nil
}
return C.CString(listen)
}
//export stopHttp
func stopHttp() {
proxy.Stop()
}

View File

@@ -0,0 +1,108 @@
package proxy
import (
"bufio"
"net"
"net/http"
"sync"
"time"
adapters "github.com/Dreamacro/clash/adapters/inbound"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
)
const (
LocalHttpTimeout = time.Millisecond * 100
)
var listener *httpListener
var lock sync.Mutex
type httpListener struct {
net.Listener
closed bool
}
func Start(listen string) (listenAt string, err error) {
lock.Lock()
defer lock.Unlock()
stopLocked()
l, err := net.Listen("tcp", listen)
if err != nil {
log.Errorln("Listen HTTP proxy at: %s: %s", listen, err.Error())
return
}
h := &httpListener{
Listener: l,
closed: false,
}
listener = h
go func() {
for !h.closed {
conn, err := h.Accept()
if err != nil {
log.Warnln("Accept connection: %s", err.Error())
continue
}
_ = conn.(*net.TCPConn).SetKeepAlive(false)
h.handleConn(conn)
}
}()
return h.Addr().String(), nil
}
func Stop() {
lock.Lock()
defer lock.Unlock()
stopLocked()
}
func stopLocked() {
if listener != nil {
listener.closed = true
_ = listener.Close()
}
listener = nil
}
func (l *httpListener) handleConn(conn net.Conn) {
_ = conn.SetReadDeadline(time.Now().Add(LocalHttpTimeout))
br := bufio.NewReader(conn)
request, err := http.ReadRequest(br)
_ = conn.SetReadDeadline(time.Time{})
if err != nil || request.URL.Host == "" {
if err != nil {
log.Warnln("HTTP Connection closed: %s", err.Error())
}
_ = conn.Close()
return
}
if request.Method == http.MethodConnect {
_, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
if err != nil {
return
}
tunnel.Add(adapters.NewHTTPS(request, conn))
return
}
tunnel.Add(adapters.NewHTTP(request, conn))
}

View File

@@ -0,0 +1,9 @@
#include "trace.h"
#if ENABLE_TRACE
void trace_method_exit(const char **name) {
__android_log_print(ANDROID_LOG_VERBOSE, TAG, "TRACE-OUT %s", *name);
}
#endif

View File

@@ -0,0 +1,19 @@
#pragma once
#include "bridge.h"
#include <android/log.h>
#define ENABLE_TRACE 0
#if ENABLE_TRACE
extern void trace_method_exit(const char **name);
#define TRACE_METHOD() __attribute__((cleanup(trace_method_exit))) const char *__method_name = __FUNCTION__; __android_log_print(ANDROID_LOG_VERBOSE, TAG, "TRACE-IN %s", __method_name)
#else
#define TRACE_METHOD()
#endif

View File

@@ -0,0 +1,83 @@
package main
//#include "bridge.h"
import "C"
import (
"context"
"unsafe"
"cfa/app"
"cfa/tun"
"golang.org/x/sync/semaphore"
"github.com/Dreamacro/clash/log"
)
type remoteTun struct {
callback unsafe.Pointer
closed bool
limit *semaphore.Weighted
}
func (t *remoteTun) markSocket(fd int) {
_ = t.limit.Acquire(context.Background(), 1)
defer t.limit.Release(1)
if t.closed {
return
}
C.mark_socket(t.callback, C.int(fd))
}
func (t *remoteTun) querySocketUid(protocol int, source, target string) int {
_ = t.limit.Acquire(context.Background(), 1)
defer t.limit.Release(1)
if t.closed {
return -1
}
return int(C.query_socket_uid(t.callback, C.int(protocol), C.CString(source), C.CString(target)))
}
func (t *remoteTun) stop() {
_ = t.limit.Acquire(context.Background(), 4)
defer t.limit.Release(4)
t.closed = true
C.release_object(t.callback)
log.Infoln("Android tun device destroyed")
}
//export startTun
func startTun(fd, mtu C.int, gateway, mirror, dns C.c_string, callback unsafe.Pointer) C.int {
f := int(fd)
m := int(mtu)
g := C.GoString(gateway)
mr := C.GoString(mirror)
d := C.GoString(dns)
remote := &remoteTun{callback: callback, closed: false, limit: semaphore.NewWeighted(4)}
if tun.Start(f, m, g, mr, d, remote.stop) != nil {
return 1
}
app.ApplyTunContext(remote.markSocket, remote.querySocketUid)
log.Infoln("Android tun device created")
return 0
}
//export stopTun
func stopTun() {
tun.Stop()
}

View File

@@ -0,0 +1,101 @@
package tun
import (
"encoding/binary"
"io"
"net"
"time"
"github.com/Dreamacro/clash/component/resolver"
D "github.com/miekg/dns"
"github.com/kr328/tun2socket/binding"
"github.com/kr328/tun2socket/redirect"
)
const defaultDnsReadTimeout = time.Second * 30
func shouldHijackDns(dnsAddr binding.Address, targetAddr binding.Address) bool {
if targetAddr.Port != 53 {
return false
}
return dnsAddr.IP.Equal(net.IPv4zero) || dnsAddr.IP.Equal(targetAddr.IP)
}
func hijackUDPDns(pkt []byte, ep *binding.Endpoint, sender redirect.UDPSender) {
go func() {
answer, err := relayDnsPacket(pkt)
if err != nil {
return
}
_ = sender(answer, &binding.Endpoint{
Source: ep.Target,
Target: ep.Source,
})
}()
}
func hijackTCPDns(conn net.Conn) {
go func() {
defer conn.Close()
for {
if err := conn.SetReadDeadline(time.Now().Add(defaultDnsReadTimeout)); err != nil {
return
}
var length uint16
if binary.Read(conn, binary.BigEndian, &length) != nil {
return
}
data := make([]byte, length)
_, err := io.ReadFull(conn, data)
if err != nil {
return
}
rb, err := relayDnsPacket(data)
if err != nil {
continue
}
if binary.Write(conn, binary.BigEndian, uint16(len(rb))) != nil {
return
}
if _, err := conn.Write(rb); err != nil {
return
}
}
}()
}
func relayDnsPacket(payload []byte) ([]byte, error) {
msg := &D.Msg{}
if err := msg.Unpack(payload); err != nil {
return nil, err
}
r, err := resolver.ResolveMsg(msg)
if err != nil {
return nil, err
}
for _, ans := range r.Answer {
header := ans.Header()
if header.Class == D.ClassINET && (header.Rrtype == D.TypeA || header.Rrtype == D.TypeAAAA) {
header.Ttl = 1
}
}
r.SetRcode(msg, r.Rcode)
return r.Pack()
}

View File

@@ -0,0 +1,21 @@
package tun
import "github.com/Dreamacro/clash/log"
type logger struct{}
func (l *logger) D(format string, args ...interface{}) {
log.Debugln(format, args...)
}
func (l *logger) I(format string, args ...interface{}) {
log.Infoln(format, args...)
}
func (l *logger) W(format string, args ...interface{}) {
log.Warnln(format, args...)
}
func (l *logger) E(format string, args ...interface{}) {
log.Errorln(format, args...)
}

View File

@@ -0,0 +1,40 @@
package tun
import (
"net"
"strconv"
"github.com/kr328/tun2socket/binding"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/context"
"github.com/Dreamacro/clash/tunnel"
)
func handleTCP(conn net.Conn, endpoint *binding.Endpoint) {
src := &net.TCPAddr{
IP: endpoint.Source.IP,
Port: int(endpoint.Source.Port),
Zone: "",
}
dst := &net.TCPAddr{
IP: endpoint.Target.IP,
Port: int(endpoint.Target.Port),
Zone: "",
}
metadata := &C.Metadata{
NetWork: C.TCP,
Type: C.SOCKS,
SrcIP: src.IP,
DstIP: dst.IP,
SrcPort: strconv.Itoa(src.Port),
DstPort: strconv.Itoa(dst.Port),
AddrType: C.AtypIPv4,
Host: "",
RawSrcAddr: src,
RawDstAddr: dst,
}
tunnel.Add(context.NewConnContext(conn, metadata))
}

View File

@@ -0,0 +1,84 @@
package tun
import (
"net"
"os"
"strconv"
"sync"
"github.com/kr328/tun2socket/binding"
"github.com/kr328/tun2socket/redirect"
"github.com/kr328/tun2socket"
)
var lock sync.Mutex
var tun *tun2socket.Tun2Socket
func Start(fd, mtu int, gateway, mirror, dns string, onStop func()) error {
lock.Lock()
defer lock.Unlock()
stopLocked()
dnsHost, dnsPort, err := net.SplitHostPort(dns)
if err != nil {
return err
}
dnsP, err := strconv.Atoi(dnsPort)
if err != nil {
return err
}
dnsAddr := binding.Address{
IP: net.ParseIP(dnsHost),
Port: uint16(dnsP),
}
t := tun2socket.NewTun2Socket(os.NewFile(uintptr(fd), "/dev/tun"), mtu, net.ParseIP(gateway), net.ParseIP(mirror))
t.SetAllocator(allocUDP)
t.SetClosedHandler(onStop)
t.SetLogger(&logger{})
t.SetTCPHandler(func(conn net.Conn, endpoint *binding.Endpoint) {
if shouldHijackDns(dnsAddr, endpoint.Target) {
hijackTCPDns(conn)
return
}
handleTCP(conn, endpoint)
})
t.SetUDPHandler(func(payload []byte, endpoint *binding.Endpoint, sender redirect.UDPSender) {
if shouldHijackDns(dnsAddr, endpoint.Target) {
hijackUDPDns(payload, endpoint, sender)
return
}
handleUDP(payload, endpoint, sender)
})
t.Start()
tun = t
return nil
}
func Stop() {
lock.Lock()
defer lock.Unlock()
stopLocked()
}
func stopLocked() {
if tun != nil {
tun.Close()
}
tun = nil
}

View File

@@ -0,0 +1,76 @@
package tun
import (
"io"
"net"
"github.com/Dreamacro/clash/transport/socks5"
"github.com/kr328/tun2socket/binding"
"github.com/kr328/tun2socket/redirect"
adapters "github.com/Dreamacro/clash/adapters/inbound"
"github.com/Dreamacro/clash/common/pool"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/tunnel"
)
type udpPacket struct {
metadata *C.Metadata
source binding.Address
data []byte
send redirect.UDPSender
}
func (u *udpPacket) Data() []byte {
return u.data
}
func (u *udpPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) {
uAddr, ok := addr.(*net.UDPAddr)
if !ok {
return 0, io.ErrClosedPipe
}
return len(b), u.send(b, &binding.Endpoint{
Source: binding.Address{IP: uAddr.IP, Port: uint16(uAddr.Port)},
Target: u.source,
})
}
func (u *udpPacket) Drop() {
recycleUDP(u.data)
}
func (u *udpPacket) LocalAddr() net.Addr {
return &net.UDPAddr{
IP: u.source.IP,
Port: int(u.source.Port),
Zone: "",
}
}
func handleUDP(payload []byte, endpoint *binding.Endpoint, sender redirect.UDPSender) {
pkt := &udpPacket{
source: endpoint.Source,
data: payload,
send: sender,
}
rAddr := &net.UDPAddr{
IP: endpoint.Target.IP,
Port: int(endpoint.Target.Port),
Zone: "",
}
adapter := adapters.NewPacket(socks5.ParseAddrToSocksAddr(rAddr), pkt, C.SOCKS)
tunnel.AddPacket(adapter)
}
func allocUDP(size int) []byte {
return pool.Get(size)
}
func recycleUDP(payload []byte) {
_ = pool.Put(payload)
}

View File

@@ -0,0 +1,124 @@
package main
//#include "bridge.h"
import "C"
import (
"unsafe"
"cfa/app"
"cfa/tunnel"
)
//export queryTunnelState
func queryTunnelState() *C.char {
mode := tunnel.QueryMode()
response := &struct {
Mode string `json:"mode"`
}{mode}
return marshalJson(response)
}
//export queryNow
func queryNow(upload, download *C.uint64_t) {
up, down := tunnel.Now()
*upload = C.uint64_t(up)
*download = C.uint64_t(down)
}
//export queryTotal
func queryTotal(upload, download *C.uint64_t) {
up, down := tunnel.Total()
*upload = C.uint64_t(up)
*download = C.uint64_t(down)
}
//export queryGroupNames
func queryGroupNames(excludeNotSelectable C.int) *C.char {
return marshalJson(tunnel.QueryProxyGroupNames(excludeNotSelectable != 0))
}
//export queryGroup
func queryGroup(name C.c_string, sortMode C.c_string) *C.char {
n := C.GoString(name)
s := C.GoString(sortMode)
mode := tunnel.Default
switch s {
case "Title":
mode = tunnel.Title
case "Delay":
mode = tunnel.Delay
}
response := tunnel.QueryProxyGroup(n, mode, app.SubtitlePattern())
if response == nil {
return nil
}
return marshalJson(response)
}
//export healthCheck
func healthCheck(completable unsafe.Pointer, name C.c_string) {
go func(name string) {
tunnel.HealthCheck(name)
C.complete(completable, nil)
}(C.GoString(name))
}
//export healthCheckAll
func healthCheckAll() {
tunnel.HealthCheckAll()
}
//export patchSelector
func patchSelector(selector, name C.c_string) C.int {
s := C.GoString(selector)
n := C.GoString(name)
if tunnel.PatchSelector(s, n) {
return 1
}
return 0
}
//export queryProviders
func queryProviders() *C.char {
return marshalJson(tunnel.QueryProviders())
}
//export updateProvider
func updateProvider(completable unsafe.Pointer, pType C.c_string, name C.c_string) {
go func(pType, name string) {
C.complete(completable, marshalString(tunnel.UpdateProvider(pType, name)))
C.release_object(completable)
}(C.GoString(pType), C.GoString(name))
}
//export suspend
func suspend(suspended C.int) {
tunnel.Suspend(suspended != 0)
}
//export installSideloadGeoip
func installSideloadGeoip(block unsafe.Pointer, blockSize C.int) *C.char {
if block == nil {
_ = tunnel.InstallSideloadGeoip(nil)
return nil
}
bytes := C.GoBytes(block, blockSize)
return marshalString(tunnel.InstallSideloadGeoip(bytes))
}

View File

@@ -0,0 +1,28 @@
package tunnel
import (
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/tunnel/statistic"
)
func closeMatch(filter func(conn C.Conn) bool) {
for _, c := range statistic.DefaultManager.Snapshot().Connections {
if cc, ok := c.(C.Conn); ok {
if filter(cc) {
_ = cc.Close()
}
}
}
}
func closeConnByGroup(name string) {
closeMatch(func(conn C.Conn) bool {
for _, c := range conn.Chains() {
if c == name {
return true
}
}
return false
})
}

View File

@@ -0,0 +1,50 @@
package tunnel
import (
"sync"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/outboundgroup"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
)
func HealthCheck(name string) {
p := tunnel.Proxies()[name]
if p == nil {
log.Warnln("Request health check for `%s`: not found", name)
return
}
g, ok := p.(*outbound.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
log.Warnln("Request health check for `%s`: invalid type %s", name, p.Type().String())
return
}
wg := &sync.WaitGroup{}
for _, pr := range g.Providers() {
wg.Add(1)
go func(provider provider.ProxyProvider) {
provider.HealthCheck()
wg.Done()
}(pr)
}
wg.Wait()
}
func HealthCheckAll() {
for _, g := range QueryProxyGroupNames(false) {
go func(group string) {
HealthCheck(group)
}(g)
}
}

View File

@@ -0,0 +1,26 @@
package tunnel
import (
"fmt"
"github.com/oschwald/geoip2-golang"
"github.com/Dreamacro/clash/component/mmdb"
)
func InstallSideloadGeoip(block []byte) error {
if block == nil {
mmdb.InstallOverride(nil)
return nil
}
db, err := geoip2.FromBytes(block)
if err != nil {
return fmt.Errorf("load sideload geoip mmdb: %s", err.Error())
}
mmdb.InstallOverride(db)
return nil
}

View File

@@ -0,0 +1,63 @@
// +build !premium
package tunnel
import (
"errors"
"fmt"
"time"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/tunnel"
)
var ErrInvalidType = errors.New("invalid type")
type Provider struct {
Name string `json:"name"`
VehicleType string `json:"vehicleType"`
Type string `json:"type"`
UpdatedAt int64 `json:"updatedAt"`
}
func QueryProviders() []*Provider {
p := tunnel.Providers()
providers := make([]provider.Provider, 0, len(p))
for _, proxy := range p {
if proxy.VehicleType() == provider.Compatible {
continue
}
providers = append(providers, proxy)
}
result := make([]*Provider, 0, len(providers))
for _, p := range providers {
updatedAt := time.Time{}
if s, ok := p.(provider.UpdatableProvider); ok {
updatedAt = s.UpdatedAt()
}
result = append(result, &Provider{
Name: p.Name(),
VehicleType: p.VehicleType().String(),
Type: p.Type().String(),
UpdatedAt: updatedAt.UnixNano() / 1000 / 1000,
})
}
return result
}
func UpdateProvider(_ string, name string) error {
p, ok := tunnel.Providers()[name]
if !ok {
return fmt.Errorf("%s not found", name)
}
return p.Update()
}

View File

@@ -0,0 +1,91 @@
// +build premium
package tunnel
import (
"errors"
"fmt"
"time"
"github.com/Dreamacro/clash/adapters/provider"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
)
var ErrInvalidType = errors.New("invalid type")
type Provider struct {
Name string `json:"name"`
VehicleType string `json:"vehicleType"`
Type string `json:"type"`
UpdatedAt int64 `json:"updatedAt"`
}
func QueryProviders() []*Provider {
r := tunnel.RuleProviders()
p := tunnel.ProxyProviders()
providers := make([]provider.Provider, 0, len(r)+len(p))
for _, rule := range r {
if rule.VehicleType() == provider.Compatible {
continue
}
providers = append(providers, rule)
}
for _, proxy := range p {
if proxy.VehicleType() == provider.Compatible {
continue
}
providers = append(providers, proxy)
}
result := make([]*Provider, 0, len(providers))
for _, p := range providers {
updatedAt := time.Time{}
if s, ok := p.(provider.UpdatableProvider); ok {
updatedAt = s.UpdatedAt()
}
result = append(result, &Provider{
Name: p.Name(),
VehicleType: p.VehicleType().String(),
Type: p.Type().String(),
UpdatedAt: updatedAt.UnixNano() / 1000 / 1000,
})
}
return result
}
func UpdateProvider(t string, name string) error {
err := ErrInvalidType
switch t {
case "Rule":
p := tunnel.RuleProviders()[name]
if p == nil {
return fmt.Errorf("%s not found", name)
}
err = p.Update()
case "Proxy":
p := tunnel.ProxyProviders()[name]
if p == nil {
return fmt.Errorf("%s not found", name)
}
err = p.Update()
}
if err != nil {
log.Warnln("Updating provider %s: %s", name, err.Error())
}
return err
}

View File

@@ -0,0 +1,195 @@
package tunnel
import (
"sort"
"strings"
"github.com/dlclark/regexp2"
"github.com/Dreamacro/clash/adapters/outbound"
"github.com/Dreamacro/clash/adapters/outboundgroup"
"github.com/Dreamacro/clash/adapters/provider"
C "github.com/Dreamacro/clash/constant"
"github.com/Dreamacro/clash/log"
"github.com/Dreamacro/clash/tunnel"
)
type SortMode int
const (
Default SortMode = iota
Title
Delay
)
type Proxy struct {
Name string `json:"name"`
Title string `json:"title"`
Subtitle string `json:"subtitle"`
Type string `json:"type"`
Delay int `json:"delay"`
}
type ProxyGroup struct {
Type string `json:"type"`
Now string `json:"now"`
Proxies []*Proxy `json:"proxies"`
}
type sortableProxyList struct {
list []*Proxy
less func(a, b *Proxy) bool
}
func (s *sortableProxyList) Len() int {
return len(s.list)
}
func (s *sortableProxyList) Less(i, j int) bool {
return s.less(s.list[i], s.list[j])
}
func (s *sortableProxyList) Swap(i, j int) {
s.list[i], s.list[j] = s.list[j], s.list[i]
}
func QueryProxyGroupNames(excludeNotSelectable bool) []string {
mode := tunnel.Mode()
if mode == tunnel.Direct {
return []string{}
}
global := tunnel.Proxies()["GLOBAL"].(*outbound.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup)
proxies := global.Providers()[0].Proxies()
result := make([]string, 0, len(proxies)+1)
if mode == tunnel.Global {
result = append(result, "GLOBAL")
}
for _, p := range proxies {
if _, ok := p.(*outbound.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup); ok {
if !excludeNotSelectable || p.Type() == C.Selector {
result = append(result, p.Name())
}
}
}
return result
}
func QueryProxyGroup(name string, sortMode SortMode, uiSubtitlePattern *regexp2.Regexp) *ProxyGroup {
p := tunnel.Proxies()[name]
if p == nil {
log.Warnln("Query group `%s`: not found", name)
return nil
}
g, ok := p.(*outbound.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
log.Warnln("Query group `%s`: invalid type %s", name, p.Type().String())
return nil
}
proxies := collectProviders(g.Providers(), uiSubtitlePattern)
switch sortMode {
case Title:
wrapper := &sortableProxyList{
list: proxies,
less: func(a, b *Proxy) bool {
return strings.Compare(a.Title, b.Title) < 0
},
}
sort.Sort(wrapper)
case Delay:
wrapper := &sortableProxyList{
list: proxies,
less: func(a, b *Proxy) bool {
return a.Delay < b.Delay
},
}
sort.Sort(wrapper)
case Default:
default:
}
return &ProxyGroup{
Type: g.Type().String(),
Now: g.Now(),
Proxies: proxies,
}
}
func PatchSelector(selector, name string) bool {
p := tunnel.Proxies()[selector]
if p == nil {
log.Warnln("Patch selector `%s`: not found", selector)
return false
}
g, ok := p.(*outbound.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup)
if !ok {
log.Warnln("Patch selector `%s`: invalid type %s", selector, p.Type().String())
return false
}
s, ok := g.(*outboundgroup.Selector)
if !ok {
log.Warnln("Patch selector `%s`: invalid type %s", selector, p.Type().String())
return false
}
if err := s.Set(name); err != nil {
log.Warnln("Patch selector `%s`: %s", selector, err.Error())
}
log.Infoln("Patch selector %s -> %s", selector, name)
closeConnByGroup(selector)
return true
}
func collectProviders(providers []provider.ProxyProvider, uiSubtitlePattern *regexp2.Regexp) []*Proxy {
result := make([]*Proxy, 0, 128)
for _, p := range providers {
for _, px := range p.Proxies() {
name := px.Name()
title := name
subtitle := px.Type().String()
if uiSubtitlePattern != nil {
if _, ok := px.(*outbound.Proxy).ProxyAdapter.(outboundgroup.ProxyGroup); !ok {
runes := []rune(name)
match, err := uiSubtitlePattern.FindRunesMatch(runes)
if err == nil && match != nil {
title = string(runes[:match.Index]) + string(runes[match.Index+match.Length:])
subtitle = string(runes[match.Index : match.Index+match.Length])
}
}
}
result = append(result, &Proxy{
Name: name,
Title: strings.TrimSpace(title),
Subtitle: strings.TrimSpace(subtitle),
Type: px.Type().String(),
Delay: int(px.LastDelay()),
})
}
}
return result
}

View File

@@ -0,0 +1,9 @@
package tunnel
import (
"github.com/Dreamacro/clash/tunnel"
)
func QueryMode() string {
return tunnel.Mode().String()
}

View File

@@ -0,0 +1,17 @@
package tunnel
import (
"github.com/Dreamacro/clash/tunnel/statistic"
)
func ResetStatistic() {
statistic.DefaultManager.ResetStatistic()
}
func Now() (up int64, down int64) {
return statistic.DefaultManager.Now()
}
func Total() (up int64, down int64) {
return statistic.DefaultManager.Total()
}

View File

@@ -0,0 +1,7 @@
package tunnel
import "github.com/Dreamacro/clash/adapters/provider"
func Suspend(s bool) {
provider.Suspend(s)
}

View File

@@ -0,0 +1,32 @@
package main
import "C"
import (
"encoding/json"
"reflect"
)
func marshalJson(obj interface{}) *C.char {
res, err := json.Marshal(obj)
if err != nil {
panic(err.Error())
}
return C.CString(string(res))
}
func marshalString(obj interface{}) *C.char {
if obj == nil {
return nil
}
switch o := obj.(type) {
case error:
return C.CString(o.Error())
case string:
return C.CString(o)
}
panic("invalid marshal type " + reflect.TypeOf(obj).Name())
}

View File

@@ -0,0 +1,228 @@
package com.github.kr328.clash.core
import com.github.kr328.clash.core.bridge.*
import com.github.kr328.clash.core.model.*
import com.github.kr328.clash.core.util.parseInetSocketAddress
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.jsonPrimitive
import java.io.File
import java.net.InetSocketAddress
object Clash {
enum class OverrideSlot {
Persist, Session
}
private val ConfigurationOverrideJson = Json {
ignoreUnknownKeys = true
encodeDefaults = false
}
fun reset() {
Bridge.nativeReset()
}
fun forceGc() {
Bridge.nativeForceGc()
}
fun suspendCore(suspended: Boolean) {
Bridge.nativeSuspend(suspended)
}
fun queryTunnelState(): TunnelState {
val json = Bridge.nativeQueryTunnelState()
return Json.decodeFromString(TunnelState.serializer(), json)
}
fun queryTrafficNow(): Traffic {
return Bridge.nativeQueryTrafficNow()
}
fun queryTrafficTotal(): Traffic {
return Bridge.nativeQueryTrafficTotal()
}
fun notifyDnsChanged(dns: List<String>) {
Bridge.nativeNotifyDnsChanged(dns.joinToString(separator = ","))
}
fun notifyInstalledAppsChanged(uids: List<Pair<Int, String>>) {
val uidList = uids.joinToString(separator = ",") { "${it.first}:${it.second}" }
Bridge.nativeNotifyInstalledAppChanged(uidList)
}
fun startTun(
fd: Int,
mtu: Int,
gateway: String,
mirror: String,
dns: String,
markSocket: (Int) -> Boolean,
querySocketUid: (protocol: Int, source: InetSocketAddress, target: InetSocketAddress) -> Int
) {
Bridge.nativeStartTun(fd, mtu, gateway, mirror, "$dns:53", object : TunInterface {
override fun markSocket(fd: Int) {
markSocket(fd)
}
override fun querySocketUid(protocol: Int, source: String, target: String): Int {
return querySocketUid(
protocol,
parseInetSocketAddress(source),
parseInetSocketAddress(target)
)
}
})
}
fun stopTun() {
Bridge.nativeStopTun()
}
fun startHttp(listenAt: String): String? {
return Bridge.nativeStartHttp(listenAt)
}
fun stopHttp() {
Bridge.nativeStopHttp()
}
fun queryGroupNames(excludeNotSelectable: Boolean): List<String> {
val names = Json.Default.decodeFromString(
JsonArray.serializer(),
Bridge.nativeQueryGroupNames(excludeNotSelectable)
)
return names.map {
require(it.jsonPrimitive.isString)
it.jsonPrimitive.content
}
}
fun queryGroup(name: String, sort: ProxySort): ProxyGroup {
return Bridge.nativeQueryGroup(name, sort.name)
?.let { Json.Default.decodeFromString(ProxyGroup.serializer(), it) }
?: ProxyGroup(Proxy.Type.Unknown, emptyList(), "")
}
fun healthCheck(name: String): CompletableDeferred<Unit> {
return CompletableDeferred<Unit>().apply {
Bridge.nativeHealthCheck(this, name)
}
}
fun healthCheckAll() {
Bridge.nativeHealthCheckAll()
}
fun patchSelector(selector: String, name: String): Boolean {
return Bridge.nativePatchSelector(selector, name)
}
fun fetchAndValid(
path: File,
url: String,
force: Boolean,
reportStatus: (FetchStatus) -> Unit
): CompletableDeferred<Unit> {
return CompletableDeferred<Unit>().apply {
Bridge.nativeFetchAndValid(
object : FetchCallback {
override fun report(statusJson: String) {
reportStatus(
Json.Default.decodeFromString(
FetchStatus.serializer(),
statusJson
)
)
}
override fun complete(error: String?) {
if (error != null)
completeExceptionally(ClashException(error))
else
complete(Unit)
}
},
path.absolutePath,
url,
force
)
}
}
fun load(path: File): CompletableDeferred<Unit> {
return CompletableDeferred<Unit>().apply {
Bridge.nativeLoad(this, path.absolutePath)
}
}
fun queryProviders(): List<Provider> {
val providers =
Json.Default.decodeFromString(JsonArray.serializer(), Bridge.nativeQueryProviders())
return List(providers.size) {
Json.Default.decodeFromJsonElement(Provider.serializer(), providers[it])
}
}
fun updateProvider(type: Provider.Type, name: String): CompletableDeferred<Unit> {
return CompletableDeferred<Unit>().apply {
Bridge.nativeUpdateProvider(this, type.toString(), name)
}
}
fun queryOverride(slot: OverrideSlot): ConfigurationOverride {
return try {
ConfigurationOverrideJson.decodeFromString(
ConfigurationOverride.serializer(),
Bridge.nativeReadOverride(slot.ordinal)
)
} catch (e: Exception) {
ConfigurationOverride()
}
}
fun patchOverride(slot: OverrideSlot, configuration: ConfigurationOverride) {
Bridge.nativeWriteOverride(
slot.ordinal,
ConfigurationOverrideJson.encodeToString(
ConfigurationOverride.serializer(),
configuration
)
)
}
fun clearOverride(slot: OverrideSlot) {
Bridge.nativeClearOverride(slot.ordinal)
}
fun installSideloadGeoip(data: ByteArray?) {
Bridge.nativeInstallSideloadGeoip(data)
}
fun queryConfiguration(): UiConfiguration {
return Json.Default.decodeFromString(
UiConfiguration.serializer(),
Bridge.nativeQueryConfiguration()
)
}
fun subscribeLogcat(): ReceiveChannel<LogMessage> {
return Channel<LogMessage>(32).apply {
Bridge.nativeSubscribeLogcat(object : LogcatInterface {
override fun received(jsonPayload: String) {
offer(Json.decodeFromString(LogMessage.serializer(), jsonPayload))
}
})
}
}
}

View File

@@ -0,0 +1,75 @@
package com.github.kr328.clash.core.bridge
import android.os.Build
import android.os.ParcelFileDescriptor
import androidx.annotation.Keep
import com.github.kr328.clash.common.Global
import kotlinx.coroutines.CompletableDeferred
import java.io.File
@Keep
object Bridge {
external fun nativeReset()
external fun nativeForceGc()
external fun nativeSuspend(suspend: Boolean)
external fun nativeQueryTunnelState(): String
external fun nativeQueryTrafficNow(): Long
external fun nativeQueryTrafficTotal(): Long
external fun nativeNotifyDnsChanged(dnsList: String)
external fun nativeNotifyInstalledAppChanged(uidList: String)
external fun nativeStartTun(
fd: Int,
mtu: Int,
gateway: String,
mirror: String,
dns: String,
cb: TunInterface
)
external fun nativeStopTun()
external fun nativeStartHttp(listenAt: String): String?
external fun nativeStopHttp()
external fun nativeQueryGroupNames(excludeNotSelectable: Boolean): String
external fun nativeQueryGroup(name: String, sort: String): String?
external fun nativeHealthCheck(completable: CompletableDeferred<Unit>, name: String)
external fun nativeHealthCheckAll()
external fun nativePatchSelector(selector: String, name: String): Boolean
external fun nativeFetchAndValid(
completable: FetchCallback,
path: String,
url: String,
force: Boolean
)
external fun nativeLoad(completable: CompletableDeferred<Unit>, path: String)
external fun nativeQueryProviders(): String
external fun nativeUpdateProvider(
completable: CompletableDeferred<Unit>,
type: String,
name: String
)
external fun nativeReadOverride(slot: Int): String
external fun nativeWriteOverride(slot: Int, content: String)
external fun nativeClearOverride(slot: Int)
external fun nativeInstallSideloadGeoip(data: ByteArray?)
external fun nativeQueryConfiguration(): String
external fun nativeSubscribeLogcat(callback: LogcatInterface)
private external fun nativeInit(home: String, versionName: String, sdkVersion: Int)
init {
System.loadLibrary("bridge")
val ctx = Global.application
ParcelFileDescriptor.open(File(ctx.packageCodePath), ParcelFileDescriptor.MODE_READ_ONLY)
.detachFd()
val home = ctx.filesDir.resolve("clash").apply { mkdirs() }.absolutePath
val versionName = ctx.packageManager.getPackageInfo(ctx.packageName, 0).versionName
val sdkVersion = Build.VERSION.SDK_INT
nativeInit(home, versionName, sdkVersion)
}
}

View File

@@ -0,0 +1,6 @@
package com.github.kr328.clash.core.bridge
import androidx.annotation.Keep
@Keep
class ClashException(msg: String) : IllegalArgumentException(msg)

View File

@@ -0,0 +1,21 @@
package com.github.kr328.clash.core.bridge
import android.net.Uri
import androidx.annotation.Keep
import com.github.kr328.clash.common.Global
import java.io.FileNotFoundException
@Keep
object Content {
@JvmStatic
fun open(url: String): Int {
val uri = Uri.parse(url)
if (uri.scheme != "content") {
throw UnsupportedOperationException("Unsupported scheme ${uri.scheme}")
}
return Global.application.contentResolver.openFileDescriptor(uri, "r")?.detachFd()
?: throw FileNotFoundException("$uri not found")
}
}

View File

@@ -0,0 +1,9 @@
package com.github.kr328.clash.core.bridge
import androidx.annotation.Keep
@Keep
interface FetchCallback {
fun report(statusJson: String)
fun complete(error: String?)
}

View File

@@ -0,0 +1,8 @@
package com.github.kr328.clash.core.bridge
import androidx.annotation.Keep
@Keep
interface LogcatInterface {
fun received(jsonPayload: String)
}

View File

@@ -0,0 +1,9 @@
package com.github.kr328.clash.core.bridge
import androidx.annotation.Keep
@Keep
interface TunInterface {
fun markSocket(fd: Int)
fun querySocketUid(protocol: Int, source: String, target: String): Int
}

View File

@@ -0,0 +1,133 @@
package com.github.kr328.clash.core.model
import android.os.Parcel
import android.os.Parcelable
import com.github.kr328.clash.core.util.Parcelizer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ConfigurationOverride(
@SerialName("port")
var httpPort: Int? = null,
@SerialName("socks-port")
var socksPort: Int? = null,
@SerialName("redir-port")
var redirectPort: Int? = null,
@SerialName("tproxy-port")
var tproxyPort: Int? = null,
@SerialName("mixed-port")
var mixedPort: Int? = null,
@SerialName("authentication")
var authentication: List<String>? = null,
@SerialName("allow-lan")
var allowLan: Boolean? = null,
@SerialName("bind-address")
var bindAddress: String? = null,
@SerialName("mode")
var mode: TunnelState.Mode? = null,
@SerialName("log-level")
var logLevel: LogMessage.Level? = null,
@SerialName("ipv6")
var ipv6: Boolean? = null,
@SerialName("hosts")
var hosts: Map<String, String>? = null,
@SerialName("dns")
val dns: Dns = Dns(),
@SerialName("clash-for-android")
val app: App = App(),
) : Parcelable {
@Serializable
data class Dns(
@SerialName("enable")
var enable: Boolean? = null,
@SerialName("listen")
var listen: String? = null,
@SerialName("ipv6")
var ipv6: Boolean? = null,
@SerialName("use-hosts")
var useHosts: Boolean? = null,
@SerialName("enhanced-mode")
var enhancedMode: DnsEnhancedMode? = null,
@SerialName("nameserver")
var nameServer: List<String>? = null,
@SerialName("fallback")
var fallback: List<String>? = null,
@SerialName("default-nameserver")
var defaultServer: List<String>? = null,
@SerialName("fake-ip-filter")
var fakeIpFilter: List<String>? = null,
@SerialName("fallback-filter")
val fallbackFilter: DnsFallbackFilter = DnsFallbackFilter()
)
@Serializable
data class DnsFallbackFilter(
@SerialName("geoip")
var geoIp: Boolean? = null,
@SerialName("ipcidr")
var ipcidr: List<String>? = null,
@SerialName("domain")
var domain: List<String>? = null,
)
@Serializable
data class App(
@SerialName("append-system-dns")
var appendSystemDns: Boolean? = null
)
@Serializable
enum class DnsEnhancedMode {
@SerialName("normal")
None,
@SerialName("redir-host")
Mapping,
@SerialName("fake-ip")
FakeIp,
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
Parcelizer.encodeToParcel(serializer(), parcel, this)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ConfigurationOverride> {
override fun createFromParcel(parcel: Parcel): ConfigurationOverride {
return Parcelizer.decodeFromParcel(serializer(), parcel)
}
override fun newArray(size: Int): Array<ConfigurationOverride?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,38 @@
package com.github.kr328.clash.core.model
import android.os.Parcel
import android.os.Parcelable
import com.github.kr328.clash.core.util.Parcelizer
import kotlinx.serialization.Serializable
@Serializable
data class FetchStatus(
val action: Action,
val args: List<String>,
val progress: Int,
val max: Int
) : Parcelable {
enum class Action {
FetchConfiguration,
FetchProviders,
Verifying,
}
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
Parcelizer.encodeToParcel(serializer(), dest, this)
}
companion object CREATOR : Parcelable.Creator<FetchStatus> {
override fun createFromParcel(parcel: Parcel): FetchStatus {
return Parcelizer.decodeFromParcel(serializer(), parcel)
}
override fun newArray(size: Int): Array<FetchStatus?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,56 @@
@file:UseSerializers(DateSerializer::class)
package com.github.kr328.clash.core.model
import android.os.Parcel
import android.os.Parcelable
import com.github.kr328.clash.core.util.DateSerializer
import com.github.kr328.clash.core.util.Parcelizer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import java.util.*
@Serializable
data class LogMessage(
val level: Level,
val message: String,
val time: Date,
) : Parcelable {
@Serializable
enum class Level {
@SerialName("debug")
Debug,
@SerialName("info")
Info,
@SerialName("warning")
Warning,
@SerialName("error")
Error,
@SerialName("silent")
Silent,
@SerialName("unknown")
Unknown,
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
Parcelizer.encodeToParcel(serializer(), parcel, this)
}
override fun describeContents(): Int {
return 0
}
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<LogMessage> {
override fun createFromParcel(parcel: Parcel): LogMessage {
return Parcelizer.decodeFromParcel(serializer(), parcel)
}
override fun newArray(size: Int): Array<LogMessage?> {
return arrayOfNulls(size)
}
}
}
}

View File

@@ -0,0 +1,44 @@
package com.github.kr328.clash.core.model
import android.os.Parcel
import android.os.Parcelable
import com.github.kr328.clash.core.util.Parcelizer
import kotlinx.serialization.Serializable
@Serializable
data class Provider(
val name: String,
val type: Type,
val vehicleType: VehicleType,
val updatedAt: Long
) : Parcelable, Comparable<Provider> {
enum class Type {
Proxy, Rule
}
enum class VehicleType {
HTTP, File, Compatible
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
Parcelizer.encodeToParcel(serializer(), parcel, this)
}
override fun describeContents(): Int {
return 0
}
override fun compareTo(other: Provider): Int {
return compareValuesBy(this, other, Provider::type, Provider::name)
}
companion object CREATOR : Parcelable.Creator<Provider> {
override fun createFromParcel(parcel: Parcel): Provider {
return Parcelizer.decodeFromParcel(serializer(), parcel)
}
override fun newArray(size: Int): Array<Provider?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,28 @@
package com.github.kr328.clash.core.model
import android.os.Parcel
import android.os.Parcelable
import com.github.kr328.clash.common.util.createListFromParcelSlice
import com.github.kr328.clash.common.util.writeToParcelSlice
class ProviderList(data: List<Provider>) : List<Provider> by data, Parcelable {
constructor(parcel: Parcel) : this(Provider.createListFromParcelSlice(parcel, 0, 20))
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
return writeToParcelSlice(parcel, flags)
}
companion object CREATOR : Parcelable.Creator<ProviderList> {
override fun createFromParcel(parcel: Parcel): ProviderList {
return ProviderList(parcel)
}
override fun newArray(size: Int): Array<ProviderList?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,55 @@
package com.github.kr328.clash.core.model
import android.os.Parcel
import android.os.Parcelable
import com.github.kr328.clash.core.util.Parcelizer
import kotlinx.serialization.Serializable
@Serializable
data class Proxy(
val name: String,
val title: String,
val subtitle: String,
val type: Type,
val delay: Int,
) : Parcelable {
@Suppress("unused")
enum class Type(val group: Boolean) {
Direct(false),
Reject(false),
Shadowsocks(false),
ShadowsocksR(false),
Snell(false),
Socks5(false),
Http(false),
Vmess(false),
Trojan(false),
Relay(true),
Selector(true),
Fallback(true),
URLTest(true),
LoadBalance(true),
Unknown(false);
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
Parcelizer.encodeToParcel(serializer(), parcel, this)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<Proxy> {
override fun createFromParcel(parcel: Parcel): Proxy {
return Parcelizer.decodeFromParcel(serializer(), parcel)
}
override fun newArray(size: Int): Array<Proxy?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,62 @@
package com.github.kr328.clash.core.model
import android.os.Parcel
import android.os.Parcelable
import com.github.kr328.clash.common.util.createListFromParcelSlice
import com.github.kr328.clash.common.util.writeToParcelSlice
import kotlinx.serialization.Serializable
@Serializable
data class ProxyGroup(
val type: Proxy.Type,
val proxies: List<Proxy>,
val now: String,
) : Parcelable {
class SliceProxyList(data: List<Proxy>) : List<Proxy> by data, Parcelable {
constructor(parcel: Parcel) : this(Proxy.createListFromParcelSlice(parcel, 0, 50))
override fun describeContents(): Int {
return 0
}
override fun writeToParcel(dest: Parcel, flags: Int) {
writeToParcelSlice(dest, flags)
}
companion object CREATOR : Parcelable.Creator<SliceProxyList> {
override fun createFromParcel(parcel: Parcel): SliceProxyList {
return SliceProxyList(parcel)
}
override fun newArray(size: Int): Array<SliceProxyList?> {
return arrayOfNulls(size)
}
}
}
constructor(parcel: Parcel) : this(
Proxy.Type.values()[parcel.readInt()],
SliceProxyList(parcel),
parcel.readString()!!,
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(type.ordinal)
SliceProxyList(proxies).writeToParcel(parcel, 0)
parcel.writeString(now)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<ProxyGroup> {
override fun createFromParcel(parcel: Parcel): ProxyGroup {
return ProxyGroup(parcel)
}
override fun newArray(size: Int): Array<ProxyGroup?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,5 @@
package com.github.kr328.clash.core.model
enum class ProxySort {
Default, Title, Delay
}

View File

@@ -0,0 +1,3 @@
package com.github.kr328.clash.core.model
typealias Traffic = Long

View File

@@ -0,0 +1,45 @@
package com.github.kr328.clash.core.model
import android.os.Parcel
import android.os.Parcelable
import com.github.kr328.clash.core.util.Parcelizer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class TunnelState(
val mode: Mode,
) : Parcelable {
@Serializable
enum class Mode {
@SerialName("direct")
Direct,
@SerialName("global")
Global,
@SerialName("rule")
Rule,
@SerialName("script")
Script,
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
Parcelizer.encodeToParcel(serializer(), parcel, this)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<TunnelState> {
override fun createFromParcel(parcel: Parcel): TunnelState {
return Parcelizer.decodeFromParcel(serializer(), parcel)
}
override fun newArray(size: Int): Array<TunnelState?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,27 @@
package com.github.kr328.clash.core.model
import android.os.Parcel
import android.os.Parcelable
import com.github.kr328.clash.core.util.Parcelizer
import kotlinx.serialization.Serializable
@Serializable
class UiConfiguration : Parcelable {
override fun writeToParcel(parcel: Parcel, flags: Int) {
Parcelizer.encodeToParcel(serializer(), parcel, this)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<UiConfiguration> {
override fun createFromParcel(parcel: Parcel): UiConfiguration {
return Parcelizer.decodeFromParcel(serializer(), parcel)
}
override fun newArray(size: Int): Array<UiConfiguration?> {
return arrayOfNulls(size)
}
}
}

View File

@@ -0,0 +1,11 @@
package com.github.kr328.clash.core.util
import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.URL
fun parseInetSocketAddress(address: String): InetSocketAddress {
val url = URL("https://$address")
return InetSocketAddress(InetAddress.getByName(url.host), url.port)
}

View File

@@ -0,0 +1,258 @@
package com.github.kr328.clash.core.util
import android.os.Parcel
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationStrategy
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.CompositeEncoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.modules.EmptySerializersModule
import kotlinx.serialization.modules.SerializersModule
object Parcelizer {
private class ParcelDecoder(private val parcel: Parcel) : Decoder, CompositeDecoder {
@ExperimentalSerializationApi
override val serializersModule: SerializersModule = EmptySerializersModule
@ExperimentalSerializationApi
override fun decodeSequentially(): Boolean = true
override fun decodeByteElement(descriptor: SerialDescriptor, index: Int) = decodeByte()
override fun decodeCharElement(descriptor: SerialDescriptor, index: Int) = decodeChar()
override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) = decodeDouble()
override fun decodeElementIndex(descriptor: SerialDescriptor) = decodeInt()
override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int) = decodeFloat()
override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int) =
decodeBoolean()
@ExperimentalSerializationApi
override fun decodeInlineElement(descriptor: SerialDescriptor, index: Int): Decoder {
return this
}
override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) = decodeInt()
override fun decodeLongElement(descriptor: SerialDescriptor, index: Int) = decodeLong()
override fun decodeShortElement(descriptor: SerialDescriptor, index: Int) = decodeShort()
override fun decodeStringElement(descriptor: SerialDescriptor, index: Int) = decodeString()
@ExperimentalSerializationApi
override fun <T : Any> decodeNullableSerializableElement(
descriptor: SerialDescriptor,
index: Int,
deserializer: DeserializationStrategy<T?>,
previousValue: T?
): T? = decodeNullableSerializableValue(deserializer)
override fun <T> decodeSerializableElement(
descriptor: SerialDescriptor,
index: Int,
deserializer: DeserializationStrategy<T>,
previousValue: T?
): T = decodeSerializableValue(deserializer)
override fun endStructure(descriptor: SerialDescriptor) {
}
override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder {
return this
}
override fun decodeCollectionSize(descriptor: SerialDescriptor): Int {
return decodeInt()
}
override fun decodeBoolean(): Boolean {
return decodeByte() != (0.toByte())
}
override fun decodeByte(): Byte {
return parcel.readByte()
}
override fun decodeChar(): Char {
return decodeInt().toChar()
}
override fun decodeDouble(): Double {
return parcel.readDouble()
}
override fun decodeEnum(enumDescriptor: SerialDescriptor): Int {
return decodeInt()
}
override fun decodeFloat(): Float {
return parcel.readFloat()
}
@ExperimentalSerializationApi
override fun decodeInline(inlineDescriptor: SerialDescriptor): Decoder {
return this
}
override fun decodeInt(): Int {
return parcel.readInt()
}
override fun decodeLong(): Long {
return parcel.readLong()
}
@ExperimentalSerializationApi
override fun decodeNotNullMark(): Boolean {
return decodeBoolean()
}
@ExperimentalSerializationApi
override fun decodeNull(): Nothing? {
return null
}
override fun decodeShort(): Short {
return decodeInt().toShort()
}
override fun decodeString(): String {
return parcel.readString()!!
}
}
private class ParcelEncoder(private val parcel: Parcel) : Encoder, CompositeEncoder {
@ExperimentalSerializationApi
override val serializersModule: SerializersModule = EmptySerializersModule
override fun encodeBooleanElement(
descriptor: SerialDescriptor,
index: Int,
value: Boolean
) = encodeBoolean(value)
override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) =
encodeByte(value)
override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) =
encodeChar(value)
override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) =
encodeDouble(value)
override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) =
encodeFloat(value)
@ExperimentalSerializationApi
override fun encodeInlineElement(descriptor: SerialDescriptor, index: Int): Encoder {
return this
}
override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) =
encodeInt(value)
override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) =
encodeLong(value)
override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) =
encodeShort(value)
override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) =
encodeString(value)
@ExperimentalSerializationApi
override fun <T : Any> encodeNullableSerializableElement(
descriptor: SerialDescriptor,
index: Int,
serializer: SerializationStrategy<T>,
value: T?
) = encodeNullableSerializableValue(serializer, value)
override fun <T> encodeSerializableElement(
descriptor: SerialDescriptor,
index: Int,
serializer: SerializationStrategy<T>,
value: T
) = encodeSerializableValue(serializer, value)
override fun endStructure(descriptor: SerialDescriptor) {
}
override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder {
return this
}
override fun beginCollection(
descriptor: SerialDescriptor,
collectionSize: Int
): CompositeEncoder {
encodeInt(collectionSize)
return super.beginCollection(descriptor, collectionSize)
}
override fun encodeBoolean(value: Boolean) {
encodeByte(if (value) 1 else 0)
}
override fun encodeByte(value: Byte) {
parcel.writeByte(value)
}
override fun encodeChar(value: Char) {
encodeInt(value.code)
}
override fun encodeDouble(value: Double) {
parcel.writeDouble(value)
}
override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) {
encodeInt(index)
}
override fun encodeFloat(value: Float) {
parcel.writeFloat(value)
}
@ExperimentalSerializationApi
override fun encodeInline(inlineDescriptor: SerialDescriptor): Encoder {
return this
}
override fun encodeInt(value: Int) {
parcel.writeInt(value)
}
override fun encodeLong(value: Long) {
parcel.writeLong(value)
}
@ExperimentalSerializationApi
override fun encodeNull() {
encodeBoolean(false)
}
override fun encodeShort(value: Short) {
encodeInt(value.toInt())
}
override fun encodeString(value: String) {
parcel.writeString(value)
}
@ExperimentalSerializationApi
override fun encodeNotNullMark() {
encodeBoolean(true)
}
}
fun <T> decodeFromParcel(deserializer: DeserializationStrategy<T>, parcel: Parcel): T {
return deserializer.deserialize(ParcelDecoder(parcel))
}
fun <T> encodeToParcel(serializer: SerializationStrategy<T>, parcel: Parcel, value: T) {
serializer.serialize(ParcelEncoder(parcel), value)
}
}

View File

@@ -0,0 +1,22 @@
package com.github.kr328.clash.core.util
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import java.util.*
object DateSerializer : KSerializer<Date> {
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("Date", PrimitiveKind.LONG)
override fun deserialize(decoder: Decoder): Date {
return Date(decoder.decodeLong())
}
override fun serialize(encoder: Encoder, value: Date) {
encoder.encodeLong(value.time)
}
}

View File

@@ -0,0 +1,54 @@
package com.github.kr328.clash.core.util
import com.github.kr328.clash.core.model.Traffic
fun Traffic.trafficUpload(): String {
return trafficString(scaleTraffic(this ushr 32))
}
fun Traffic.trafficDownload(): String {
return trafficString(scaleTraffic(this and 0xFFFFFFFF))
}
fun Traffic.trafficTotal(): String {
val upload = scaleTraffic(this ushr 32)
val download = scaleTraffic(this and 0xFFFFFFFF)
return trafficString(upload + download)
}
private fun trafficString(scaled: Long): String {
return when {
scaled > 1024 * 1024 * 1024 * 100L -> {
val data = scaled / 1024 / 1024 / 1024
"${data / 100}.${data % 100} GiB"
}
scaled > 1024 * 1024 * 100L -> {
val data = scaled / 1024 / 1024
"${data / 100}.${data % 100} MiB"
}
scaled > 1024 * 100L -> {
val data = scaled / 1024
"${data / 100}.${data % 100} KiB"
}
else -> {
"$scaled Bytes"
}
}
}
private fun scaleTraffic(value: Long): Long {
val type = (value ushr 30) and 0x3
val data = value and 0x3FFFFFFF
return when (type) {
0L -> data
1L -> data * 1024
2L -> data * 1024 * 1024
3L -> data * 1024 * 1024 * 1024
else -> throw IllegalArgumentException("invalid value type")
}
}