Skip to content

Commit

Permalink
Better ipv6 support (#173)
Browse files Browse the repository at this point in the history
* utility to map ipv4 address into ipv6 space

* respond to AAAA queries: map ziti IPv4 addresses into IPv6 space

* ZME nameserver: listen on IPv6 address so we intercept all queries

* route IPv6 DNS request to our resolver
  • Loading branch information
ekoby authored Aug 16, 2023
1 parent d336e8e commit 04014ee
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 114 deletions.
1 change: 1 addition & 0 deletions app/src/main/java/org/openziti/mobile/ZitiVPNService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ class ZitiVPNService : VpnService(), CoroutineScope {
allowBypass()

addAddress(ZitiRouteManager.defaultRoute.ip, ZitiRouteManager.defaultRoute.ip.address.size * 8)
addAddress(ZitiRouteManager.defaultRoute6.ip, ZitiRouteManager.defaultRoute6.ip.address.size * 8)

// default route
addRoute(ZitiRouteManager.defaultRoute.ip, ZitiRouteManager.defaultRoute.bits)
Expand Down
85 changes: 35 additions & 50 deletions app/src/main/java/org/openziti/mobile/net/DNS.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

package org.openziti.mobile.net

import android.util.Log
import org.openziti.net.dns.DNSResolver
import org.openziti.util.Logged
import org.openziti.util.ZitiLog
import org.pcap4j.packet.DnsPacket
import org.pcap4j.packet.DnsQuestion
import org.pcap4j.packet.DnsRDataA
import org.pcap4j.packet.DnsRDataAaaa
import org.pcap4j.packet.DnsResourceRecord
Expand All @@ -19,59 +21,12 @@ import java.net.Inet6Address
import java.net.InetAddress
import java.net.UnknownHostException

class DNS(val dnsResolver: DNSResolver) {
class DNS(val dnsResolver: DNSResolver): Logged by ZitiLog() {

fun resolve(packet: DnsPacket): DnsPacket {
val q = packet.header.questions.firstOrNull()

val resp = q?.let {
when(it.qType) {
DnsResourceRecordType.A -> {
val answer = DnsResourceRecord.Builder()
.dataType(it.qType)
.dataClass(it.qClass)
.name(it.qName)
.ttl(30)
.rdLength(ByteArrays.INET4_ADDRESS_SIZE_IN_BYTES.toShort())

val ip = dnsResolver.resolve(it.qName.name) ?: bypassDNS(it.qName.name, it.qType)

if (ip != null) {
val rdata = DnsRDataA.Builder()
.address(ip as Inet4Address).build()
answer.rData(rdata)
}

answer.build()
}

DnsResourceRecordType.AAAA -> {
val answer = DnsResourceRecord.Builder()
.dataType(it.qType)
.dataClass(it.qClass)
.name(it.qName)
.ttl(30)
.rdLength(ByteArrays.INET6_ADDRESS_SIZE_IN_BYTES.toShort())


val ip = bypassDNS(it.qName.name, it.qType)

if (ip != null) {
val rdata = DnsRDataAaaa.Builder()
.address(ip as Inet6Address).build()
answer.rData(rdata)
}

answer.build()
}

else -> {
Log.d("DNS", "request ${it.qType} ${it.qName}")
null
}
}
}

val resp = findAnswer(q)

val rb = DnsPacket.Builder()
.id(packet.header.id)
Expand All @@ -93,6 +48,36 @@ class DNS(val dnsResolver: DNSResolver) {
return rb.build()
}

private fun findAnswer(q: DnsQuestion?): DnsResourceRecord? {
if (q == null) return null
if (q.qType != DnsResourceRecordType.A && q.qType != DnsResourceRecordType.AAAA) return null

// don't log since it may be sensitive, keep for debugging purposes
// d { "resolving ${q.qType} ${q.qName}"}
val addr = dnsResolver.resolve(q.qName.name) ?: bypassDNS(q.qName.name, q.qType) ?: return null
// d { "resolved $q => $addr" }

val answer = DnsResourceRecord.Builder()
.dataType(q.qType)
.dataClass(q.qClass)
.name(q.qName)
.ttl(30)

if (q.qType == DnsResourceRecordType.A) {
answer.rdLength(ByteArrays.INET4_ADDRESS_SIZE_IN_BYTES.toShort())

val rdata = DnsRDataA.Builder()
.address(addr as Inet4Address).build()
answer.rData(rdata)
} else if (q.qType == DnsResourceRecordType.AAAA) {
val addr6 = mapToIPv6(addr)
answer.rdLength(ByteArrays.INET6_ADDRESS_SIZE_IN_BYTES.toShort())
answer.rData(DnsRDataAaaa.Builder().address(addr6).build())
}

return answer.build()
}

private fun bypassDNS(name: String, type: DnsResourceRecordType): InetAddress? {

try {
Expand Down
25 changes: 21 additions & 4 deletions app/src/main/java/org/openziti/mobile/net/PacketRouterImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import kotlinx.coroutines.runBlocking
import org.pcap4j.packet.IpPacket
import org.pcap4j.packet.IpSelector
import org.pcap4j.packet.IpV4Packet
import org.pcap4j.packet.IpV4Packet.IpV4Header
import org.pcap4j.packet.IpV6Packet.IpV6Header
import org.pcap4j.packet.TcpPacket
import org.pcap4j.packet.namednumber.IpNumber
import java.net.InetSocketAddress
Expand All @@ -26,21 +28,36 @@ class PacketRouterImpl(val dnsServer: ZitiNameserver, val inbound: suspend (b: B

override fun route(b: ByteBuffer) {

val packet = IpSelector.newPacket(b.array(), b.arrayOffset(), b.limit())
val packet = IpSelector.newPacket(b.array(), b.arrayOffset(), b.limit()) as? IpPacket

if (packet !is IpV4Packet) {
Log.w(TAG, "not handling ${packet.header} at this time")
if (packet == null) {
Log.w(TAG, "unknown packet type")
return
}

if (packet.header.dstAddr in dnsServer.addresses) {
val header = packet.header
val dstAddr = when (header) {
is IpV4Header -> header.dstAddr
is IpV6Header -> header.dstAddr
else -> {
Log.d(TAG, "dropping packet $header")
return
}
}

if (dstAddr in dnsServer.addresses) {
dnsServer.process(packet)?.let {
runBlocking { inbound(ByteBuffer.wrap(it.rawData)) }
}

return
}

if (packet !is IpV4Packet) {
Log.w(TAG, "not handling ${packet.header} at this time")
return
}

val protocol = packet.header.protocol

when (protocol) {
Expand Down
129 changes: 73 additions & 56 deletions app/src/main/java/org/openziti/mobile/net/ZitiNameserver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,95 +9,112 @@ import org.openziti.net.dns.DNSResolver
import org.pcap4j.packet.DnsPacket
import org.pcap4j.packet.IpPacket
import org.pcap4j.packet.IpV4Packet
import org.pcap4j.packet.Packet
import org.pcap4j.packet.IpV6Packet
import org.pcap4j.packet.SimpleBuilder
import org.pcap4j.packet.TcpPacket
import org.pcap4j.packet.UdpPacket
import org.pcap4j.packet.namednumber.IpNumber
import org.pcap4j.packet.namednumber.IpVersion
import java.net.Inet4Address
import java.net.Inet6Address


class ZitiNameserver(resolver: DNSResolver) {

val addresses = setOf(defaultIPv4Address)
val dns = DNS(resolver)
val addresses = setOf(defaultIPv4Address, defaultIPv6Address)
private val dns = DNS(resolver)

fun process(packet: IpPacket): IpPacket? {

if (packet.header.version != IpVersion.IPV4) {
Log.w(TAG, "not handling ${packet.header.version} at this time")
return null
when (packet.header.protocol) {
IpNumber.UDP -> {}
IpNumber.TCP -> return rejectTCP(packet)

IpNumber.ICMPV4,IpNumber.ICMPV6 -> {
Log.v(TAG, "ignoring received ${packet.payload.header}")
return null
}

else -> {
Log.wtf(TAG, "WTF is this? ${packet.payload.header}")
return null
}
}

val ipv4 = packet as IpV4Packet
val udpPacket = packet.payload as UdpPacket
val dnsPacket = udpPacket.payload as DnsPacket

val payload: Packet.Builder? = when (ipv4.header.protocol) {
IpNumber.UDP -> {
val udpPacket = packet.payload as UdpPacket
if (udpPacket.payload is DnsPacket) {
val dnsResp = dns.resolve(dnsPacket)

val resp = dns.resolve(udpPacket.payload as DnsPacket)
val respUDP = UdpPacket.Builder()
.dstAddr(packet.header.srcAddr)
.srcAddr(packet.header.dstAddr)
.dstPort(udpPacket.header.srcPort)
.srcPort(udpPacket.header.dstPort)
.correctChecksumAtBuild(true)
.correctLengthAtBuild(true)
.payloadBuilder(SimpleBuilder(dnsResp))

UdpPacket.Builder()
.dstAddr(packet.header.srcAddr)
.srcAddr(packet.header.dstAddr)
.dstPort(udpPacket.header.srcPort)
.srcPort(udpPacket.header.dstPort)
.correctChecksumAtBuild(true)
.correctLengthAtBuild(true)
.payloadBuilder(SimpleBuilder(resp))
val resp = packet.builder.payloadBuilder(respUDP)

} else null
when (resp) {
is IpV4Packet.Builder -> {
resp.dstAddr(packet.header.srcAddr as Inet4Address)
resp.srcAddr(packet.header.dstAddr as Inet4Address)
resp.correctChecksumAtBuild(true)
resp.correctLengthAtBuild(true)
}

IpNumber.TCP -> {
val tcpPacket = ipv4.payload as TcpPacket
Log.w(TAG, "closing attempted TCP connection: ${tcpPacket.header.dstPort}")
TcpPacket.Builder()
.dstAddr(packet.header.srcAddr)
.srcAddr(packet.header.dstAddr)
.dstPort(tcpPacket.header.srcPort)
.srcPort(tcpPacket.header.dstPort)
.ack(true)
.acknowledgmentNumber(tcpPacket.header.sequenceNumber + 1)
.rst(true)
.correctChecksumAtBuild(true)
.correctLengthAtBuild(true)
is IpV6Packet.Builder -> {
resp.dstAddr(packet.header.srcAddr as Inet6Address)
resp.srcAddr(packet.header.dstAddr as Inet6Address)
resp.correctLengthAtBuild(true)
}
}

IpNumber.ICMPV4,IpNumber.ICMPV6 -> { // TODO?
Log.v(TAG, "ignoring received ${ipv4.payload.header}")
null
}
return resp.build() as IpPacket
}

else -> {
Log.wtf(TAG, "WTF is this? ${packet.payload.header}")
null
private fun rejectTCP(packet: IpPacket): IpPacket {
val tcpPacket = packet.payload as TcpPacket

Log.w(TAG, "closing attempted TCP connection: ${tcpPacket.header.dstPort}")
val tcpOut = TcpPacket.Builder()
.dstAddr(packet.header.srcAddr)
.srcAddr(packet.header.dstAddr)
.dstPort(tcpPacket.header.srcPort)
.srcPort(tcpPacket.header.dstPort)
.ack(true)
.acknowledgmentNumber(tcpPacket.header.sequenceNumber + 1)
.rst(true)
.correctChecksumAtBuild(true)
.correctLengthAtBuild(true)

val outIp = packet.builder.apply {
when(this) {
is IpV4Packet.Builder -> {
dstAddr(packet.header.srcAddr as Inet4Address)
srcAddr(packet.header.dstAddr as Inet4Address)
correctChecksumAtBuild(true)
correctLengthAtBuild(true)
}
is IpV6Packet.Builder -> {
dstAddr(packet.header.srcAddr as Inet6Address)
srcAddr(packet.header.dstAddr as Inet6Address)
correctLengthAtBuild(true)
}
}
payloadBuilder(tcpOut)
}

return payload?.let {
IpV4Packet.Builder()
.version(IpVersion.IPV4)
.protocol(ipv4.header.protocol)
.tos(ipv4.header.tos)
.dstAddr(ipv4.header.srcAddr)
.srcAddr(ipv4.header.dstAddr)
.correctChecksumAtBuild(true)
.correctLengthAtBuild(true)
.payloadBuilder(it).build()
}
return outIp.build() as IpPacket
}
companion object {
const val TAG = "ziti-dns"

val defaultIPv4Address = Inet4Address.getByAddress(byteArrayOf(100, 64, 0, 2))
val defaultIPv6Address = Inet6Address.getByAddress(
byteArrayOf(
0xFDu.toByte(), // ULA prefix
*("ziti!".toByteArray(Charsets.US_ASCII)),
0xFDu.toByte(), 0, // ULA prefix
*("ziti".toByteArray(Charsets.US_ASCII)),
0, 0,// uniq Global ID
0, 0, 0, 0,
0, 0, 0, 2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class ZitiRouteManager: RouteManager {
CIDRBlock(
InetAddress.getByAddress(
byteArrayOf(
0xfd.toByte(), *("ziti!".toByteArray(Charsets.US_ASCII)),
0xfd.toByte(), 0,
*("ziti".toByteArray(Charsets.US_ASCII)),
0, 0,
0, 0, 0, 0,
0, 0, 0, 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
package org.openziti.mobile.net

import android.util.Log
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.openziti.Ziti
import org.openziti.ZitiContext
import org.openziti.mobile.net.tcp.TCP
Expand Down
Loading

0 comments on commit 04014ee

Please sign in to comment.