Skip to content

Commit

Permalink
Merge pull request #36 from compscidr/jason/tun-tap
Browse files Browse the repository at this point in the history
Tun tap implementation, refactor state machine
  • Loading branch information
compscidr authored Nov 19, 2024
2 parents 2eadf37 + 00e9e29 commit 4438003
Show file tree
Hide file tree
Showing 44 changed files with 3,485 additions and 3,326 deletions.
18 changes: 15 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
104 changes: 2 additions & 102 deletions build.gradle.kts
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()
}
30 changes: 30 additions & 0 deletions client/build.gradle.kts
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)
}
12 changes: 12 additions & 0 deletions client/scripts/cleanup.sh
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
20 changes: 20 additions & 0 deletions client/scripts/tuntap.sh
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 client/src/main/kotlin/com/jasonernst/kanonproxy/Client.kt
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 client/src/main/kotlin/com/jasonernst/kanonproxy/tuntap/IfReq.kt
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})"
}
Loading

0 comments on commit 4438003

Please sign in to comment.