-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #36 from compscidr/jason/tun-tap
Tun tap implementation, refactor state machine
- Loading branch information
Showing
44 changed files
with
3,485 additions
and
3,326 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,8 +34,10 @@ jobs: | |
- name: Tar Reports | ||
if: always() | ||
run: | | ||
mkdir -p kanonproxy/build/reports/ && | ||
tar -czvf kanonproxy-reports.tar.gz -C kanonproxy/build reports | ||
pwd | ||
ls -la | ||
mkdir -p ./core/build/reports/ && | ||
tar -czvf kanonproxy-reports.tar.gz -C core/build reports | ||
- name: Upload Reports | ||
uses: actions/[email protected] | ||
if: always() | ||
|
@@ -48,4 +50,14 @@ jobs: | |
with: | ||
token: ${{ secrets.CODECOV_TOKEN }} | ||
flags: libunittests | ||
files: ./kanonproxy/build/reports/jacoco/test/jacocoTestReport.xml | ||
files: ./core/build/reports/jacoco/test/jacocoTestReport.xml | ||
# TODO: need to debug why creating the tun/tap is failing - perhaps not possible on GH runners | ||
# - name: End to End Tests | ||
# run: | | ||
# bash client/scripts/tuntap.sh $USER | ||
# ./gradlew :client:run & | ||
# ./gradlew :server:run & | ||
# sleep 5 | ||
# ping -I kanon -c 5 8.8.8.8 | ||
# curl --interface kanon https://www.google.com | ||
# bash client/scripts/cleanup.sh |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,105 +1,5 @@ | ||
plugins { | ||
alias(libs.plugins.jetbrains.kotlin.jvm) | ||
alias(libs.plugins.kotlinter) | ||
id("java-library") | ||
id("jacoco") | ||
alias(libs.plugins.git.version) | ||
alias(libs.plugins.sonatype.maven.central) | ||
alias(libs.plugins.gradleup.nmcp) | ||
alias(libs.plugins.jetbrains.kotlin.jvm) apply false | ||
alias(libs.plugins.kotlinter) apply false | ||
} | ||
|
||
java { | ||
sourceCompatibility = JavaVersion.VERSION_17 | ||
targetCompatibility = JavaVersion.VERSION_17 | ||
} | ||
|
||
kotlin { | ||
jvmToolchain(17) | ||
} | ||
|
||
tasks.jacocoTestReport { | ||
reports { | ||
xml.required = true | ||
html.required = true | ||
} | ||
} | ||
|
||
tasks.withType<Test>().configureEach { | ||
useJUnitPlatform() | ||
finalizedBy("jacocoTestReport") | ||
testLogging { | ||
// get the test stdout / stderr to show up when we run gradle from command line | ||
// https://itecnote.com/tecnote/gradle-how-to-get-output-from-test-stderr-stdout-into-console/ | ||
// https://developer.android.com/studio/test/advanced-test-setup | ||
// https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/testing/Test.html | ||
outputs.upToDateWhen {true} | ||
showStandardStreams = true | ||
} | ||
} | ||
|
||
jacoco { | ||
toolVersion = "0.8.12" | ||
} | ||
|
||
dependencies { | ||
api(libs.slf4j.api) | ||
api(libs.knet) | ||
testImplementation(libs.icmp.linux) | ||
testImplementation(libs.bundles.test) | ||
testRuntimeOnly(libs.junit.jupiter.engine) | ||
testImplementation(libs.logback.classic) | ||
testImplementation(libs.testservers) | ||
implementation(kotlin("stdlib")) | ||
} | ||
|
||
version = "0.0.0-SNAPSHOT" | ||
gitVersioning.apply { | ||
refs { | ||
branch(".+") { version = "\${ref}-SNAPSHOT" } | ||
tag("v(?<version>.*)") { version = "\${ref.version}" } | ||
} | ||
} | ||
|
||
// see: https://github.com/vanniktech/gradle-maven-publish-plugin/issues/747#issuecomment-2066762725 | ||
// and: https://github.com/GradleUp/nmcp | ||
nmcp { | ||
val props = project.properties | ||
publishAllPublications { | ||
username = props["centralPortalToken"] as String? ?: "" | ||
password = props["centralPortalPassword"] as String? ?: "" | ||
// or if you want to publish automatically | ||
publicationType = "AUTOMATIC" | ||
} | ||
} | ||
|
||
// see: https://vanniktech.github.io/gradle-maven-publish-plugin/central/#configuring-the-pom | ||
mavenPublishing { | ||
coordinates("com.jasonernst.kanonproxy", "kanonproxy", version.toString()) | ||
pom { | ||
name = "kanonproxy" | ||
description = "An anonymous proxy written in kotlin." | ||
inceptionYear = "2024" | ||
url = "https://github.com/compscidr/kanonproxynet" | ||
licenses { | ||
license { | ||
name = "GPL-3.0" | ||
url = "https://www.gnu.org/licenses/gpl-3.0.en.html" | ||
distribution = "repo" | ||
} | ||
} | ||
developers { | ||
developer { | ||
id = "compscidr" | ||
name = "Jason Ernst" | ||
url = "https://www.jasonernst.com" | ||
} | ||
} | ||
scm { | ||
url = "https://github.com/compscidr/kanonproxy" | ||
connection = "scm:git:git://github.com/compscidr/kanonproxy.git" | ||
developerConnection = "scm:git:ssh://[email protected]/compscidr/kanonproxy.git" | ||
} | ||
} | ||
|
||
signAllPublications() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
plugins { | ||
alias(libs.plugins.jetbrains.kotlin.jvm) | ||
alias(libs.plugins.kotlinter) | ||
id("application") | ||
id("jacoco") | ||
} | ||
|
||
java { | ||
sourceCompatibility = JavaVersion.VERSION_17 | ||
targetCompatibility = JavaVersion.VERSION_17 | ||
} | ||
|
||
kotlin { | ||
jvmToolchain(17) | ||
} | ||
|
||
jacoco { | ||
toolVersion = "0.8.12" | ||
} | ||
|
||
application { | ||
mainClass = "com.jasonernst.kanonproxy.Client" | ||
} | ||
|
||
dependencies { | ||
implementation(libs.jna) | ||
implementation(libs.jnr.enxio) | ||
implementation(libs.knet) | ||
runtimeOnly(libs.logback.classic) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
#!/bin/bash | ||
# Kill and java -jar invoked processes, delete kanon namespace, delete kanon interface | ||
sudo pkill -f "java -jar" | ||
sudo pkill -f "java -Djava.library.path" | ||
{ | ||
# try to cleanup old interfaces first | ||
# hide output of these - we'll get a failure message if the bump interface doesn't exist | ||
# and we don't care if it doesn't | ||
sudo ip link set dev kanon down | ||
sudo ip tuntap del dev kanon mode tun | ||
rm *.jar *.so | ||
} &> /dev/null |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
#!/bin/bash | ||
# usage: ./tun-tap <USERNAME> | ||
if [ -z "$1" ]; then | ||
echo "usage: ./tun-tap <USERNAME>" | ||
exit 22 | ||
fi | ||
{ | ||
# try to cleanup old interfaces first | ||
# hide output of these - we'll get a failure message if the kanon interface doesn't exist | ||
# and we don't care if it doesn't | ||
sudo ip link set dev kanon down | ||
sudo ip tuntap del dev kanon mode tun | ||
} &> /dev/null | ||
|
||
# exit when any command fails | ||
set -e | ||
|
||
sudo ip tuntap add dev kanon mode tun user $1 group $1 | ||
sudo ip addr add 10.0.1.1/24 dev kanon | ||
sudo ip link set dev kanon up mtu 1024 |
136 changes: 136 additions & 0 deletions
136
client/src/main/kotlin/com/jasonernst/kanonproxy/Client.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package com.jasonernst.kanonproxy | ||
|
||
import com.jasonernst.kanonproxy.tuntap.TunTapDevice | ||
import com.jasonernst.knet.Packet | ||
import com.jasonernst.knet.Packet.Companion.parseStream | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.Dispatchers | ||
import kotlinx.coroutines.SupervisorJob | ||
import kotlinx.coroutines.launch | ||
import kotlinx.coroutines.runBlocking | ||
import org.slf4j.LoggerFactory | ||
import java.net.DatagramPacket | ||
import java.net.DatagramSocket | ||
import java.net.InetSocketAddress | ||
import java.nio.ByteBuffer | ||
import java.util.concurrent.atomic.AtomicBoolean | ||
import kotlin.math.min | ||
|
||
class Client( | ||
private val socketAddress: InetSocketAddress = InetSocketAddress("127.0.0.1", 8080), | ||
) { | ||
private val logger = LoggerFactory.getLogger(javaClass) | ||
private val socket = DatagramSocket() | ||
private val tunTapDevice = TunTapDevice() | ||
|
||
private val isConnected = AtomicBoolean(false) | ||
|
||
private val readFromTunJob = SupervisorJob() | ||
private val readFromTunJobScope = CoroutineScope(Dispatchers.IO + readFromTunJob) | ||
private val readFromProxyJob = SupervisorJob() | ||
private val readFromProxyJobScope = CoroutineScope(Dispatchers.IO + readFromProxyJob) | ||
|
||
companion object { | ||
private const val MAX_STREAM_BUFFER_SIZE = 1048576 // max we can write into the stream without parsing | ||
private const val MAX_RECEIVE_BUFFER_SIZE = 1500 // max amount we can recv in one read (should be the MTU or bigger probably) | ||
|
||
@JvmStatic | ||
fun main(args: Array<String>) { | ||
val client = | ||
if (args.isEmpty()) { | ||
println("Using default server: 127.0.0.1 8080") | ||
Client() | ||
} else { | ||
if (args.size != 2) { | ||
println("Usage: Client <server> <port>") | ||
return | ||
} | ||
val server = args[0] | ||
val port = args[1].toInt() | ||
Client(InetSocketAddress(server, port)) | ||
} | ||
client.connect() | ||
} | ||
} | ||
|
||
fun connect() { | ||
if (isConnected.get()) { | ||
println("Client is already connected") | ||
return | ||
} | ||
|
||
println("Connecting to server: $socketAddress") | ||
socket.connect(socketAddress) | ||
println("Connected to server: $socketAddress") | ||
isConnected.set(true) | ||
tunTapDevice.open() | ||
|
||
readFromProxyJobScope.launch { | ||
readFromProxyWriteToTun() | ||
} | ||
|
||
readFromTunJobScope.launch { | ||
readFromTunWriteToProxy() | ||
} | ||
|
||
// block until the read job is finished | ||
runBlocking { | ||
readFromProxyJob.join() | ||
readFromTunJob.join() | ||
} | ||
} | ||
|
||
private fun readFromProxyWriteToTun() { | ||
val buffer = ByteArray(MAX_RECEIVE_BUFFER_SIZE) | ||
val datagram = DatagramPacket(buffer, buffer.size) | ||
val stream = ByteBuffer.allocate(MAX_STREAM_BUFFER_SIZE) | ||
|
||
while (isConnected.get()) { | ||
logger.debug("Waiting for response from server") | ||
socket.receive(datagram) | ||
stream.put(buffer, 0, datagram.length) | ||
stream.flip() | ||
val packets = parseStream(stream) | ||
for (packet in packets) { | ||
tunTapDevice.write(ByteBuffer.wrap(packet.toByteArray())) | ||
} | ||
} | ||
logger.warn("No longer reading from server") | ||
} | ||
|
||
private fun writePackets(packets: List<Packet>) { | ||
packets.forEach { packet -> | ||
val buffer = packet.toByteArray() | ||
val datagramPacket = DatagramPacket(buffer, buffer.size, socketAddress) | ||
socket.send(datagramPacket) | ||
} | ||
} | ||
|
||
private fun readFromTunWriteToProxy() { | ||
val readBuffer = ByteArray(MAX_RECEIVE_BUFFER_SIZE) | ||
val stream = ByteBuffer.allocate(MAX_STREAM_BUFFER_SIZE) | ||
|
||
while (isConnected.get()) { | ||
val bytesToRead = min(MAX_RECEIVE_BUFFER_SIZE, stream.remaining()) | ||
val bytesRead = tunTapDevice.read(readBuffer, bytesToRead) | ||
if (bytesRead == -1) { | ||
logger.warn("End of OS stream") | ||
break | ||
} | ||
if (bytesRead > 0) { | ||
stream.put(readBuffer, 0, bytesRead) | ||
logger.debug("Read {} bytes from OS. position: {} remaining {}", bytesRead, stream.position(), stream.remaining()) | ||
stream.flip() | ||
logger.debug("After flip: position: {} remaining {}", stream.position(), stream.remaining()) | ||
val packets = parseStream(stream) | ||
logger.debug("After parse: position: {} remaining {}", stream.position(), stream.remaining()) | ||
writePackets(packets) | ||
} | ||
} | ||
logger.warn("No longer reading from TUN adapter") | ||
} | ||
|
||
fun close() { | ||
TODO() | ||
} | ||
} |
39 changes: 39 additions & 0 deletions
39
client/src/main/kotlin/com/jasonernst/kanonproxy/tuntap/IfReq.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package com.jasonernst.kanonproxy.tuntap | ||
|
||
import com.sun.jna.Native | ||
import com.sun.jna.Structure | ||
import org.slf4j.LoggerFactory | ||
import java.nio.charset.StandardCharsets | ||
|
||
@Structure.FieldOrder("name", "flags", "padding") | ||
class IfReq( | ||
@JvmField var flags: Short = 0, | ||
nameString: String = "", | ||
) : Structure() { | ||
companion object { | ||
// from if.h - the max length of an interface name | ||
const val IF_NAME_LENGTH: Int = 16 | ||
const val IF_REQ_LENGTH: Int = 40 | ||
} | ||
|
||
private val logger = LoggerFactory.getLogger(javaClass) | ||
|
||
@JvmField var name: ByteArray = ByteArray(IF_NAME_LENGTH) | ||
|
||
@JvmField var padding: ByteArray = ByteArray(IF_REQ_LENGTH - IF_NAME_LENGTH - 2) | ||
|
||
init { | ||
val nameBytes = nameString.toByteArray(StandardCharsets.US_ASCII) | ||
if (nameBytes.size > IF_NAME_LENGTH) { | ||
logger.warn("Interface name is too long, truncating to $IF_NAME_LENGTH characters") | ||
nameBytes.copyInto(this.name, 0, 0, IF_NAME_LENGTH) | ||
} else { | ||
nameBytes.copyInto(this.name, 0, 0, nameBytes.size) | ||
} | ||
} | ||
// there are actually other fields in here, but we don't need them to set this as a TAP | ||
// device, we just calling it "padding": https://github.com/spotify/linux/blob/master/include/linux/if.h#L172 | ||
|
||
override fun toString(): String = | ||
"IfReq(name=${Native.toString(name, StandardCharsets.US_ASCII)}, flags=$flags, padding length=${padding.size})" | ||
} |
Oops, something went wrong.