Skip to content

Commit

Permalink
open sourcing android part
Browse files Browse the repository at this point in the history
  • Loading branch information
eycorsican committed Nov 13, 2018
1 parent c2fec94 commit 6ae4ffe
Show file tree
Hide file tree
Showing 35 changed files with 1,382 additions and 7 deletions.
11 changes: 4 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Kitsunebi

Not yet ready for opensourcing.
A fully-featured V2Ray client for Android.

## 下载

Expand All @@ -14,9 +14,9 @@ Github Releases: https://github.com/eycorsican/Kitsunebi4Android/releases
- 把配置文件复制粘贴至主界面后,点击连接按钮即可启动
- 如果配置文件不正确或者出错,通常不会有任何错误提示
- 配置文件可使用一个常见的 V2Ray 配置
- 配置文件的 freedom outbound 需要使用 [`UseIP` 策略](https://www.v2ray.com/chapter_02/protocols/freedom.html#outboundconfigurationobject)
- 配置文件的 freedom outbound 推荐使用 [`UseIP` 策略](https://www.v2ray.com/chapter_02/protocols/freedom.html#outboundconfigurationobject)
- 配置文件不需要有 Inbound,app 使用了 `tun2socks` 作为 inbound,并已开启 [http,tls 流量嗅探](https://www.v2ray.com/chapter_02/01_overview.html#sniffingobject)
- 如果 freedom outbound 正确配置了 `UseIP`设备所有 DNS 请求均由 V2Ray 的 [DNS 服务器](https://www.v2ray.com/chapter_02/04_dns.html)来解析,正确设置了 DNS 服务器可以避免 DNS 污染以及 CDN 相关的 DNS 问题
- 设备所有 DNS 请求均会由 V2Ray 的 [DNS 服务器](https://www.v2ray.com/chapter_02/04_dns.html) 来解析,正确设置了 DNS 服务器可以避免 DNS 污染以及 CDN 相关的 DNS 问题
- 目前,非 VMess 协议的 outbound 不可以用域名来做服务器地址,必须要用 IP
- 下面是一个示例配置:
```json
Expand All @@ -32,10 +32,7 @@ Github Releases: https://github.com/eycorsican/Kitsunebi4Android/releases
"dns": {
"clientIP": "115.239.211.92", # 你的对外地址(或者随便找个同地区的 IP),用来提示 DNS 服务器返回合适的 IP
"hosts": {
"localhost": "127.0.0.1",
"domain:lan": "127.0.0.1",
"domain:local": "127.0.0.1",
"domain:arpa": "127.0.0.1"
"localhost": "127.0.0.1"
},
"servers": [
"8.8.8.8",
Expand Down
1 change: 1 addition & 0 deletions app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
44 changes: 44 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
compileSdkVersion 28
defaultConfig {
applicationId 'fun.kitsunebi.kitsunebi4android'
minSdkVersion 17
targetSdkVersion 28
versionCode 2
versionName "0.2.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
productFlavors {
}
}

dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
implementation 'com.android.support:design:28.0.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
implementation 'com.beust:klaxon:3.0.1'
implementation(name: 'tun2socks', ext: 'aar')
}

repositories {
flatDir {
dirs 'libs'
}
}
21 changes: 21 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package `fun`.kitsunebi.kitsunebi4android

import android.support.test.InstrumentationRegistry
import android.support.test.runner.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getTargetContext()
assertEquals("fun.kitsunebi.kitsunebi4android", appContext.packageName)
}
}
35 changes: 35 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="fun.kitsunebi.kitsunebi4android">

<uses-permission android:name="android.permission.INTERNET"/>

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name="fun.kitsunebi.kitsunebi4android.MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<service
android:name="fun.kitsunebi.kitsunebi4android.KitsunebiVpnService"
android:permission="android.permission.BIND_VPN_SERVICE" >
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package `fun`.kitsunebi.kitsunebi4android

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.VpnService
import android.os.Build
import android.os.ParcelFileDescriptor
import tun2socks.PacketFlow
import tun2socks.VpnService as Tun2socksVpnService
import kotlin.concurrent.thread
import com.beust.klaxon.Klaxon
import java.io.FileInputStream
import java.io.FileOutputStream
import java.net.InetAddress
import java.nio.ByteBuffer

open class KitsunebiVpnService: VpnService() {
var configString: String = ""
var proxyDomainIPMap: HashMap<String, String> = HashMap<String, String>()
var pfd: ParcelFileDescriptor? = null
var inputStream: FileInputStream? = null
var outputStream: FileOutputStream? = null
var buffer = ByteBuffer.allocate(1501)
var isStopped = false

data class Config(val outbounds: List<Outbound>?)
data class Outbound(val protocol: String = "", val settings: Settings? = null)
data class Settings(val vnext: List<Server?>? = emptyList())
data class Server(val address: String? = null)

val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(contxt: Context?, intent: Intent?) {
when (intent?.action) {
"stop_vpn" -> {
stopVPN()
}
"ping" -> {
if (!isStopped) {
sendBroadcast(Intent("pong"))
}
}

}
}
}

private fun stopVPN() {
isStopped = true
tun2socks.Tun2socks.stopV2Ray()
pfd?.close()
pfd = null
inputStream = null
outputStream = null
sendBroadcast(Intent("vpn_stopped"))
stopSelf()
}

class Flow(stream: FileOutputStream?): PacketFlow {
val flowOutputStream = stream
override fun writePacket(pkt: ByteArray?) {
flowOutputStream?.write(pkt)
}
}

class Service(service: VpnService): Tun2socksVpnService {
val vpnService = service
override fun protect(fd: Long) {
vpnService.protect(fd.toInt())
}
}

fun handlePackets() {
while (!isStopped) {
val n = inputStream?.read(buffer.array())
n?.let { it } ?: return
if (n > 0) {
buffer.limit(n)
tun2socks.Tun2socks.inputPacket(buffer.array())
buffer.clear()
}
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
// In non-blocking mode
Thread.sleep(50)
}
}
}

override fun onCreate() {
super.onCreate()
registerReceiver(broadcastReceiver, IntentFilter("stop_vpn"))
registerReceiver(broadcastReceiver, IntentFilter("ping"))
}

override public fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
configString = intent?.extras?.get("config").toString()

thread (start = true) {
var error = false

val config = Klaxon().parse<Config>(configString)
if (config != null) {
config.outbounds?.forEach {
if (it.protocol == "vmess") {
it.settings?.vnext?.forEach {
if (it != null && it.address != null) {
println("vmess server address: ${it.address}")
try {
val addr = InetAddress.getByName(it.address)
val ip = addr.getHostAddress()
if (it.address != ip) {
// address is a domain name
proxyDomainIPMap.put(it.address, ip)
}
} catch (e: Exception) {
error = true
}
}
}
}
}
} else {
println("parsing v2ray config failed")
error = true
}

if (error) {
sendBroadcast(Intent("vpn_start_err"))
return@thread
}

val builder = Builder()
builder.setSession("vv")
.setMtu(1500)
.addAddress("10.233.233.233", 30)
.addDnsServer("223.5.5.5")
.addSearchDomain("local")
.addRoute("0.0.0.0", 0)
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
builder.setBlocking(true)
}
pfd = builder.establish()

inputStream = FileInputStream(pfd?.fileDescriptor)
outputStream = FileOutputStream(pfd?.fileDescriptor)

val flow = Flow(outputStream)
val service = Service(this)

val files = filesDir.list()
if (!files.contains("geoip.dat") || !files.contains("geosite.dat")) {
val geoipBytes = resources.openRawResource(R.raw.geoip).readBytes()
val fos = openFileOutput("geoip.dat", Context.MODE_PRIVATE)
fos.write(geoipBytes)
fos.close()

val geositeBytes = resources.openRawResource(R.raw.geosite).readBytes()
val fos2 = openFileOutput("geosite.dat", Context.MODE_PRIVATE)
fos2.write(geositeBytes)
fos2.close()
}
val serverDomains = proxyDomainIPMap.keys.joinToString(separator = ",")
val serverIPs = proxyDomainIPMap.values.joinToString(separator = ",")
tun2socks.Tun2socks.startV2Ray(flow, service, configString.toByteArray(), filesDir.absolutePath, serverDomains, serverIPs)

sendBroadcast(Intent("vpn_started"))

handlePackets()
}

return START_STICKY
}

override fun onDestroy() {
super.onDestroy()
unregisterReceiver(broadcastReceiver)
}
}
Loading

0 comments on commit 6ae4ffe

Please sign in to comment.