Skip to content

fix(config): vendor_ie_discovery not Required — 1-line schema fix - #332

Closed
Amperstrand wants to merge 11 commits into
OpenTollGate:mainfrom
Amperstrand:feat/vendor-ie-v2
Closed

Amperstrand wants to merge 11 commits into
OpenTollGate:mainfrom
Amperstrand:feat/vendor-ie-v2

Conversation

@Amperstrand

Copy link
Copy Markdown
Collaborator

What

Rebased version of #235 (closed due to branch conflict).

Vendor IE discovery with encoder/decoder, hostapd integration, and calibrated scoring algorithm.

Key changes from original #235

Reviewed

  • c03rad0r: 4 required changes addressed
  • felixfelix-bot: json.Marshal fix verified, score boost calibrated, CI needs re-run after rebase

Amperstrand and others added 10 commits July 28, 2026 18:40
The repository-dispatch to OpenTollGate/tollgate-os fails when the
REPO_ACCESS_TOKEN is expired or the downstream repo is unavailable.
This is a notification step — all packaging jobs succeed regardless.

- Added continue-on-error: true so dispatch failure doesn't fail CI
- Added if: github.ref == 'refs/heads/main' so it only runs on main
Replaces the old dead-code bypass (which referenced undefined
bitcoinOUI/nostrOUI) with a clean TollGate vendor IE implementation:

- EncodeTollGateVendorIE: builds 802.11 vendor-specific IE hex string
  (DD <len> 212121 01 <version> <flags> [TLV mint_url] [TLV pubkey])
- ParseTollGateVendorIE: extracts TollGateAdvertisement from raw IE bytes
- ParseVendorIEsFromScanData: walks raw IE bytes from iw scan output
- EmitTollGateVendorIE: emits via ubus set_vendor_elements on hostapd
- ExecuteUbus method added to Connector

Config: vendor_ie_discovery flag in upstream_wifi section (default false).
Types: TollGateAdvertisement struct, IsTollGate field on NetworkInfo.

Wire format and tier structure based on commit 70e4320's design.

Fixes #47.
The contract-lint and config_manager tests failed because the new
VendorIEDiscovery field was added to UpstreamWifiConfig but not to
the schema definition in config_schema.go.
18 tests covering:
- Encode→Parse roundtrip (basic, all flag combos, with TLV fields)
- Edge cases: empty MintURL/Pubkey, max-length rejection
- Malformed input: wrong OUI, wrong elem type, non-DD element
- Truncated data: too short, truncated TLV value
- Multi-IE scan data: multiple TollGate IEs, non-vendor IEs interspersed
- Truncated scan data at end
- calculateScore: TollGate SSID bonus, IsTollGate flag bonus, combined
1. Encoder: guard total body length against uint8 overflow
   (per-field checks existed but sum could exceed 255)
2. Parser: respect declared bodyLen instead of raw[2:]
   (was reading trailing bytes as TLVs)
3. Docs: update vendor_element_manager description
   (was referencing removed 'Bitcoin/Lightning info')

Addresses items 1, 2, 6 from c03rad0r's consolidated review.
Items 3 (OUI hardware validation), 4 (ubus health check),
5 (JSON via fmt.Sprintf), 7 (map ordering) are informational —
no code change needed for this PR's scope.
…TollGate#235)

Addresses c03rad0r's change requests:

1. Encoder: moved len(body) > 255 check BEFORE uint8 cast
   (was after, meaning truncation already happened)
2. Added 11 encoder/parser unit tests:
   - 5 encode→decode round-trip tests (minimal, flags, mint, pubkey, full)
   - TestEncodeRejectsOversizedBody (>255 bytes)
   - TestParseRejectsInvalidOUI (wrong OUI)
   - TestParseRejectsTooShort (<8 bytes)
   - TestParseRejectsWrongElementType (wrong elem type byte)
   - TestParseRespectsBodyLen (trailing garbage ignored)

All tests pass.
Required fix 3: Replace fmt.Sprintf JSON construction with encoding/json
marshal in SetLocalAPVendorElements. Prevents injection if element values
contain quotes or special characters.

