Move MAC utils to MacAddressCompat

This commit is contained in:
Mygod
2020-05-30 02:06:41 -04:00
parent d328215764
commit e8fb62a0b3
15 changed files with 97 additions and 61 deletions

View File

@@ -16,7 +16,10 @@ android {
targetCompatibility = javaVersion
}
compileSdkVersion("android-R")
kotlinOptions.jvmTarget = javaVersion.toString()
kotlinOptions {
freeCompilerArgs = listOf("-XXLanguage:+InlineClasses")
jvmTarget = javaVersion.toString()
}
defaultConfig {
applicationId = "be.mygod.vpnhotspot"
minSdkVersion(21)

View File

@@ -16,6 +16,7 @@ import androidx.annotation.StringRes
import androidx.core.content.edit
import androidx.core.content.getSystemService
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
@@ -23,7 +24,6 @@ import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupI
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
import be.mygod.vpnhotspot.net.wifi.configuration.channelToFrequency
import be.mygod.vpnhotspot.room.macToString
import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.*
@@ -381,7 +381,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
check(routingManager == null)
routingManager = object : RoutingManager.LocalOnly(this, group.`interface`!!) {
override fun ifaceHandler(iface: NetworkInterface) {
iface.hardwareAddress?.asIterable()?.macToString()?.let { lastMac = it }
iface.hardwareAddress?.let { lastMac = MacAddressCompat.bytesToString(it) }
}
}.apply { start() }
status = Status.ACTIVE

View File

@@ -9,16 +9,16 @@ import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.InetAddressComparator
import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.room.ClientRecord
import be.mygod.vpnhotspot.room.macToString
import be.mygod.vpnhotspot.util.makeIpSpan
import be.mygod.vpnhotspot.util.makeMacSpan
import java.net.InetAddress
import java.util.*
open class Client(val mac: Long, val iface: String) {
open class Client(val mac: MacAddressCompat, val iface: String) {
companion object DiffCallback : DiffUtil.ItemCallback<Client>() {
override fun areItemsTheSame(oldItem: Client, newItem: Client) =
oldItem.iface == newItem.iface && oldItem.mac == newItem.mac
@@ -26,7 +26,7 @@ open class Client(val mac: Long, val iface: String) {
}
val ip = TreeMap<InetAddress, IpNeighbour.State>(InetAddressComparator)
val macString by lazy { mac.macToString() }
val macString by lazy { mac.toString() }
private val record = AppDatabase.instance.clientRecordDao.lookupOrDefaultSync(mac)
private val macIface get() = SpannableStringBuilder(makeMacSpan(macString)).apply {
append('%')
@@ -65,7 +65,7 @@ open class Client(val mac: Long, val iface: String) {
}.trimEnd()
}
fun obtainRecord() = record.value ?: ClientRecord(mac)
fun obtainRecord() = record.value ?: ClientRecord(mac.addr)
override fun equals(other: Any?): Boolean {
if (this === other) return true

View File

@@ -10,11 +10,11 @@ import androidx.lifecycle.ViewModel
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.room.macToLong
import be.mygod.vpnhotspot.util.broadcastReceiver
class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callback {
@@ -31,15 +31,15 @@ class ClientViewModel : ViewModel(), ServiceConnection, IpNeighbourMonitor.Callb
val clients = MutableLiveData<List<Client>>()
private fun populateClients() {
val clients = HashMap<Pair<String, Long>, Client>()
val clients = HashMap<Pair<String, MacAddressCompat>, Client>()
val group = repeater?.group
val p2pInterface = group?.`interface`
if (p2pInterface != null) {
for (client in p2p) clients[Pair(p2pInterface, client.deviceAddress.macToLong())] =
for (client in p2p) clients[p2pInterface to MacAddressCompat.fromString(client.deviceAddress)] =
WifiP2pClient(p2pInterface, client)
}
for (neighbour in neighbours) {
val key = Pair(neighbour.dev, neighbour.lladdr)
val key = neighbour.dev to neighbour.lladdr
var client = clients[key]
if (client == null) {
if (!tetheredInterfaces.contains(neighbour.dev)) continue

View File

@@ -30,13 +30,13 @@ import be.mygod.vpnhotspot.Empty
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.databinding.FragmentClientsBinding
import be.mygod.vpnhotspot.databinding.ListitemClientBinding
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.room.ClientStats
import be.mygod.vpnhotspot.room.TrafficRecord
import be.mygod.vpnhotspot.room.macToString
import be.mygod.vpnhotspot.util.SpanFormatter
import be.mygod.vpnhotspot.util.toPluralInt
import be.mygod.vpnhotspot.widget.SmartSnackbar
@@ -46,11 +46,11 @@ import java.text.NumberFormat
class ClientsFragment : Fragment() {
@Parcelize
data class NicknameArg(val mac: Long, val nickname: CharSequence) : Parcelable
data class NicknameArg(val mac: MacAddressCompat, val nickname: CharSequence) : Parcelable
class NicknameDialogFragment : AlertDialogFragment<NicknameArg, Empty>() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
setView(R.layout.dialog_nickname)
setTitle(getString(R.string.clients_nickname_title, arg.mac.macToString()))
setTitle(getString(R.string.clients_nickname_title, arg.mac.toString()))
setPositiveButton(android.R.string.ok, listener)
setNegativeButton(android.R.string.cancel, null)
setNeutralButton(emojize(getText(R.string.clients_nickname_set_to_vendor)), listener)
@@ -153,7 +153,7 @@ class ClientsFragment : Fragment() {
withContext(Dispatchers.Unconfined) {
StatsDialogFragment().withArg(StatsArg(
client.title.value ?: return@withContext,
AppDatabase.instance.trafficRecordDao.queryStats(client.mac)
AppDatabase.instance.trafficRecordDao.queryStats(client.mac.addr)
)).show(this@ClientsFragment)
}
}
@@ -181,7 +181,7 @@ class ClientsFragment : Fragment() {
override fun onBindViewHolder(holder: ClientViewHolder, position: Int) {
val client = getItem(position)
holder.binding.client = client
holder.binding.rate = rates.computeIfAbsent(Pair(client.iface, client.mac)) { TrafficRate() }
holder.binding.rate = rates.computeIfAbsent(client.iface to client.mac) { TrafficRate() }
holder.binding.executePendingBindings()
}
@@ -196,7 +196,9 @@ class ClientsFragment : Fragment() {
check(newRecord.receivedPackets == oldRecord.receivedPackets)
check(newRecord.receivedBytes == oldRecord.receivedBytes)
} else {
val rate = rates.computeIfAbsent(Pair(newRecord.downstream, newRecord.mac)) { TrafficRate() }
val rate = rates.computeIfAbsent(newRecord.downstream to MacAddressCompat(newRecord.mac)) {
TrafficRate()
}
if (rate.send < 0 || rate.receive < 0) {
rate.send = 0
rate.receive = 0
@@ -211,7 +213,7 @@ class ClientsFragment : Fragment() {
private lateinit var binding: FragmentClientsBinding
private val adapter = ClientAdapter()
private var rates = mutableMapOf<Pair<String, Long>, TrafficRate>()
private var rates = mutableMapOf<Pair<String, MacAddressCompat>, TrafficRate>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentClientsBinding.inflate(inflater, container, false)

View File

@@ -5,8 +5,8 @@ import android.os.Build
import androidx.annotation.MainThread
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.room.macToString
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
@@ -22,26 +22,26 @@ import java.net.URL
* This class generates a default nickname for new clients.
*/
object MacLookup {
class UnexpectedError(val mac: Long, val error: String) : JSONException("") {
class UnexpectedError(val mac: MacAddressCompat, val error: String) : JSONException("") {
private fun formatMessage(context: Context) =
context.getString(R.string.clients_mac_lookup_unexpected_error, mac.macToString(), error)
context.getString(R.string.clients_mac_lookup_unexpected_error, mac.toString(), error)
override val message get() = formatMessage(app.english)
override fun getLocalizedMessage() = formatMessage(app)
}
private val macLookupBusy = mutableMapOf<Long, Pair<HttpURLConnection, Job>>()
private val macLookupBusy = mutableMapOf<MacAddressCompat, Pair<HttpURLConnection, Job>>()
private val countryCodeRegex = "([A-Z]{2})\\s*\$".toRegex() // http://en.wikipedia.org/wiki/ISO_3166-1
@MainThread
fun abort(mac: Long) = macLookupBusy.remove(mac)?.let { (conn, job) ->
fun abort(mac: MacAddressCompat) = macLookupBusy.remove(mac)?.let { (conn, job) ->
job.cancel()
if (Build.VERSION.SDK_INT >= 26) conn.disconnect() else GlobalScope.launch(Dispatchers.IO) { conn.disconnect() }
}
@MainThread
fun perform(mac: Long, explicit: Boolean = false) {
fun perform(mac: MacAddressCompat, explicit: Boolean = false) {
abort(mac)
val conn = URL("https://macvendors.co/api/" + mac.macToString()).openConnection() as HttpURLConnection
val conn = URL("https://macvendors.co/api/$mac").openConnection() as HttpURLConnection
macLookupBusy[mac] = conn to GlobalScope.launch(Dispatchers.IO) {
try {
val response = conn.inputStream.bufferedReader().readText()
@@ -69,7 +69,7 @@ object MacLookup {
}
}
private fun extractCountry(mac: Long, response: String, obj: JSONObject): MatchResult? {
private fun extractCountry(mac: MacAddressCompat, response: String, obj: JSONObject): MatchResult? {
countryCodeRegex.matchEntire(obj.optString("country"))?.also { return it }
val address = obj.optString("address")
if (address.isBlank()) return null

View File

@@ -1,9 +1,10 @@
package be.mygod.vpnhotspot.client
import android.net.wifi.p2p.WifiP2pDevice
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.room.macToLong
class WifiP2pClient(p2pInterface: String, p2p: WifiP2pDevice) : Client(p2p.deviceAddress!!.macToLong(), p2pInterface) {
class WifiP2pClient(p2pInterface: String, p2p: WifiP2pDevice) :
Client(MacAddressCompat.fromString(p2p.deviceAddress!!), p2pInterface) {
override val icon: Int get() = TetherType.WIFI_P2P.icon
}

View File

@@ -3,7 +3,6 @@ package be.mygod.vpnhotspot.net
import android.os.Build
import android.system.ErrnoException
import android.system.OsConstants
import be.mygod.vpnhotspot.room.macToLong
import be.mygod.vpnhotspot.util.parseNumericAddress
import timber.log.Timber
import java.io.File
@@ -13,7 +12,7 @@ import java.net.InetAddress
import java.net.NetworkInterface
import java.net.SocketException
data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: Long, val state: State) {
data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddressCompat, val state: State) {
enum class State {
INCOMPLETE, VALID, FAILED, DELETING
}
@@ -56,12 +55,12 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: Long, v
else -> throw IllegalArgumentException("Unknown state encountered: ${match.groupValues[7]}")
}
val mac = try {
lladdr.macToLong()
} catch (e: NumberFormatException) {
MacAddressCompat.fromString(lladdr)
} catch (e: IllegalArgumentException) {
if (match.groups[4] == null) return emptyList()
// for DELETING, we only care about IP address and do not care if MAC is not present
if (state != State.DELETING) Timber.w(IOException("Failed to find MAC address for $line"))
0L
MacAddressCompat.ALL_ZEROS_ADDRESS
}
val result = IpNeighbour(ip, dev, mac, state)
val devParser = devFallback.matchEntire(dev)

View File

@@ -0,0 +1,45 @@
package be.mygod.vpnhotspot.net
import java.nio.ByteBuffer
import java.nio.ByteOrder
inline class MacAddressCompat(val addr: Long) {
companion object {
private const val ETHER_ADDR_LEN = 6
/**
* The MacAddress zero MAC address.
*
* Not publicly exposed or treated specially since the OUI 00:00:00 is registered.
* @hide
*/
val ALL_ZEROS_ADDRESS = MacAddressCompat(0)
fun bytesToString(addr: ByteArray): String {
require(addr.size == ETHER_ADDR_LEN) { addr.contentToString() + " was not a valid MAC address" }
return addr.joinToString(":") { "%02x".format(it) }
}
fun bytesToString(addr: Collection<Byte>): String {
require(addr.size == ETHER_ADDR_LEN) { addr.joinToString() + " was not a valid MAC address" }
return addr.joinToString(":") { "%02x".format(it) }
}
@Throws(IllegalArgumentException::class)
fun fromString(addr: String) = MacAddressCompat(ByteBuffer.allocate(8).run {
order(ByteOrder.LITTLE_ENDIAN)
mark()
try {
put(addr.split(':').map { Integer.parseInt(it, 16).toByte() }.toByteArray())
} catch (e: NumberFormatException) {
throw IllegalArgumentException(e)
}
reset()
long
})
}
override fun toString() = ByteBuffer.allocate(8).run {
order(ByteOrder.LITTLE_ENDIAN)
putLong(addr)
bytesToString(array().take(6))
}
}

View File

@@ -208,7 +208,7 @@ class Routing(private val caller: Any, private val downstream: String,
private val upstream = Upstream(RULE_PRIORITY_UPSTREAM)
private var disableSystem: RootSession.Transaction? = null
private inner class Client(private val ip: Inet4Address, mac: Long) : AutoCloseable {
private inner class Client(private val ip: Inet4Address, mac: MacAddressCompat) : AutoCloseable {
private val transaction = RootSession.beginTransaction().safeguard {
val address = ip.hostAddress
iptablesInsert("vpnhotspot_acl -i $downstream -s $address -j ACCEPT")

View File

@@ -1,6 +1,7 @@
package be.mygod.vpnhotspot.net.monitor
import android.util.LongSparseArray
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.room.TrafficRecord
@@ -20,8 +21,8 @@ object TrafficRecorder {
private val records = mutableMapOf<Pair<InetAddress, String>, TrafficRecord>()
val foregroundListeners = Event2<Collection<TrafficRecord>, LongSparseArray<TrafficRecord>>()
fun register(ip: InetAddress, downstream: String, mac: Long) {
val record = TrafficRecord(mac = mac, ip = ip, downstream = downstream)
fun register(ip: InetAddress, downstream: String, mac: MacAddressCompat) {
val record = TrafficRecord(mac = mac.addr, ip = ip, downstream = downstream)
AppDatabase.instance.trafficRecordDao.insert(record)
synchronized(this) {
Timber.d("Registering $ip%$downstream")
@@ -149,5 +150,5 @@ object TrafficRecorder {
/**
* Possibly inefficient. Don't call this too often.
*/
fun isWorking(mac: Long) = records.values.any { it.mac == mac }
fun isWorking(mac: MacAddressCompat) = records.values.any { it.mac == mac.addr }
}

View File

@@ -3,6 +3,7 @@ package be.mygod.vpnhotspot.room
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
import androidx.room.*
import be.mygod.vpnhotspot.net.MacAddressCompat
@Entity
data class ClientRecord(@PrimaryKey
@@ -14,7 +15,7 @@ data class ClientRecord(@PrimaryKey
abstract class Dao {
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
protected abstract fun lookupBlocking(mac: Long): ClientRecord?
fun lookupOrDefaultBlocking(mac: Long) = lookupBlocking(mac) ?: ClientRecord(mac)
fun lookupOrDefaultBlocking(mac: MacAddressCompat) = lookupBlocking(mac.addr) ?: ClientRecord(mac.addr)
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
protected abstract suspend fun lookup(mac: Long): ClientRecord?
@@ -22,14 +23,15 @@ data class ClientRecord(@PrimaryKey
@Query("SELECT * FROM `ClientRecord` WHERE `mac` = :mac")
protected abstract fun lookupSync(mac: Long): LiveData<ClientRecord?>
fun lookupOrDefaultSync(mac: Long) = lookupSync(mac).map { it ?: ClientRecord(mac) }
fun lookupOrDefaultSync(mac: MacAddressCompat) = lookupSync(mac.addr).map { it ?: ClientRecord(mac.addr) }
@Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract suspend fun updateInternal(value: ClientRecord): Long
suspend fun update(value: ClientRecord) = check(updateInternal(value) == value.mac)
@Transaction
open suspend fun upsert(mac: Long, operation: suspend ClientRecord.() -> Unit) = lookupOrDefault(mac).apply {
open suspend fun upsert(mac: MacAddressCompat, operation: suspend ClientRecord.() -> Unit) = lookupOrDefault(
mac.addr).apply {
operation()
update(this)
}

View File

@@ -5,8 +5,6 @@ import androidx.room.TypeConverter
import be.mygod.vpnhotspot.util.useParcel
import timber.log.Timber
import java.net.InetAddress
import java.nio.ByteBuffer
import java.nio.ByteOrder
object Converters {
@JvmStatic
@@ -37,18 +35,3 @@ object Converters {
@TypeConverter
fun unpersistInetAddress(data: ByteArray): InetAddress = InetAddress.getByAddress(data)
}
fun String.macToLong(): Long = ByteBuffer.allocate(8).run {
order(ByteOrder.LITTLE_ENDIAN)
mark()
put(split(':').map { Integer.parseInt(it, 16).toByte() }.toByteArray())
reset()
long
}
fun Iterable<Byte>.macToString() = joinToString(":") { "%02x".format(it) }
fun Long.macToString(): String = ByteBuffer.allocate(8).run {
order(ByteOrder.LITTLE_ENDIAN)
putLong(this@macToString)
array().take(6).macToString()
}

View File

@@ -19,7 +19,7 @@ import androidx.databinding.BindingAdapter
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.room.macToString
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.widget.SmartSnackbar
import java.net.InetAddress
import java.net.NetworkInterface
@@ -94,7 +94,7 @@ fun makeMacSpan(mac: String) = if (app.hasTouch) SpannableString(mac).apply {
fun NetworkInterface.formatAddresses(macOnly: Boolean = false) = SpannableStringBuilder().apply {
try {
hardwareAddress?.apply { appendln(makeMacSpan(asIterable().macToString())) }
hardwareAddress?.let { appendln(makeMacSpan(MacAddressCompat.bytesToString(it))) }
} catch (_: SocketException) { }
if (!macOnly) for (address in interfaceAddresses) {
append(makeIpSpan(address.address))

View File

@@ -1,4 +1,4 @@
package be.mygod.vpnhotspot.room
package be.mygod.vpnhotspot.net
import org.junit.Assert.*
import org.junit.Test
@@ -7,7 +7,7 @@ class ConvertersTest {
@Test
fun macSerialization() {
for (test in listOf("01:23:45:67:89:ab", "DE:AD:88:88:BE:EF")) {
assertTrue(test.equals(test.macToLong().macToString(), true))
assertTrue(test.equals(MacAddressCompat.fromString(test).toString(), true))
}
}
}