Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ All notable changes to mobileGF2logger are documented here.
parser, order known same-day joins by instant, bound manual weekly notes,
preserve captured member names, replace timezone-derived history atomically,
and accept `21905` checklist evidence only after identity validation.
- Bound the profile registry, profile metadata, and pre-identity flow buffer;
reject invalid restores before metadata changes, preserve the selected import
scope through preview/apply, and restore the matching client-region routing.
- Bound the profile registry, profile metadata, and pre-identity quarantine with
both per-flow and aggregate decoded-payload caps; reject invalid restores before
metadata changes, preserve the selected import scope through preview/apply, and
restore the matching client-region routing.
- Quarantine identity-changing or admission-failed flows, admit at most one new
profile per client per user-started capture, and let users forget selector
metadata without deleting the isolated Platoon data.
Expand Down
8 changes: 7 additions & 1 deletion app/src/main/java/dev/gf2log/app/WeeklyReportActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1106,7 +1106,13 @@ class WeeklyReportActivity : LocalizedActivity() {
setTextColor(getColor(R.color.accent_text))
background = ModernUi.panelBackground(context, emphasized = true)
setPadding(dp(8), dp(8), dp(8), dp(8))
}, LinearLayout.LayoutParams(dp(76), dp(48)).apply {
minWidth = dp(76)
minHeight = dp(48)
maxLines = 1
}, LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
).apply {
marginEnd = dp(12)
})
addView(TextView(context).apply {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,53 @@
package dev.gf2log.app.capture

/** Bounded pre-identity quarantine; overflow permanently rejects that flow until closure. */
internal class BoundedFlowPayloadBuffer<T>(private val maxItemsPerFlow: Int) {
internal class BoundedFlowPayloadBuffer<T>(
private val maxItemsPerFlow: Int,
private val maxTotalItems: Int,
) {
private val pending = mutableMapOf<Long, ArrayDeque<T>>()
private val rejected = mutableSetOf<Long>()
private var totalItems = 0

init {
require(maxItemsPerFlow > 0)
require(maxTotalItems > 0)
}

fun offer(flowId: Long, item: T): OfferResult {
if (flowId in rejected) return OfferResult.REJECTED
val items = pending.getOrPut(flowId, ::ArrayDeque)
if (items.size >= maxItemsPerFlow) {
pending.remove(flowId)
rejected += flowId
if (items.size >= maxItemsPerFlow || totalItems >= maxTotalItems) {
reject(flowId)
return OfferResult.OVERFLOW
}
items.addLast(item)
totalItems += 1
Comment thread
1window2 marked this conversation as resolved.
return OfferResult.ACCEPTED
}

fun take(flowId: Long): List<T> = pending.remove(flowId)?.toList().orEmpty()
fun take(flowId: Long): List<T> {
val items = pending.remove(flowId) ?: return emptyList()
totalItems -= items.size
return items.toList()
}

fun isRejected(flowId: Long): Boolean = flowId in rejected

fun reject(flowId: Long) {
pending.remove(flowId)
totalItems -= pending.remove(flowId)?.size ?: 0
rejected += flowId
}

fun remove(flowId: Long) {
pending.remove(flowId)
totalItems -= pending.remove(flowId)?.size ?: 0
rejected.remove(flowId)
}

fun clear() {
pending.clear()
rejected.clear()
totalItems = 0
}

enum class OfferResult { ACCEPTED, OVERFLOW, REJECTED }
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/dev/gf2log/app/capture/CaptureVpnService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class CaptureVpnService : VpnService() {
private val pendingAdmissionByFlow = ConcurrentHashMap<Long, String>()
private val pendingFlowPayloads = BoundedFlowPayloadBuffer<ParsedPayload>(
MAX_PENDING_PAYLOADS_PER_FLOW,
MAX_PENDING_PAYLOADS_TOTAL,
)
private val decodedPayloadCount = AtomicLong()
private val observedPayloadBytes = AtomicLong()
Expand Down Expand Up @@ -968,6 +969,7 @@ class CaptureVpnService : VpnService() {
private const val TRAFFIC_REPORT_BYTES = 64 * 1024
private const val CAPTURE_ONCE_GRACE_MILLIS = 60_000L
private const val MAX_PENDING_PAYLOADS_PER_FLOW = 32
private const val MAX_PENDING_PAYLOADS_TOTAL = 128
private val REQUIRED_CAPTURE_TYPES = setOf(
Gfl2PayloadDecoder.TYPE_PLATOON_PROFILE,
Gfl2PayloadDecoder.TYPE_GUILD_MEMBERS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import org.junit.Test
class BoundedFlowPayloadBufferTest {
@Test
fun overflowDiscardsAndRejectsUntilFlowRemoval() {
val buffer = BoundedFlowPayloadBuffer<String>(2)
val buffer = BoundedFlowPayloadBuffer<String>(2, 4)
assertEquals(BoundedFlowPayloadBuffer.OfferResult.ACCEPTED, buffer.offer(7, "a"))
assertEquals(BoundedFlowPayloadBuffer.OfferResult.ACCEPTED, buffer.offer(7, "b"))
assertEquals(BoundedFlowPayloadBuffer.OfferResult.OVERFLOW, buffer.offer(7, "c"))
Expand All @@ -24,11 +24,40 @@ class BoundedFlowPayloadBufferTest {

@Test
fun identityTakesOneFlowWithoutTouchingAnother() {
val buffer = BoundedFlowPayloadBuffer<String>(2)
val buffer = BoundedFlowPayloadBuffer<String>(2, 4)
buffer.offer(1, "one")
buffer.offer(2, "two")

assertEquals(listOf("one"), buffer.take(1))
assertEquals(listOf("two"), buffer.take(2))
}

@Test
fun aggregateOverflowRejectsOnlyTheFlowThatExceededTheGlobalBudget() {
val buffer = BoundedFlowPayloadBuffer<String>(4, 3)
assertEquals(BoundedFlowPayloadBuffer.OfferResult.ACCEPTED, buffer.offer(1, "one-a"))
assertEquals(BoundedFlowPayloadBuffer.OfferResult.ACCEPTED, buffer.offer(1, "one-b"))
assertEquals(BoundedFlowPayloadBuffer.OfferResult.ACCEPTED, buffer.offer(2, "two"))

assertEquals(BoundedFlowPayloadBuffer.OfferResult.OVERFLOW, buffer.offer(1, "one-c"))
assertTrue(buffer.isRejected(1))
assertTrue(buffer.take(1).isEmpty())
assertEquals(listOf("two"), buffer.take(2))

assertEquals(BoundedFlowPayloadBuffer.OfferResult.ACCEPTED, buffer.offer(3, "three"))
assertEquals(listOf("three"), buffer.take(3))
}

@Test
fun removalAndClearReleaseAggregateCapacity() {
val buffer = BoundedFlowPayloadBuffer<String>(2, 2)
buffer.offer(1, "one")
buffer.offer(2, "two")
buffer.remove(1)
assertEquals(BoundedFlowPayloadBuffer.OfferResult.ACCEPTED, buffer.offer(3, "three"))

buffer.clear()
assertEquals(BoundedFlowPayloadBuffer.OfferResult.ACCEPTED, buffer.offer(4, "four"))
assertEquals(listOf("four"), buffer.take(4))
}
}
Loading