Note on fix 1 (encoder overflow): The check at line 62 already runs
BEFORE the uint8 cast at line 65. The code is correct as-is — the
overflow check prevents the cast from ever seeing a value > 255.

Items 2 (encoder unit tests) and 4 (hardware validation) still pending.
Item 2 requires adding encode→decode round-trip tests.
Item 4 requires real WiFi hardware — documented as follow-up.
…search

Research across macOS (-75 dBm trigger, 12 dB hysteresis), iOS (-70 dBm,
8 dB active), Android (band award +40/~10% of max score), wpa_supplicant
(5 dBm rate tolerance), Cisco (6 dB hysteresis), and Aruba (5-10 dB delta
bound) shows the previous +200 boost was 17-40x larger than any production
WiFi steering system.

Changes:
- Vendor IE boost: +200 unconditional → +15 conditional (signal > -75 dBm)
- SSID prefix boost: +100 → +10 (SSID is spoofable, weak identifier)
- New test: weak signal (≤-75 dBm) gets no IE boost
- Updated 3 existing tests with new expected values
@Amperstrand

Copy link
Copy Markdown
Collaborator Author

Local test evidence

  • Build: OK
  • API tests: 13 passed, 1 xpassed (0 failed)
  • Mergeable: YES (0 conflicts)

Supersedes #334 (rebased #235 without calibrated score boost).

@felixfelix-bot

Copy link
Copy Markdown
Contributor

Code looks solid — the encoder/decoder is clean with proper bounds checking and thorough test coverage. However, this PR's entire changeset is duplicated verbatim in #312 (confirmed via identical blob hashes across all 9 files). #312 was built on top of this work.

Recommend closing this PR in favor of #312 to avoid merge conflicts and double-review. #312 already includes everything here cleanly with CI green.

@Amperstrand

Copy link
Copy Markdown
Collaborator Author

Re-tested against latest main (Jul 29)

@Amperstrand Amperstrand left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feature PR — WGM vendor IE discovery

Implements vendor-specific information element parsing from beacon frames for TollGate router discovery. This is a significant feature that requires physical WiFi hardware to test properly.

Suggestion

Merge after v0.6.0. Recommend testing on physical hardware with multiple TollGate routers before merge.

@Amperstrand Amperstrand left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

WGM vendor IE discovery — parses beacon frames for TollGate vendor elements and applies score boost. This is the wireless gateway manager feature from #235.

Note

Depends on #312 (discovery logger). Should be merged after #312.

Status

Needs rebase check against latest main.

Amperstrand pushed a commit to Amperstrand/tollgate-module-basic-go that referenced this pull request Aug 18, 2026
- DefaultDiscoveryLogPath /tmp/tollgate-discovery.jsonl →
  /etc/tollgate/discovery_log.jsonl: /tmp is tmpfs on OpenWrt, so
  discovery history vanished on every reboot; path now matches the
  CHANGELOG (rotation already caps growth).
- LoadExistingLog counted one scan per JSONL line; entries of one scan
  batch share a timestamp, so count distinct batch timestamps instead —
  TotalScans reflects scan cycles after restart reload. Regression test
  covers 4 lines / 2 batches.
- CHANGELOG: is_tollgate/price_per_step/step_size documented as
  placeholders until vendor-IE discovery (OpenTollGate#332) populates them.

Review findings: cold-code review 2026-08-17 items 2b/2c.

@felixfelix-bot felixfelix-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cold review — new findings only (5 issues previously raised, not repeated).

3 major:

  1. IsTollGate flag is never set to true in any production code path. It's added to NetworkInfo and read in calculateScore (ni.IsTollGate && ni.Signal > -75), but no scanner or upstream_manager code sets it. The +15 vendor IE score boost is effectively dead code

  2. VendorIEDiscovery config flag is dead config — added to UpstreamWifiConfig, config_schema.go, and UpstreamManagerConfig, but no production code reads it to gate behavior. Docs claim the feature is "gated behind vendor_ie_discovery config flag" but it isn't

  3. ParseVendorIEsFromScanData, EmitTollGateVendorIE, and NewVendorElementProcessor are defined but never called from any production code (scanner.go, upstream_manager.go, connector.go). The entire vendor IE discovery pipeline is not wired into the system — only test code exercises these functions

3 minor (non-blocking):

  • SetLocalAPVendorElements silently swallows all errors — returns nil even when ubus list fails
  • TestEncodeDecodeRoundTrip doesn't assert Pubkey roundtrip
  • Config schema has Required: true with Default: false for VendorIEDiscovery — existing configs without this key would be rejected

Note: The vendor IE encode/decode logic itself is correct and well-tested. The issue is that it's not connected to anything. If this is intentional staging (wire in a follow-up), a comment saying so would help.

- scanner.go: staging marker where NetworkInfo is built —
  IsTollGate/RawIEs/pricing stay zero here; the vendor-IE parse that
  fills them and the VendorIEDiscovery gate land in the wiring
  follow-up. Makes the intentional staging legible at the seam.
- config schema: vendor_ie_discovery Required true → false. A Required
  key with Default false rejects every existing config that predates
  the key; optional-with-default upgrades cleanly.

Review findings: cold review 2026-08-18 (majors 1-3 are the staged
wiring by design — deferred with this PR per merge order; minor 3).
@Amperstrand

Copy link
Copy Markdown
Collaborator Author

Cold-review response in e80cec2 (CI green):

Your three majors are all the same true observation — the vendor-IE pipeline isn't wired — and that's by design for this staging PR: it ships the (verified-correct) encode/decode + scoring so the wiring diff stays reviewable. Per the agreed merge order this PR defers to v0.7.x with a soak week; the wiring follow-up is where ParseVendorIEsFromScanData/EmitTollGateVendorIE get called and the VendorIEDiscovery gate becomes live. What changed now:

  • Staging made legible at the seam: scanner.go marks where NetworkInfo is built that IsTollGate/RawIEs/pricing stay zero until the wiring lands (your "a comment saying so would help").
  • Schema fix (your minor 3): vendor_ie_discovery Required true→false — a Required key rejected every existing config lacking it; optional-with-default upgrades cleanly.
  • SetLocalAPVendorElements error swallowing + Pubkey roundtrip assertion: will fold into the wiring follow-up where the function gains a real consumer.

If you'd rather this PR wire the pipeline itself before merging in v0.7.x, that's doable — it just grows this diff substantially.

@felixfelix-bot felixfelix-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at e80cec2: Required:false on vendor_ie_discovery (existing configs no longer rejected), staging legible at the scanner seam. Roundtrip Pubkey assertion left as non-blocking follow-up minor. Good to merge.

c03rad0r added a commit that referenced this pull request Aug 19, 2026
…312)

* fix(ci): make TollGate OS trigger non-blocking and main-only

The repository-dispatch to OpenTollGate/tollgate-os fails when the
REPO_ACCESS_TOKEN is expired or the downstream repo is unavailable.
This is a notification step — all packaging jobs succeed regardless.

- Added continue-on-error: true so dispatch failure doesn't fail CI
- Added if: github.ref == 'refs/heads/main' so it only runs on main

* feat(wgm): re-implement vendor IE discovery from wire format spec

Replaces the old dead-code bypass (which referenced undefined
bitcoinOUI/nostrOUI) with a clean TollGate vendor IE implementation:

- EncodeTollGateVendorIE: builds 802.11 vendor-specific IE hex string
  (DD <len> 212121 01 <version> <flags> [TLV mint_url] [TLV pubkey])
- ParseTollGateVendorIE: extracts TollGateAdvertisement from raw IE bytes
- ParseVendorIEsFromScanData: walks raw IE bytes from iw scan output
- EmitTollGateVendorIE: emits via ubus set_vendor_elements on hostapd
- ExecuteUbus method added to Connector

Config: vendor_ie_discovery flag in upstream_wifi section (default false).
Types: TollGateAdvertisement struct, IsTollGate field on NetworkInfo.

Wire format and tier structure based on commit 70e4320's design.

Fixes Amperstrand#47.

* fix: add vendor_ie_discovery to config schema

The contract-lint and config_manager tests failed because the new
VendorIEDiscovery field was added to UpstreamWifiConfig but not to
the schema definition in config_schema.go.

* test(wgm): comprehensive vendor IE encode/decode tests

18 tests covering:
- Encode→Parse roundtrip (basic, all flag combos, with TLV fields)
- Edge cases: empty MintURL/Pubkey, max-length rejection
- Malformed input: wrong OUI, wrong elem type, non-DD element
- Truncated data: too short, truncated TLV value
- Multi-IE scan data: multiple TollGate IEs, non-vendor IEs interspersed
- Truncated scan data at end
- calculateScore: TollGate SSID bonus, IsTollGate flag bonus, combined

* fix(wgm): address c03rad0r change requests on #199

1. Encoder: guard total body length against uint8 overflow
   (per-field checks existed but sum could exceed 255)
2. Parser: respect declared bodyLen instead of raw[2:]
   (was reading trailing bytes as TLVs)
3. Docs: update vendor_element_manager description
   (was referencing removed 'Bitcoin/Lightning info')

Addresses items 1, 2, 6 from c03rad0r's consolidated review.
Items 3 (OUI hardware validation), 4 (ubus health check),
5 (JSON via fmt.Sprintf), 7 (map ordering) are informational —
no code change needed for this PR's scope.

* chore: re-trigger CI after rebase

* fix(wgm): encoder overflow check before cast + round-trip tests (#235)

Addresses c03rad0r's change requests:

1. Encoder: moved len(body) > 255 check BEFORE uint8 cast
   (was after, meaning truncation already happened)
2. Added 11 encoder/parser unit tests:
   - 5 encode→decode round-trip tests (minimal, flags, mint, pubkey, full)
   - TestEncodeRejectsOversizedBody (>255 bytes)
   - TestParseRejectsInvalidOUI (wrong OUI)
   - TestParseRejectsTooShort (<8 bytes)
   - TestParseRejectsWrongElementType (wrong elem type byte)
   - TestParseRespectsBodyLen (trailing garbage ignored)

All tests pass.

* fix(#235): address cobrador's CHANGES_REQUESTED feedback

Required fix 3: Replace fmt.Sprintf JSON construction with encoding/json
marshal in SetLocalAPVendorElements. Prevents injection if element values
contain quotes or special characters.

Note on fix 1 (encoder overflow): The check at line 62 already runs
BEFORE the uint8 cast at line 65. The code is correct as-is — the
overflow check prevents the cast from ever seeing a value > 255.

Items 2 (encoder unit tests) and 4 (hardware validation) still pending.
Item 2 requires adding encode→decode round-trip tests.
Item 4 requires real WiFi hardware — documented as follow-up.

* fix(wgm): calibrate vendor IE score boost from cross-platform WiFi research

Research across macOS (-75 dBm trigger, 12 dB hysteresis), iOS (-70 dBm,
8 dB active), Android (band award +40/~10% of max score), wpa_supplicant
(5 dBm rate tolerance), Cisco (6 dB hysteresis), and Aruba (5-10 dB delta
bound) shows the previous +200 boost was 17-40x larger than any production
WiFi steering system.

Changes:
- Vendor IE boost: +200 unconditional → +15 conditional (signal > -75 dBm)
- SSID prefix boost: +100 → +10 (SSID is spoofable, weak identifier)
- New test: weak signal (≤-75 dBm) gets no IE boost
- Updated 3 existing tests with new expected values

* docs: reference #311 in score boost comment

* feat(wgm): discovery logger — structured scan history + tollgate-cli upstream known

Adds structured JSONL logging of every AP discovered during background
scan cycles, with an in-memory registry that tracks known TollGates
across scans (BSSID, SSID, signal range, sample count, latest pricing).

- New: discovery_log.go with DiscoveryLogger (LogScan, Summary, LoadExistingLog)
- New: discovery_log_test.go with 5 tests (all pass)
- Wire: UpstreamManager.runScanCycle calls DiscoveryLog.LogScan after each scan
- CLI: 'upstream scan' output now includes is_tollgate, price_per_step, step_size
- CLI: New 'tollgate-cli upstream known' shows discovery summary
- Logs to /etc/tollgate/discovery_log.jsonl (JSONL format for analysis)
- Foundation for Phase 2 (speed probing) and Phase 3 (advertised pricing) in #311

Depends on #235 (vendor IE parsing populates IsTollGate/PricePerStep).

* fix: log to tmpfs + add speed probe (Phase 2)

Flash protection: discovery log moved from /etc/tollgate/ (flash) to
/tmp/ (tmpfs/RAM). Log survives across scans within a boot but does not
wear out the router flash.

Speed probe: new SpeedProbe(gatewayIP) function does HTTP GET to
gateway:2121/, measures RTT, and reads response size. Probe results
logged to /tmp/tollgate-discovery.jsonl.probes and tracked in the
in-memory registry (last_rtt_ms, best_rtt_ms, probe_count).

Architecture: two-speed model — we measure link RTT, TollGate advertises
uplink speed. Vendor IE stays static (radio restart required to change).
Dynamic data (price, load, speed) comes from HTTP API (GET / on 2121).

* feat(wgm): wire SpeedProbe into post-switch verification

After UpstreamManager connects to a new AP and DHCP assigns a gateway
IP, the existing probeTollGateGateway() now uses SpeedProbe() instead
of a raw http.Client. This measures RTT and records the result in the
discovery registry via LogProbe(bssid, result).

Changes:
- Candidate struct: added BSSID field, set at both creation sites
- verifyPostSwitchConnectivity: now takes (ssid, bssid)
- probeTollGateGateway: replaced inline HTTP GET with SpeedProbe +
  LogProbe, preserving existing return-bool semantics
- Removed unused net/http import from upstream_manager.go

Flow: scan -> find candidate(BSSID) -> switch -> DHCP -> waitForDefaultRoute
      -> SpeedProbe(gatewayIP) -> LogProbe(bssid, result) -> registry

RTT data now accumulates per-TollGate alongside signal history.

* fix: call LoadExistingLog on startup so registry survives restarts

Without this, the in-memory discovery registry starts empty after every
backend restart, even though /tmp/tollgate-discovery.jsonl persists.
LoadExistingLog reads the JSONL back into the registry on Start().

* fix: add log rotation to prevent unbounded tmpfs growth

DiscoveryLogMaxEntries constant was declared but never enforced. On a
router scanning every 30s, the JSONL file would grow without bound in
/tmp/ (tmpfs), eventually exhausting RAM.

Replaced with size-based rotation: when the file exceeds 1MB, it is
renamed to .old and a fresh file starts. The in-memory registry retains
all historical data across rotations. LoadExistingLog reads only the
current file (plus .old if present).

* chore: gofmt files touched by this branch

* fix(wgm): persistent discovery log path + batch-accurate reload counting

- DefaultDiscoveryLogPath /tmp/tollgate-discovery.jsonl →
  /etc/tollgate/discovery_log.jsonl: /tmp is tmpfs on OpenWrt, so
  discovery history vanished on every reboot; path now matches the
  CHANGELOG (rotation already caps growth).
- LoadExistingLog counted one scan per JSONL line; entries of one scan
  batch share a timestamp, so count distinct batch timestamps instead —
  TotalScans reflects scan cycles after restart reload. Regression test
  covers 4 lines / 2 batches.
- CHANGELOG: is_tollgate/price_per_step/step_size documented as
  placeholders until vendor-IE discovery (#332) populates them.

Review findings: cold-code review 2026-08-17 items 2b/2c.

* docs: fix stale SSID format comment

Update example from old 'TollGate-ABCD-2.4GHz-1' (band suffix)
to simplified 'TollGate-A1B2' (random chars only).

---------

Co-authored-by: Amperstrand <amperstrand@localhost>
Co-authored-by: amperstand <atlas@oh-my-opencode.com>
Co-authored-by: Sisyphus <sisyphus@opencode.local>
Co-authored-by: c03rad0r <1100745+c03rad0r@users.noreply.github.com>
@felixfelix-bot

Copy link
Copy Markdown
Contributor

Closing — rebased onto main as #353. Conflicts resolved, build verified.

@felixfelix-bot

Copy link
Copy Markdown
Contributor

SUPERSEDED by #353 — this branch was rebased onto main as rebase/pr-350 and a clean PR was opened at #353. Please close this PR. The original branch is on Amperstrand's fork and cannot be rebased by felixfelix-bot.

@c03rad0r
c03rad0r self-requested a review August 20, 2026 12:29
Amperstrand pushed a commit to Amperstrand/tollgate-module-basic-go that referenced this pull request Aug 21, 2026
Surgical rebuild after the merge wave: OpenTollGate#312's squash already carried
this PR's vendor-IE content to main (encode/decode/scoring, staging
markers, tests — verified zero semantic delta in
vendor_element_manager.go, vendor_ie_encode_test.go, scanner.go).
The only content main still lacked is this schema fix: a Required
key with Default false rejected every existing config predating the
key; optional-with-default upgrades cleanly. Includes gofmt of the
file (converges with OpenTollGate#341's repo-wide pass).

Cold-review item from the OpenTollGate#332 round (minor 3).
@Amperstrand Amperstrand changed the title feat(wgm): vendor IE discovery + calibrated score boost (v2, rebased) fix(config): vendor_ie_discovery not Required — 1-line schema fix Aug 21, 2026
@Amperstrand

Copy link
Copy Markdown
Collaborator Author

Rebuilt onto post-wave main (81dd1da) — and shrunk to what main actually still needs.

Auditing the residual after the merge wave: #312's squash already carried this PR's vendor-IE content to main — encode/decode/scoring (vendor_element_manager.go), the tests (vendor_ie_encode_test.go), and the scanner staging markers (scanner.go) all show zero semantic delta vs this branch's reviewed tip (e80cec2). The rebase itself was impossible (the branch's history carries files inside mislabeled commits — same pathology the ELF binary rode in on), so this is a content-first rebuild.

What remains is exactly one line — the cold-review schema fix: vendor_ie_discovery Required: true → false. A Required key with Default false rejected every existing config predating the key; optional-with-default upgrades cleanly. Plus gofmt of that file (converges with #341's repo-wide pass) and a CHANGELOG entry.

The rest of the original PR's scope (wiring the pipeline, SetLocalAPVendorElements error handling, Pubkey roundtrip assertion) stays deferred to the v0.7.x follow-up as agreed in the merge-order round.

felixfelix-bot pushed a commit to felixfelix-bot/tollgate-module-basic-go that referenced this pull request Sep 20, 2026
…penTollGate#312)

* fix(ci): make TollGate OS trigger non-blocking and main-only

The repository-dispatch to OpenTollGate/tollgate-os fails when the
REPO_ACCESS_TOKEN is expired or the downstream repo is unavailable.
This is a notification step — all packaging jobs succeed regardless.

- Added continue-on-error: true so dispatch failure doesn't fail CI
- Added if: github.ref == 'refs/heads/main' so it only runs on main

* feat(wgm): re-implement vendor IE discovery from wire format spec

Replaces the old dead-code bypass (which referenced undefined
bitcoinOUI/nostrOUI) with a clean TollGate vendor IE implementation:

- EncodeTollGateVendorIE: builds 802.11 vendor-specific IE hex string
  (DD <len> 212121 01 <version> <flags> [TLV mint_url] [TLV pubkey])
- ParseTollGateVendorIE: extracts TollGateAdvertisement from raw IE bytes
- ParseVendorIEsFromScanData: walks raw IE bytes from iw scan output
- EmitTollGateVendorIE: emits via ubus set_vendor_elements on hostapd
- ExecuteUbus method added to Connector

Config: vendor_ie_discovery flag in upstream_wifi section (default false).
Types: TollGateAdvertisement struct, IsTollGate field on NetworkInfo.

Wire format and tier structure based on commit 70e4320's design.

Fixes Amperstrand#47.

* fix: add vendor_ie_discovery to config schema

The contract-lint and config_manager tests failed because the new
VendorIEDiscovery field was added to UpstreamWifiConfig but not to
the schema definition in config_schema.go.

* test(wgm): comprehensive vendor IE encode/decode tests

18 tests covering:
- Encode→Parse roundtrip (basic, all flag combos, with TLV fields)
- Edge cases: empty MintURL/Pubkey, max-length rejection
- Malformed input: wrong OUI, wrong elem type, non-DD element
- Truncated data: too short, truncated TLV value
- Multi-IE scan data: multiple TollGate IEs, non-vendor IEs interspersed
- Truncated scan data at end
- calculateScore: TollGate SSID bonus, IsTollGate flag bonus, combined

* fix(wgm): address c03rad0r change requests on OpenTollGate#199

1. Encoder: guard total body length against uint8 overflow
   (per-field checks existed but sum could exceed 255)
2. Parser: respect declared bodyLen instead of raw[2:]
   (was reading trailing bytes as TLVs)
3. Docs: update vendor_element_manager description
   (was referencing removed 'Bitcoin/Lightning info')

Addresses items 1, 2, 6 from c03rad0r's consolidated review.
Items 3 (OUI hardware validation), 4 (ubus health check),
5 (JSON via fmt.Sprintf), 7 (map ordering) are informational —
no code change needed for this PR's scope.

* chore: re-trigger CI after rebase

* fix(wgm): encoder overflow check before cast + round-trip tests (OpenTollGate#235)

Addresses c03rad0r's change requests:

1. Encoder: moved len(body) > 255 check BEFORE uint8 cast
   (was after, meaning truncation already happened)
2. Added 11 encoder/parser unit tests:
   - 5 encode→decode round-trip tests (minimal, flags, mint, pubkey, full)
   - TestEncodeRejectsOversizedBody (>255 bytes)
   - TestParseRejectsInvalidOUI (wrong OUI)
   - TestParseRejectsTooShort (<8 bytes)
   - TestParseRejectsWrongElementType (wrong elem type byte)
   - TestParseRespectsBodyLen (trailing garbage ignored)

All tests pass.

* fix(OpenTollGate#235): address cobrador's CHANGES_REQUESTED feedback

Required fix 3: Replace fmt.Sprintf JSON construction with encoding/json
marshal in SetLocalAPVendorElements. Prevents injection if element values
contain quotes or special characters.

Note on fix 1 (encoder overflow): The check at line 62 already runs
BEFORE the uint8 cast at line 65. The code is correct as-is — the
overflow check prevents the cast from ever seeing a value > 255.

Items 2 (encoder unit tests) and 4 (hardware validation) still pending.
Item 2 requires adding encode→decode round-trip tests.
Item 4 requires real WiFi hardware — documented as follow-up.

* fix(wgm): calibrate vendor IE score boost from cross-platform WiFi research

Research across macOS (-75 dBm trigger, 12 dB hysteresis), iOS (-70 dBm,
8 dB active), Android (band award +40/~10% of max score), wpa_supplicant
(5 dBm rate tolerance), Cisco (6 dB hysteresis), and Aruba (5-10 dB delta
bound) shows the previous +200 boost was 17-40x larger than any production
WiFi steering system.

Changes:
- Vendor IE boost: +200 unconditional → +15 conditional (signal > -75 dBm)
- SSID prefix boost: +100 → +10 (SSID is spoofable, weak identifier)
- New test: weak signal (≤-75 dBm) gets no IE boost
- Updated 3 existing tests with new expected values

* docs: reference OpenTollGate#311 in score boost comment

* feat(wgm): discovery logger — structured scan history + tollgate-cli upstream known

Adds structured JSONL logging of every AP discovered during background
scan cycles, with an in-memory registry that tracks known TollGates
across scans (BSSID, SSID, signal range, sample count, latest pricing).

- New: discovery_log.go with DiscoveryLogger (LogScan, Summary, LoadExistingLog)
- New: discovery_log_test.go with 5 tests (all pass)
- Wire: UpstreamManager.runScanCycle calls DiscoveryLog.LogScan after each scan
- CLI: 'upstream scan' output now includes is_tollgate, price_per_step, step_size
- CLI: New 'tollgate-cli upstream known' shows discovery summary
- Logs to /etc/tollgate/discovery_log.jsonl (JSONL format for analysis)
- Foundation for Phase 2 (speed probing) and Phase 3 (advertised pricing) in OpenTollGate#311

Depends on OpenTollGate#235 (vendor IE parsing populates IsTollGate/PricePerStep).

* fix: log to tmpfs + add speed probe (Phase 2)

Flash protection: discovery log moved from /etc/tollgate/ (flash) to
/tmp/ (tmpfs/RAM). Log survives across scans within a boot but does not
wear out the router flash.

Speed probe: new SpeedProbe(gatewayIP) function does HTTP GET to
gateway:2121/, measures RTT, and reads response size. Probe results
logged to /tmp/tollgate-discovery.jsonl.probes and tracked in the
in-memory registry (last_rtt_ms, best_rtt_ms, probe_count).

Architecture: two-speed model — we measure link RTT, TollGate advertises
uplink speed. Vendor IE stays static (radio restart required to change).
Dynamic data (price, load, speed) comes from HTTP API (GET / on 2121).

* feat(wgm): wire SpeedProbe into post-switch verification

After UpstreamManager connects to a new AP and DHCP assigns a gateway
IP, the existing probeTollGateGateway() now uses SpeedProbe() instead
of a raw http.Client. This measures RTT and records the result in the
discovery registry via LogProbe(bssid, result).

Changes:
- Candidate struct: added BSSID field, set at both creation sites
- verifyPostSwitchConnectivity: now takes (ssid, bssid)
- probeTollGateGateway: replaced inline HTTP GET with SpeedProbe +
  LogProbe, preserving existing return-bool semantics
- Removed unused net/http import from upstream_manager.go

Flow: scan -> find candidate(BSSID) -> switch -> DHCP -> waitForDefaultRoute
      -> SpeedProbe(gatewayIP) -> LogProbe(bssid, result) -> registry

RTT data now accumulates per-TollGate alongside signal history.

* fix: call LoadExistingLog on startup so registry survives restarts

Without this, the in-memory discovery registry starts empty after every
backend restart, even though /tmp/tollgate-discovery.jsonl persists.
LoadExistingLog reads the JSONL back into the registry on Start().

* fix: add log rotation to prevent unbounded tmpfs growth

DiscoveryLogMaxEntries constant was declared but never enforced. On a
router scanning every 30s, the JSONL file would grow without bound in
/tmp/ (tmpfs), eventually exhausting RAM.

Replaced with size-based rotation: when the file exceeds 1MB, it is
renamed to .old and a fresh file starts. The in-memory registry retains
all historical data across rotations. LoadExistingLog reads only the
current file (plus .old if present).

* chore: gofmt files touched by this branch

* fix(wgm): persistent discovery log path + batch-accurate reload counting

- DefaultDiscoveryLogPath /tmp/tollgate-discovery.jsonl →
  /etc/tollgate/discovery_log.jsonl: /tmp is tmpfs on OpenWrt, so
  discovery history vanished on every reboot; path now matches the
  CHANGELOG (rotation already caps growth).
- LoadExistingLog counted one scan per JSONL line; entries of one scan
  batch share a timestamp, so count distinct batch timestamps instead —
  TotalScans reflects scan cycles after restart reload. Regression test
  covers 4 lines / 2 batches.
- CHANGELOG: is_tollgate/price_per_step/step_size documented as
  placeholders until vendor-IE discovery (OpenTollGate#332) populates them.

Review findings: cold-code review 2026-08-17 items 2b/2c.

* docs: fix stale SSID format comment

Update example from old 'TollGate-ABCD-2.4GHz-1' (band suffix)
to simplified 'TollGate-A1B2' (random chars only).

---------

Co-authored-by: Amperstrand <amperstrand@localhost>
Co-authored-by: amperstand <atlas@oh-my-opencode.com>
Co-authored-by: Sisyphus <sisyphus@opencode.local>
Co-authored-by: c03rad0r <1100745+c03rad0r@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants