Author SHA1 Message Date
evgeny 962a2df3f8 Add local channel deletion to desktop channel settings 2026-10-26 10:47:39 +01:00
evgeny d67e0b0c23 Report desktop startup failures and validate generated configs 2026-10-26 10:12:30 +01:00
evgeny 9ab9d01907 Fix Android config storage limits for the strict parser 2026-10-26 10:12:30 +01:00
evgeny 4abebdf35c Record passing stream publisher checks after integration 2026-10-26 09:58:45 +01:00
evgeny ced9dc2340 Merge remote-tracking branch 'origin/master' 2026-10-26 09:50:34 +01:00
evgeny 8627e4f6a5 Record stream packing fix and native regression coverage 2026-10-26 09:50:34 +01:00
evgeny a056b2c262 Describe publisher packing budgets and queued audio rejection 2026-10-26 09:50:34 +01:00
evgeny 017a326a13 Cover stream publisher packing boundaries and actor progress 2026-10-26 09:46:24 +01:00
evgeny 1a9fe0a33b Fix stream publisher audio packing and video progress 2026-10-26 09:44:16 +01:00
evgeny 8e74619501 Make chat joins atomic and test invitation lifecycle failures 2026-10-26 09:19:35 +01:00
evgeny bb6ed1723f Close instance audit with full integration and sanitizer results 2026-10-26 09:09:00 +01:00
evgeny 435ca204fe Clarify that repeated core start preserves the application running flag 2026-10-26 09:08:42 +01:00
evgeny 8640b45e99 Record instance lifecycle fixes, contracts and sanitizer audit evidence 2026-10-26 09:08:42 +01:00
evgeny 4219d5ce1d Specify reload behavior for repeated server sections in public header 2026-10-26 09:08:42 +01:00
evgeny 85e87cc9f8 Read radio hop list through aligned storage 2026-10-26 09:08:42 +01:00
evgeny e577daf6ab Describe conservative reload for duplicate and resolved IPv6 selectors 2026-10-26 09:08:42 +01:00
evgeny 0d6d1aeb19 Restart instances with duplicate server names to preserve all endpoints 2026-10-26 09:08:42 +01:00
evgeny a58f3516ab Clarify first-opened file logger ownership across instances 2026-10-26 09:08:42 +01:00
evgeny 44103038c0 Preserve selected global log path and verify database owner reload 2026-10-26 09:08:42 +01:00
evgeny 06ad255b95 Align instance guide and core lifecycle documentation with audited code 2026-10-26 09:08:42 +01:00
evgeny 9201d8c4f7 Use aligned PCM samples in headless radio integration fixture 2026-10-26 09:08:42 +01:00
evgeny ceaf71caf8 Align headless PCM before passing frames to Opus 2026-10-26 09:08:42 +01:00
evgeny 817e3744ee Reclaim cancelled heap timers before checking async resource counters 2026-10-26 09:08:42 +01:00
evgeny 68ead8c7dc Expose remaining async resource owners in instance regression test 2026-10-26 09:08:42 +01:00
evgeny 59f900810e Check headless clients, timer rollback and external subscription cleanup 2026-10-26 09:08:42 +01:00
evgeny 0d14613244 Document instance ownership, lifecycle, reload and callback contracts 2026-10-26 09:08:42 +01:00
evgeny f5488adab0 Release headless audio contexts, clients and timers on chat stop 2026-10-26 09:08:42 +01:00
evgeny fd93f39c78 Release remaining transport subscriptions at instance destruction 2026-10-26 09:08:42 +01:00
evgeny 1d92591780 Validate headless bindings and honor ephemeral ports in chat startup 2026-10-26 09:08:42 +01:00
evgeny 234c21fb6a Report outstanding allocations before instance teardown assertion 2026-10-26 09:08:42 +01:00
evgeny 88801318bb Include complete ETCP connection type in instance contract test 2026-10-26 09:08:42 +01:00
evgeny 9992228dcb Cover instance callbacks, constructor failures and selective/full reload 2026-10-26 09:08:42 +01:00
evgeny 4e32de8e65 Drain instance-owned sockets and registries without saved callback neighbors 2026-10-26 09:08:42 +01:00
evgeny a7d2333deb Reload complete endpoint settings and restart changed resource owners 2026-10-26 09:08:42 +01:00
evgeny 5e63bc2a42 Share atomic network index construction and typed socket teardown 2026-10-26 09:08:42 +01:00
evgeny b44ea19b4b Close config parser audit after the full integration suite 2026-10-26 09:08:42 +01:00
evgeny d681d5abf0 Record parser boundary, rollback and sanitizer audit evidence 2026-10-26 09:08:24 +01:00
evgeny 7ea1fa9954 Validate instance construction and retain the global log path safely 2026-10-26 09:08:24 +01:00
evgeny 372b7d7f65 Retain removed instance callbacks until nested dispatch completes 2026-10-26 09:08:24 +01:00
evgeny e5e61723d8 Inject config read faults through fortified fread as well 2026-10-26 09:08:24 +01:00
evgeny 7fe77a49c9 Restore console diagnostics after parser dump assertions 2026-10-26 09:08:24 +01:00
evgeny 9888820146 Describe parser validation, list order and service defaults 2026-10-26 09:08:23 +01:00
evgeny 9a7091b595 Document strict parser ownership and validate provided peer keys 2026-10-26 09:08:23 +01:00
evgeny 1041849030 Reject oversized service strings and cover scalar bounds 2026-10-26 09:08:23 +01:00
evgeny eff0446f8d Align boolean settings and timer bounds across parser and updater 2026-10-26 09:08:23 +01:00
evgeny e4852c82b4 Check service scalars and proxy ports before storing configuration 2026-10-26 09:08:23 +01:00
evgeny 99d52afd3f Validate global scalar options before narrowing or timer conversion 2026-10-26 09:08:23 +01:00
evgeny 4abeafa20e Normalize malformed key duplicates before strict config verification 2026-10-26 09:08:23 +01:00
evgeny 70b7823993 Use debug and socket test APIs with their declared contracts 2026-10-26 09:08:23 +01:00
evgeny 46bf5b8541 Test parser rejection, CIDR boundaries and allocation rollback 2026-10-26 09:08:23 +01:00
evgeny d21af324ac Validate network keys before use and keep secret keys out of dumps 2026-10-26 09:08:23 +01:00
evgeny f581fa423b Reject configuration when an option handler or allocation fails 2026-10-26 09:08:23 +01:00
evgeny bff354f9cc Validate complete protocol versions, network keys and endpoint ports 2026-10-26 09:08:23 +01:00
evgeny 7593120bd9 Parse CIDR, control endpoints and storage sizes without truncation 2026-10-26 09:08:23 +01:00
evgeny 869421cc05 Audit LAN docs and enforce stream member exclusions
Document LAN transport contracts and the completed part of the stream audit. Reject owner-deleted members before profile verification and add network lifecycle coverage for source, supplier and local member removal.

Validation: seven stream checks and all 195 core tests pass; proxy 9/9, burst and load 8/8 pass.
2026-10-26 08:55:31 +01:00
evgeny 6bd4690c3b Date transport audit completion after final sanitizer checks 2026-10-26 00:01:10 +01:00
evgeny 018f8a20f3 Close transport header audit and record integration validation 2026-10-26 00:00:11 +01:00
evgeny 13f379621a Keep route ACK fault tests effective with fortified libc calls 2026-10-25 23:57:53 +01:00
evgeny 8bacf73e86 Intercept fortified recvfrom in socket monitor fault tests 2026-10-25 23:56:40 +01:00
evgeny f327f7f434 Describe REALITY codec, protected stream and relay lifecycle 2026-10-25 23:53:34 +01:00
evgeny 5e7eb6290f Test STUN recheck with an existing and a newly discovered path 2026-10-25 23:52:41 +01:00
evgeny bdf0bdf873 Close REALITY relay cleanly when event monitoring fails 2026-10-25 23:52:41 +01:00
evgeny 2f4830a3d0 Recheck each STUN path once and clarify result validity 2026-10-25 23:51:47 +01:00
evgeny f37ab0824d Complete REALITY relay half-close on socket hangup 2026-10-25 23:49:47 +01:00
evgeny 7e526521eb Document REALITY session ownership and test relay failure cleanup 2026-10-25 23:48:38 +01:00
evgeny 88b05c07d9 Harden REALITY record lifetime, relay cleanup and input contracts 2026-10-25 23:40:56 +01:00
evgeny 47bf724748 Include errno in call ring I/O fault test 2026-10-25 23:37:09 +01:00
evgeny 2949101ff8 Test per-thread call traces, counter wrap and dump I/O errors 2026-10-25 23:30:56 +01:00
evgeny c0365ebb38 Keep teardown traces per thread and retain history across wrap 2026-10-25 23:29:45 +01:00
evgeny 9a13989c7e Document SOCKS ownership and cover empty UDP and input limits 2026-10-25 23:26:24 +01:00
evgeny 36900780f2 Preserve SOCKS stream data and make control startup cancellable 2026-10-25 23:25:16 +01:00
evgeny 6660f59b3f Fix automatic endpoint ownership, retries and scan teardown 2026-10-25 23:16:17 +01:00
evgeny f8c299c2e3 Test socket monitor address loss, callbacks and init faults 2026-10-25 22:58:54 +01:00
evgeny 9296b36366 Make socket monitoring bounded and cancel pending notifications 2026-10-25 22:56:45 +01:00
evgeny fe6b79127f Record passing packet dump sanitizer validation 2026-10-25 22:46:21 +01:00
evgeny dd81f7a665 Record packet dump fix and FreeBSD socket monitor finding 2026-10-25 22:44:55 +01:00
evgeny ce30ec5fe6 Align packet dump and CRC32 contracts with implementations 2026-10-25 22:43:43 +01:00
evgeny c3fa6d4db1 Exercise packet dump limits, protocols and fragment output 2026-10-25 22:32:22 +01:00
evgeny 235c2c4f4d Bound IPv4 packet dump reads and handle fragments 2026-10-25 22:32:22 +01:00
evgeny 9fb939ff24 Document unsafe packet dump reads found during audit 2026-10-25 22:32:22 +01:00
evgeny e035f53ab6 Fix stale socket references in connectivity probes 2026-10-25 22:30:52 +01:00
evgeny 8b0002e489 Disable UTUN by default in chat frontends and retain core bootstrap transports 2026-10-25 22:10:06 +01:00
evgeny fcc4a2cf42 Ignore zero-port addresses in connection manager candidates 2026-10-25 21:01:03 +01:00
evgeny 539f09004c Use channel member snapshots for desktop participant details 2026-10-25 20:04:59 +01:00
evgeny c832a5d7a5 Unify member snapshots and fix Android member status flicker 2026-10-25 19:29:44 +01:00
evgeny f09eb81381 Fix NAT stress test time helper on Windows 2026-10-25 18:24:07 +01:00
evgeny 59b3b9bd34 Check remote and local member-file ACL refusals separately 2026-10-25 18:10:58 +01:00
evgeny b3d3df5c57 Exercise signed LAN endpoints over isolated IPv6 interfaces 2026-10-25 18:06:46 +01:00
evgeny e75bd460b8 Avoid identity reload when removing expired LAN candidates 2026-10-25 18:00:56 +01:00
evgeny 77b43363fb Run signed LAN integration separately from other network tests 2026-10-25 18:00:01 +01:00
evgeny 199839b273 Cover socket reuse and TCP LAN recovery and document lifecycle 2026-10-25 17:58:18 +01:00
evgeny 8dd28fb90a Distinguish LAN nonce failures from ordinary unknown devices 2026-10-25 17:55:35 +01:00
evgeny 94a76e569e Respect SOCKS policy and report LAN lookup failures precisely 2026-10-25 17:55:34 +01:00
evgeny 09c7e0c937 Test signed LAN discovery, replay rejection and NCD recovery 2026-10-25 17:52:08 +01:00
evgeny 469f95c596 Apply nonce-verified LAN endpoints to offline NCD transports 2026-10-25 17:47:08 +01:00
evgeny 88c2d7a4e3 Share LAN interface matching for socket announcements and NCD 2026-10-25 17:44:52 +01:00
evgeny e8713e7a2c Load known node identities for verified LAN candidates 2026-10-25 17:43:21 +01:00
evgeny 28c447370a Add signed LAN endpoint announcements and nonce proofs 2026-10-25 17:42:40 +01:00
evgeny f276fda411 Restore Android core activity after internal restart 2026-10-25 17:20:42 +01:00
evgeny c26b626c35 Handle comments consistently in desktop socket protocol settings 2026-10-25 16:35:54 +01:00
evgeny 7bcdccc4d6 Verify autosocket protocol modes across IPv4 and IPv6 lifecycle 2026-10-25 16:35:54 +01:00
evgeny fc8a06547d Test automatic socket protocol configuration and Android port reuse 2026-10-25 16:35:54 +01:00
evgeny b1a07df606 Translate automatic socket protocol settings 2026-10-25 16:35:54 +01:00
evgeny 9d465bce16 Add Android autosocket protocol selection to network settings 2026-10-25 16:35:54 +01:00
evgeny eda65ece52 Generate Android autosockets per selected protocol and retain ports 2026-10-25 16:35:54 +01:00
evgeny ae85423e39 Add automatic socket protocol selector to desktop settings 2026-10-25 16:35:54 +01:00
evgeny f3bc5acacc Persist automatic socket protocols in desktop configuration 2026-10-25 16:35:54 +01:00
evgeny 556f6e0b9e Add selectable protocols for automatic transport sockets 2026-10-25 16:35:54 +01:00
evgeny 270b9fcf80 Merge remote-tracking branch 'origin/master' 2026-10-25 16:30:55 +01:00
evgeny 288b8091ce Document targeted recovery and retain scheduler diagnostics 2026-10-25 16:26:14 +01:00
evgeny 46f6c03147 Keep requests queued by callbacks for the next scheduler batch 2026-10-25 16:19:40 +01:00
evgeny 87dd27d022 Test targeted chat synchronization across peer events and service lifecycle 2026-10-25 16:13:53 +01:00
evgeny 2eade0a7c0 Scope deferred chat synchronization to channel and peer 2026-10-25 16:11:11 +01:00
evgeny 0e17756e19 Show stream playback buffer chains on desktop and Android
Send coalesced per-hop telemetry every 200 ms ahead of video DATA using the shared ETCP transport. Measure pending frames before ETCP handoff, retain stale and unavailable node values, and display the worst path with playback tempo.

Bump stream wire protocol to v7. Validate with 187 core tests, 21 desktop player tests, two Android indicator tests, and an Android APK build.
2026-10-25 15:21:23 +01:00
evgeny 5b5221189c Buffer stream PCM before playback and recover underruns without resets 2026-10-25 12:51:19 +01:00
evgeny 677f79881c Diagnose stream audio underruns and fixture timing jitter 2026-10-25 12:30:30 +01:00
evgeny 3ffacbfe37 Document stream recovery validation and existing Qt audio flake 2026-10-25 12:06:35 +01:00
evgeny c3cf86b9e0 Recover streams after five seconds and start from complete keys
Preserve queued media under backpressure, advertise retained keyframe ranges in stream wire v6, and reopen stalled subflows without resetting playback.

Cover large fragmented keys, sustained slow delivery, missing subflows, buffer eviction and stale bootstrap snapshots in integration regressions.
2026-10-25 12:03:49 +01:00
evgeny 4a785c2598 Improve stream recovery, delivery diagnostics and buffer indicators
Preserve essential FEC parts under pressure, coordinate stale-data gaps and reopen source subflows in parallel after transport failures.

Add per-peer network and playback diagnostics, Android and desktop buffer indicators, and regression coverage for Wi-Fi switching and congestion.
2026-10-25 01:18:02 +02:00
evgeny b7d74d8f87 Verify every NCD control ID byte and clarify new-socket scope 2026-10-24 20:27:57 +02:00
evgeny db0d7a1f47 Document NCD startup rollback reentrant lifetime and address snapshots 2026-10-24 20:24:22 +02:00
evgeny 9844fdefcb Align automatic socket documentation with shared NCD link matching 2026-10-24 20:24:22 +02:00
evgeny a353797edb Specify NCD ownership callbacks matching and fixed little-endian control wire 2026-10-24 20:22:45 +02:00
evgeny b83a28becc Keep NAT remeasurement fixture isolated from new direct helper links 2026-10-24 20:20:09 +02:00
evgeny a3f004772a Align ETCP fixtures with NCD identity and failed-open contracts 2026-10-24 20:20:09 +02:00
evgeny 702389d8d9 Retain NCD entry and transport through synchronous CLOSE callbacks 2026-10-24 20:18:15 +02:00
evgeny 88004f11ca Trigger NCD CLOSE on an empty unsuspended queue 2026-10-24 20:17:13 +02:00
evgeny 1f6f815917 Reproduce NCD teardown during synchronous CLOSE queue notification 2026-10-24 20:15:30 +02:00
evgeny 2a7266e17f Check IPv6 scope on the matching NCD link 2026-10-24 20:15:30 +02:00
evgeny e137778ba2 Exercise IPv6 REALITY guards and reentrant node snapshot replacement 2026-10-24 20:15:30 +02:00
evgeny 496a3c788e Cover NCD candidate policy constructor cancellation and nested removal 2026-10-24 20:15:30 +02:00
evgeny 772c0728e1 Use one NCD address matcher for opens updates and new sockets 2026-10-24 20:15:30 +02:00
evgeny b23d5e6047 Unify NCD opens and retain owners across failed startup and callbacks 2026-10-24 20:15:30 +02:00
evgeny 4695fe4371 Register NCD transport events atomically and reuse ETCP close lifecycle 2026-10-24 20:15:30 +02:00
evgeny 28c951ed9e Reject stale NCD notifications and validate complete control frames 2026-10-24 20:15:30 +02:00
evgeny 2cc5a6df91 Reproduce NCD startup failures stale events and malformed control 2026-10-24 20:15:30 +02:00
evgeny 10d6ddee38 Wait for target discovery before member file network transfers 2026-10-24 20:15:30 +02:00
evgeny 4314dff390 Expose CM and topology failures in member file network diagnostics 2026-10-24 20:14:58 +02:00
evgeny c311bb7a1f Prevent deferred CM readiness from restarting cancelled file transfers 2026-10-24 20:14:58 +02:00
evgeny b387c4b127 Preserve unowned member download temporaries on refusal and cancellation 2026-10-24 20:14:29 +02:00
evgeny 6d6afb4faa Align FULL and inline loss counters in dummynet documentation 2026-10-24 19:57:24 +02:00
evgeny e996e09f6e Specify dummynet datagram timing reconfiguration and filter ownership contracts 2026-10-24 19:57:24 +02:00
evgeny 8d035a065a Cover partial dummynet startup rollback without resource leaks 2026-10-24 19:57:24 +02:00
evgeny 0df339c384 Verify inline filters with real UDP delivery and independent ETCP teardown 2026-10-24 19:57:24 +02:00
evgeny c341a9b6cc Record passing full validation after ETCP diagnostics merge 2026-10-24 19:45:37 +02:00
evgeny 897cc007e1 Merge current master and reconcile completed member-files fixes 2026-10-24 19:38:40 +02:00
evgeny 6c9be8b134 Record full-suite failure deleting an unowned member-files temp 2026-10-24 19:26:27 +02:00
evgeny cfa0419576 Record repeated slow-duplex failures with native and real clocks 2026-10-24 19:09:34 +02:00
evgeny f8a2ecaabd Add isolated slow-duplex measurements and record BBR diagnosis 2026-10-24 19:07:18 +02:00
evgeny 79e37f64f8 Document member files audit findings and verified fixes 2026-10-24 18:47:06 +02:00
evgeny 4d729a2ff0 docs(member-files): align lifecycle and receipt retry behavior 2026-10-24 18:47:06 +02:00
evgeny 4f96926f6b docs(member-files): complete public request and transfer contracts 2026-10-24 18:47:06 +02:00
evgeny c1da55ad87 docs(member-files): clarify storage lifecycle and internal owners 2026-10-24 18:47:06 +02:00
evgeny f7b064a11a docs(member-files): specify wire codec and worker ownership 2026-10-24 18:47:06 +02:00
evgeny 41f0aa2439 fix(member-files): reject local FIFO without blocking disk worker 2026-10-24 18:47:06 +02:00
evgeny 1c1b2b2026 test(member-files): detect blocking open of local FIFO sources 2026-10-24 18:47:06 +02:00
evgeny 488577ac0e test(member-files): verify timed replay and bulk removal 2026-10-24 18:47:06 +02:00
evgeny 4d40b6f5f7 fix(member-files): bound busy retries by receipt lifetime 2026-10-24 18:47:06 +02:00
evgeny 2d8384830d test(member-files): cover bulk mutations and lost result replay 2026-10-24 18:47:06 +02:00
evgeny 0c8e9eef3c fix(member-files): retry busy mutations within replay guarantees 2026-10-24 18:47:06 +02:00
evgeny 20b4975d57 fix(member-files): prevent restart after early transfer cancellation 2026-10-24 18:47:06 +02:00
evgeny 0f42f92feb fix(member-files): clean up only owned download temporaries 2026-10-24 18:47:06 +02:00
evgeny 0b2dac3c5b test(member-files): retain route precondition after ABI diagnosis 2026-10-24 18:47:06 +02:00
evgeny 5df6d0c8f3 test(member-files): log startup state before transfer 2026-10-24 18:47:06 +02:00
evgeny a198cfb9e3 Bind inline dummynet filters to UDP link lifetime and report failures 2026-10-24 17:33:59 +02:00
evgeny 83bf76dd41 Release borrowed send-hook owners before ETCP link teardown 2026-10-24 17:33:59 +02:00
evgeny 36c096c4e9 Make UDP dummynet configuration and resource failures recoverable 2026-10-24 17:33:59 +02:00
evgeny 29d54ed20d Reproduce dummynet configuration datagram and timer failures 2026-10-24 17:33:59 +02:00
evgeny e59ef240e8 test(member-files): let transfer own initial route request 2026-10-24 17:33:47 +02:00
evgeny 27dd9e7b60 test(member-files): preserve unowned download temp on refusal 2026-10-24 17:33:47 +02:00
evgeny 8185b83eba Record final RTT timer, callback and network validation 2026-10-24 17:26:59 +02:00
evgeny 76afc56be8 Make RTT callback regression independent of host uptime 2026-10-24 17:26:59 +02:00
evgeny 5420a07ffc Refresh RTO before RTT callbacks can close the link 2026-10-24 17:26:59 +02:00
evgeny f00d67ca3c Trace refreshed RTO deadlines without per-packet debug noise 2026-10-24 17:26:59 +02:00
evgeny 90dff0b2f2 Refresh pending retransmit timer when timestamp RTT changes 2026-10-24 17:26:59 +02:00
evgeny 83734c920d Expose existing RTO timer refresh and regress timestamp shrink 2026-10-24 17:26:59 +02:00
evgeny e1dcee3c86 Record verified RTT filter and RTO recovery fixes 2026-10-24 17:26:59 +02:00
evgeny d5114f36b4 Clarify timestamp RTT role in transport contracts 2026-10-24 17:26:59 +02:00
evgeny 68a225baec Exercise RTT changes through real ETCP and document RTO inputs 2026-10-24 17:26:59 +02:00
evgeny ef404da44c Recover DATA RTT sampling after path delay increases 2026-10-24 17:26:59 +02:00
evgeny dbda8a24d7 Verify RTT hold rounds and document filter timing 2026-10-24 17:26:59 +02:00
evgeny bb462bad69 Preserve BBR RTT filter age and full-width probe deadlines 2026-10-24 17:26:59 +02:00
evgeny deee734e6c Record duplex validation and remaining BBR follow-ups 2026-10-24 17:26:59 +02:00
evgeny 8ff4a0bf46 Document ETCP receiver-clock bandwidth sampling 2026-10-24 17:26:59 +02:00
evgeny d2703b2145 Use receiver delivery clock for ETCP bandwidth samples 2026-10-24 17:26:59 +02:00
evgeny 9b72a0dadf Close media contract audit and record architecture review 2026-10-24 17:26:12 +02:00
evgeny 5e3701bb2f Clarify encrypted attachment MIME and UI contracts 2026-10-24 17:26:12 +02:00
evgeny 4bab1f7ea4 Align media delivery architecture docs with implementation 2026-10-24 17:26:12 +02:00
evgeny 9d9c82fb03 Document validated MIME fix and regression results 2026-10-24 17:26:12 +02:00
evgeny 99c1ee3394 test(media): cover encrypted MIME validation and side effect free rejection 2026-10-24 17:26:11 +02:00
evgeny d88e39fb4e chat: reject unsupported encrypted attachment types before side effects 2026-10-24 17:26:11 +02:00
evgeny f63d640c95 media: share bounded content type validation in encrypted containers 2026-10-24 17:26:11 +02:00
evgeny 86b7f9cf44 docs(media): record accepted but unreadable MIME containers 2026-10-24 17:26:11 +02:00
evgeny f331d177ca docs(media): clarify preparation ownership and active protocol commands 2026-10-24 17:26:11 +02:00
evgeny 28bbd62d74 docs(media): specify attachment metadata and voice file contracts 2026-10-24 17:26:11 +02:00
evgeny c1717d4047 docs(media): record validated SQLite announcement fix 2026-10-24 17:26:11 +02:00
evgeny 4bfdb1dcc2 test(media): restore console after SQL diagnostic assertions 2026-10-24 17:26:11 +02:00
evgeny 6e3be1aaf3 test(media): verify exact announcement identities after SQL recovery 2026-10-24 17:26:11 +02:00
evgeny eea2b22c4e test(media): cover malformed local announcements and SQLite failures 2026-10-24 17:26:11 +02:00
evgeny 81fc4291db media: validate SQLite records before local block announcements 2026-10-24 17:26:11 +02:00
evgeny 1a212489fd docs(media): correct announcement function reference 2026-10-24 17:26:11 +02:00
evgeny 0d616ce1e7 docs(media): record unchecked SQLite announcements and fix plan 2026-10-24 17:26:11 +02:00
evgeny 7ff8e1f15a docs(media): specify crypto and file transfer ownership contracts 2026-10-24 17:26:11 +02:00
evgeny 40e2fe2e27 docs(media): record validated voice duration fix 2026-10-24 17:26:11 +02:00
evgeny 359a9f3dfc test(media): cover voice duration bounds and early rejection 2026-10-24 17:26:11 +02:00
evgeny 68a7695d81 media: reject voice duration by complete frames before encoding 2026-10-24 17:26:11 +02:00
evgeny 6f3d634c77 media: share attachment duration limit 2026-10-24 17:26:11 +02:00
evgeny 4177ab3536 docs(media): record voice duration boundary defect and fix plan 2026-10-24 17:26:11 +02:00
evgeny 80558f6686 Merge remote-tracking branch 'origin/master' 2026-10-24 17:15:42 +02:00
evgeny da5cf6fc99 Prepare signed existing members before end-to-end CHAT joining 2026-10-24 17:00:53 +02:00
evgeny e6e866a41a Seed complete members in isolated database sync fixtures 2026-10-24 17:00:53 +02:00
evgeny e1675f6910 Preserve chat stress failure logs and group admission diagnostics 2026-10-24 16:56:52 +02:00
evgeny bb13824ad8 Complete NAT wire lifecycle scheduling and diagnostic contracts 2026-10-24 16:53:10 +02:00
evgeny 857a4cee2f Exercise NAT startup rollback and UDP timeout allocation failure 2026-10-24 16:52:10 +02:00
evgeny a6444725da Reject UDP probes when their timeout cannot be scheduled 2026-10-24 16:51:34 +02:00
evgeny 4a00da31da Document complete membership queries separately from row presence 2026-10-24 16:50:41 +02:00
evgeny af50fde9d9 Verify CHAT admission excludes placeholders and deleted members 2026-10-24 16:50:41 +02:00
evgeny f1b62f9112 Require active complete membership for CHAT group admission 2026-10-24 16:46:01 +02:00
evgeny 0b4997c5d5 Release removed NAT helper sockets and reject partial startup 2026-10-24 16:40:51 +02:00
evgeny b8dd2b8cc5 Verify media cancellation without reading a closed CM handle 2026-10-24 16:31:49 +02:00
evgeny 6cfd88afcf Reject zero CM request ids before creating peer state 2026-10-24 16:25:27 +02:00
evgeny 8e7fb1f31b Reconcile CM protocol scheduling cache and cleanup contracts 2026-10-24 16:24:30 +02:00
evgeny 00a85a1468 Keep local CM demand after stale control and reject zero requests 2026-10-24 16:22:15 +02:00
evgeny 182fb97970 Remove obsolete CM disconnect and clarify service ownership 2026-10-24 16:22:15 +02:00
evgeny e6a812042f Check CM bootstrap ownership and mandatory monitor timers 2026-10-24 16:17:20 +02:00
evgeny 2f3cf0933c Keep READY intermediaries eligible and test CM allocation failures 2026-10-24 16:14:48 +02:00
evgeny 80451c43be Make CM cleanup allocation-free and reject incomplete startup 2026-10-24 16:09:39 +02:00
evgeny 007f7c8ae8 Preserve group attachment before transport readiness 2026-10-24 16:01:29 +02:00
evgeny f541ab428b Describe BGP commit boundaries and diagnose initialization failures 2026-10-24 15:57:40 +02:00
evgeny 7ebfddfc4b Clarify group processing callbacks and failed route installation 2026-10-24 15:56:36 +02:00
evgeny 56d513ed85 Inject topology pool database registry and radio timer failures 2026-10-24 15:52:39 +02:00
evgeny d1562bdd0e Check that late BGP allocation failure preserves accepted state 2026-10-24 15:51:43 +02:00
evgeny 145dd1e5ab Prepare BGP paths before committing received node state 2026-10-24 15:50:17 +02:00
evgeny ab5c077600 Unwind topology initialization on pool and SQLite failures 2026-10-24 15:49:42 +02:00
evgeny 6b413d67a6 Exercise group creation rollback radio retry and BGP subnets 2026-10-24 15:48:58 +02:00
evgeny 4363a424d0 Reject incomplete groups and refresh BGP subnet ownership 2026-10-24 15:47:28 +02:00
evgeny 086eefed55 Keep both test transports owned while checking invite timeout 2026-10-24 15:41:16 +02:00
evgeny 5e8ff95c99 Verify invite deadlines and transport loss with shared owners 2026-10-24 15:38:42 +02:00
evgeny 21d8b3844e Apply refreshed peer priorities without replacing group requests 2026-10-24 15:36:12 +02:00
evgeny a7ffaf497e Include concrete transport state in invite regression tests 2026-10-24 15:30:37 +02:00
evgeny 9ce0abe5a9 Cover invite response bounds isolation and failure propagation 2026-10-24 15:30:04 +02:00
evgeny 483e89cd79 Validate channel descriptions and isolate invite request ownership 2026-10-24 15:28:44 +02:00
evgeny 4d9e624f8e Reconcile router protocol and architecture with implementation 2026-10-24 15:15:35 +02:00
evgeny bafda5ae3a Merge origin/master with encrypted attachment ownership fixes
Validation: ./build.sh --no-gui -j4 and make check -j4 passed; 186 tests, no failures.
2026-10-24 15:11:50 +02:00
evgeny 09e620e12a Specify router local delivery, callbacks, flow control and reset contracts 2026-10-24 15:10:06 +02:00
evgeny 3bd9b964fa Remove obsolete router state and clarify receive diagnostics 2026-10-24 15:07:41 +02:00
evgeny a258b77240 Cancel pending transit queues when destroying the router 2026-10-24 15:06:48 +02:00
evgeny 936bacbe4b Clarify encrypted output cleanup contract on filesystem errors 2026-10-24 15:00:05 +02:00
evgeny 1227027c2f Record validated encrypted attachment ownership fix and remaining audit scope 2026-10-24 14:59:12 +02:00
evgeny d8089f8d12 Release router unit fixture registry ownership completely 2026-10-24 14:56:23 +02:00
evgeny 4862a52684 Document encrypted attachment preparation publication and recovery ownership 2026-10-24 14:55:24 +02:00
evgeny e40d8de1b2 Keep full router jitter precision in monitor snapshots 2026-10-24 14:54:48 +02:00
evgeny dbf6bb759c Preserve the unsigned range of router RTT jitter 2026-10-24 14:54:48 +02:00
evgeny be0bd224fa Use canonical media base throughout attachment recovery 2026-10-24 14:51:32 +02:00
evgeny 6bf6d99b9c Test service cancellation after encrypted attachment rename 2026-10-24 14:50:32 +02:00
evgeny 7caebc5a11 Verify attachment recovery preserves active temp owners across restarts 2026-10-24 14:48:25 +02:00
evgeny 6c13755a5c Reject invalid local router payloads and report delivery failures 2026-10-24 14:48:04 +02:00
evgeny c6f28571b7 Keep local callback context private and fully detach services 2026-10-24 14:46:33 +02:00
evgeny 76ba7aae6a Cover encrypted file aliases and I/O failures through publication 2026-10-24 14:45:32 +02:00
evgeny 066b895055 Remove closing router channels before notifying services 2026-10-24 14:44:22 +02:00
evgeny a297d2d649 Record validated ACK batching and remaining solo duplex limits 2026-10-24 14:43:03 +02:00
evgeny f09d76e3fd Recover owned encrypted attachment temporaries in both storage trees 2026-10-24 14:43:03 +02:00
evgeny 7737391ded Document absolute ACK scheduling and cross-path precedence 2026-10-24 14:43:03 +02:00
evgeny dc08256025 Clarify due ACK precedence and bounded burst behavior 2026-10-24 14:43:03 +02:00
evgeny 72d7405a97 Serve due path ACKs before DATA on another link 2026-10-24 14:43:03 +02:00
evgeny 73096e8e3e Verify ACK deadlines through link down and removal 2026-10-24 14:43:03 +02:00
evgeny 51c667edd8 Distinguish early ACK packets from legitimate batched entries 2026-10-24 14:43:03 +02:00
evgeny 2edf09fde1 Cover ACK batching deadlines, timer races, and failure cleanup 2026-10-24 14:43:03 +02:00
evgeny 6ab2526966 Align ACK contracts and diagnostics with deadline state 2026-10-24 14:43:03 +02:00
evgeny c1b0772071 Honor absolute ACK deadlines across wakes and piggyback 2026-10-24 14:43:03 +02:00
evgeny 159237a70c Define absolute ACK deadlines and urgent fallback contracts 2026-10-24 14:43:03 +02:00
evgeny c8051619c1 Reproduce premature ACKs before their absolute deadline 2026-10-24 14:43:03 +02:00
evgeny 2b74d2bf4f Record final queue diagnostics validation and observations 2026-10-24 14:43:03 +02:00
evgeny 35bd19b6c0 Observe ACK queue peaks before immediate send 2026-10-24 14:43:03 +02:00
evgeny 7e166c650c Document multipath diagnostics and the ACK deadline defect 2026-10-24 14:43:03 +02:00
evgeny 055ffa27b2 Add offline multipath charts and count timestamped ACKs 2026-10-24 14:43:03 +02:00
evgeny cd3cf38f06 Expose callback and stream progress in multipath snapshots 2026-10-24 14:43:03 +02:00
evgeny 12b0ab437e Capture buffered multipath queue and pacing diagnostics 2026-10-24 14:43:03 +02:00
evgeny a9bbb470c2 Record full validation of retry scheduling changes 2026-10-24 14:43:03 +02:00
evgeny 6894c66f3a Record passing fault matrix and remaining duplex limits 2026-10-24 14:43:03 +02:00
evgeny a2f9b76d10 Record retry scheduler architecture and regression coverage 2026-10-24 14:43:03 +02:00
evgeny 3d40951e66 Clarify input resumption with queued DATA 2026-10-24 14:43:03 +02:00
evgeny 4350b54769 Publish attachment output once and preserve ownership through cancellation 2026-10-24 14:41:39 +02:00
evgeny 92d52d25a8 Prepare encrypted attachments in exclusively owned durable temporaries 2026-10-24 14:40:53 +02:00
evgeny 9e0949434e Preserve video temp mode and test durable file commit boundaries 2026-10-24 14:39:46 +02:00
evgeny aa7a9b74f4 Select ordinary temporary file mode for copies and media blocks 2026-10-24 14:38:54 +02:00
evgeny 12556344b0 Add durable owned temporary file lifecycle and shared publication helpers 2026-10-24 14:38:34 +02:00
evgeny 2e3ab00ce6 Design owned temporary output and durable publication for encrypted attachments 2026-10-24 14:34:00 +02:00
evgeny f92c78a2e0 Merge remote-tracking branch 'origin/master' 2026-10-24 14:33:18 +02:00
evgeny 7c7c316c3f Document atomic local publication and bootstrap cleanup 2026-10-24 14:25:18 +02:00
evgeny 94aaf3c8e3 Align topology model documentation with current runtime contracts 2026-10-24 14:23:50 +02:00
evgeny c282d03f25 Describe NODEINFO byte layout, identity and registry semantics 2026-10-24 14:20:29 +02:00
evgeny f7db49fc97 Cover local topology replacement allocation failures 2026-10-24 14:18:42 +02:00
evgeny 8cc97d2ffe Record reproducible encrypted file alias data loss during media audit 2026-10-24 14:17:35 +02:00
evgeny f9994e11b5 Clarify local update ownership, results and failure contracts 2026-10-24 14:17:13 +02:00
evgeny 1e0281d4d8 Prepare local topology replacements before retiring old state 2026-10-24 14:16:39 +02:00
evgeny 42a58881ca Document SQLite snapshots, projections and membership contracts 2026-10-24 14:13:26 +02:00
evgeny 19c3cc0434 Reject interrupted membership channel queries 2026-10-24 14:10:28 +02:00
evgeny 81e2a9aa4a Return peer lists atomically after successful SQL iteration 2026-10-24 14:09:36 +02:00
evgeny 575a3788e3 Roll back failed channel deletion and node classification 2026-10-24 14:07:28 +02:00
evgeny 461417beac Record completed proxy fixes and architecture validation results 2026-10-24 14:03:56 +02:00
evgeny 26b5da6835 Release partial SQL bootstrap records on allocation failures 2026-10-24 14:03:48 +02:00
evgeny 99608de811 Reject failed topology writes and malformed stored signatures 2026-10-24 14:01:37 +02:00
evgeny 5c4207c145 Keep resolved routing audit details out of the task backlog 2026-10-24 14:00:22 +02:00
evgeny a42dc38874 Check real raw ICMP explicitly alongside deterministic loopback 2026-10-24 14:00:17 +02:00
evgeny 83194b19c7 Use supported TCP proxy exit option in TUN integration fixture 2026-10-24 13:59:29 +02:00
evgeny 14c5ddf864 Propagate failures when storing verified node identities 2026-10-24 13:57:05 +02:00
evgeny f10a6f70c5 Describe checked datagram startup and bounded TCP HUP handling 2026-10-24 13:54:40 +02:00
evgeny e2ee5f2397 Document atomic proxy client creation and instance shutdown order 2026-10-24 13:52:22 +02:00
evgeny 324b289968 Bound topology dump writes and return the actual length 2026-10-24 13:45:08 +02:00
evgeny 5914e26579 Update datagram and TCP exit documentation for explicit ownership 2026-10-24 13:42:20 +02:00
evgeny 2894e06014 Document HUP polling contract and isolate ICMP fault injection 2026-10-24 13:41:16 +02:00
evgeny d0edacb7b9 Test HUP registration failure and teardown with pending TCP writes 2026-10-24 13:39:50 +02:00
evgeny 1ce285c09b Preserve datagram bindings and verify optional ICMP behavior 2026-10-24 13:38:45 +02:00
evgeny 8dcd2fac3b Record topology dump truncation defect found during audit 2026-10-24 13:37:22 +02:00
evgeny bb2ef05157 Roll back required TUN mappings and verify instance startup failures 2026-10-24 13:36:22 +02:00
evgeny c4eddfe50e Cover proxy startup rollback and datagram socket failures 2026-10-24 13:34:51 +02:00
evgeny f0629ea679 Exercise orderly and abrupt TLS shutdown in proxy integration 2026-10-24 13:29:45 +02:00
evgeny 4da9d85545 Honor TCP read backpressure across HUP and restore polling 2026-10-24 13:28:44 +02:00
evgeny d5444fb9bc Check UDP socket operations and roll back incomplete flows 2026-10-24 13:25:43 +02:00
evgeny 50da7fe457 Make TCP proxy construction atomic and retain instance datagrams 2026-10-24 13:24:30 +02:00
evgeny 99be040488 Separate ICMP exit startup from shared context ownership 2026-10-24 13:23:58 +02:00
evgeny 902ffa05b4 Record NODEINFO parsing fix and sanitizer validation 2026-10-24 13:21:42 +02:00
evgeny 051b13e161 docs(proxy): align planned teardown with instance ownership 2026-10-24 13:18:52 +02:00
evgeny 04ace670ed docs(proxy): design error handling and lifecycle fixes 2026-10-24 13:17:33 +02:00
evgeny 99dd48480f Align REALITY round-trip fixture with NODEINFO ownership 2026-10-24 13:09:16 +02:00
evgeny acd9dcd1a8 Cover empty NODEINFO outputs and malformed BGP preservation 2026-10-24 13:03:42 +02:00
evgeny 790978458c Make NODEINFO section parsing independent of requested outputs 2026-10-24 13:01:30 +02:00
evgeny f9a3830416 Add NODEINFO wire parsing and allocation regressions 2026-10-24 12:59:00 +02:00
evgeny 5f51149622 Record routing audit progress and NODEINFO parsing defect 2026-10-24 12:42:47 +02:00
evgeny 0688ffc26d docs(proxy): qualify integration audit results 2026-10-24 12:39:54 +02:00
evgeny ec1fc52da1 docs(proxy): record teardown findings and refine counters 2026-10-24 12:39:09 +02:00
evgeny 5e80a9cc6c docs(proxy): consolidate current protocol and audit coverage 2026-10-24 12:38:27 +02:00
evgeny f003aaa92d docs(proxy): reconcile TCP client server and listener behavior 2026-10-24 12:36:16 +02:00
evgeny bf03348c64 Correct connectivity probe and IPv4 routing documentation 2026-10-24 12:19:22 +02:00
evgeny 0445c38c31 Clarify address profiles and route statistics contracts 2026-10-24 12:18:30 +02:00
evgeny 30a69712fc Align route table and observation documentation 2026-10-24 12:17:44 +02:00
evgeny 27cf6ae365 docs(proxy): document per-instance datagram contexts 2026-10-24 12:16:47 +02:00
evgeny 1ad8ff73ad Document routing helper ownership and wire contracts 2026-10-24 12:16:31 +02:00
evgeny 0bc0d6cc62 Enforce disjoint IPv4 route insertion atomically 2026-10-24 12:15:29 +02:00
evgeny 23cec465bf docs(proxy): specify TCP lifecycle and UDP ownership 2026-10-24 12:14:42 +02:00
evgeny a4abe30b18 docs(proxy): clarify flow control and HTTP parser contracts 2026-10-24 12:13:05 +02:00
evgeny 34d1dcb7c8 Document send scanning, retry rounds, and fast stall recovery 2026-10-24 12:01:20 +02:00
evgeny 93bf846fc4 Preserve retry priority during bounded burst measurements 2026-10-24 11:46:42 +02:00
evgeny c87cc6e4a2 Match the input waiter callback signature 2026-10-24 11:41:39 +02:00
evgeny 1fbd1e466b Verify retry history, stall recovery, and admission callbacks 2026-10-24 11:39:31 +02:00
evgeny b4d5da1df2 Wait for both configured RTOs in queue bypass regression 2026-10-24 11:35:25 +02:00
evgeny ff74ac8295 Scan past blocked DATA while prioritizing sendable retries 2026-10-24 11:31:46 +02:00
evgeny dcd6cd93c4 Select unused retry paths before starting another round 2026-10-24 11:29:13 +02:00
evgeny d439ee7ca7 Track stalled DATA paths and accepted retry rounds safely 2026-10-24 11:28:29 +02:00
evgeny 39ee5b392b Define retry path history and fast stall state contracts 2026-10-24 11:27:48 +02:00
evgeny acde6bda9d Test send queue bypass, retry priority, and bounded admission 2026-10-24 11:22:28 +02:00
evgeny 8167e0e5f5 Record recovery stage checks and repeated stalled-path cycles 2026-10-24 10:43:25 +02:00
evgeny 5a32d08ffd Buffer retry choice diagnostics until measurement completes 2026-10-24 10:32:44 +02:00
evgeny b98b87ea12 Explain retry choices between stalled multipath channels 2026-10-24 10:26:14 +02:00
evgeny 05d3fa30d9 Record path history for the fragment blocking delivery 2026-10-24 10:19:36 +02:00
evgeny 516ed04897 Identify the fragment behind maximum multipath message latency 2026-10-24 10:18:11 +02:00
evgeny 1d295c7d3a Measure multipath recovery stages separately from RTO waits 2026-10-24 10:05:13 +02:00
evgeny 1c6a7e7bf9 Remove speculative early retries from the ETCP plan 2026-10-24 09:56:54 +02:00
evgeny f1f4976222 Record retry recovery status and full regression results 2026-10-24 09:48:19 +02:00
evgeny 14515ca446 Document retry wait policy and fault recovery measurements 2026-10-24 09:46:38 +02:00
evgeny 8dd03c2b5c Wait for another UP path before retrying ETCP DATA 2026-10-24 09:38:20 +02:00
evgeny 412b9bc713 Test retry waiting for another available ETCP path 2026-10-24 09:34:21 +02:00
evgeny ff2860ffbc Record pacer validation and remaining multipath failures 2026-10-24 09:09:42 +02:00
evgeny c7a530092d Check pacer reset callback against its actual contract 2026-10-24 02:54:41 +02:00
evgeny deac10e3f6 Wake ETCP pacer at its first permitted transmit time 2026-10-24 02:49:25 +02:00
evgeny 42c4195f2e Test pacer eligibility boundaries and readiness lifecycle 2026-10-24 02:25:23 +02:00
evgeny ce3d942efe Record multipath baseline and verify pacer wakeup defect 2026-10-24 02:17:38 +02:00
evgeny a22a27b7a9 Record validated signature lifecycle fix and exact contracts 2026-10-24 01:15:49 +02:00
evgeny 7d57b484d8 Focus signature test logs on lifecycle and failure stages 2026-10-24 01:10:12 +02:00
evgeny 44acb5aecf Document streaming signature state ownership and retries 2026-10-24 01:03:24 +02:00
evgeny 5d11f90cd4 Make streaming signature initialization and cleanup safe 2026-10-24 01:02:10 +02:00
evgeny 4824f3523d Test streaming signature ownership and OpenSSL failures 2026-10-24 01:00:36 +02:00
evgeny 5a7823e6ef Record streaming signature OOM defect and completed temp fix 2026-10-24 00:46:40 +02:00
evgeny 5792db996c Clarify crypto key derivation and actual module dependencies 2026-10-24 00:44:29 +02:00
evgeny 16a33ac9f7 Correct remaining crypto examples and streaming signature buffer contract 2026-10-24 00:37:18 +02:00
evgeny 3afd31e561 Align crypto documentation with random CCM nonces and public-key node IDs 2026-10-24 00:35:13 +02:00
evgeny 2715faffff Document receiver-bounded BBR samples and diagnostic input contracts 2026-10-24 00:33:19 +02:00
evgeny 9459b43211 Remove stale pacing and handshake descriptions from ETCP protocol specification 2026-10-24 00:32:42 +02:00
evgeny 964f6cb045 Correct ETCP diagnostic filters and state dump documentation 2026-10-24 00:30:33 +02:00
evgeny bed99abbe9 Clarify STCP client ownership, ping lifetime and handshake byte order 2026-10-24 00:28:58 +02:00
evgeny d6eecd6f1c Document STCP listener arguments, accepted ownership and current handshake phases 2026-10-24 00:27:56 +02:00
evgeny ecdd767934 Align connection documentation with current UDP sessions and keepalive wire format 2026-10-24 00:27:14 +02:00
evgeny 1cf2cc10dd Keep file I/O return-value overview consistent with orphan cleanup API 2026-10-24 00:21:25 +02:00
evgeny 39f9d6a0e9 Keep active block and worker temporaries during media cache cleanup 2026-10-24 00:16:57 +02:00
evgeny e84c66a3d3 Reproduce orphan cleanup during a real encrypted attachment transfer 2026-10-24 00:15:16 +02:00
evgeny e195894048 Protect owned temporary files from concurrent orphan removal 2026-10-24 00:13:18 +02:00
evgeny ee437b9473 Align ETCP header declarations, timestamp units and UDP contracts with code 2026-10-24 00:06:45 +02:00
evgeny 299c613760 Correct ETCP API callback and datagram ownership documentation 2026-10-24 00:06:00 +02:00
evgeny 74b63cc315 Verify ETCP registry identity and failure ownership invariants 2026-10-24 00:01:02 +02:00
evgeny 032f8e83b2 Publish incoming UDP peers before callbacks and preserve accepted ownership 2026-10-23 23:57:51 +02:00
evgeny c7d7789a58 Reuse ETCP registry entries and reject duplicate peer indexing 2026-10-23 23:57:32 +02:00
evgeny fb995db489 Cover UDP accept cancellation with surviving paths and NCD owners 2026-10-23 23:50:39 +02:00
evgeny 942736d604 Check accepted and rejected MTU boundaries in both UDP handshake directions 2026-10-23 23:42:49 +02:00
evgeny b8021efed8 Validate UDP handshake MTU and nonzero path IDs before state updates 2026-10-23 23:41:58 +02:00
evgeny 1260884485 Exercise invalid UDP INIT path identifiers and MTU limits 2026-10-23 23:40:37 +02:00
evgeny 52d9077bb5 Describe retained link scopes and callback-safe RX timeouts 2026-10-23 23:37:44 +02:00
evgeny 519a2ee242 Cover UDP callback deletion, connection close and path replacement 2026-10-23 23:35:55 +02:00
evgeny 6d6ec7e725 Keep RX timeout and link-status callbacks inside retained link scopes 2026-10-23 23:31:16 +02:00
evgeny d0a168f89e Retain ETCP links across callbacks and release closed paths on scope exit 2026-10-23 23:30:43 +02:00
evgeny 5ccd47dd17 Bind incoming UDP INIT identity to the authenticated key 2026-10-23 23:17:25 +02:00
evgeny 2f6ff9303e Reject authenticated INIT requests claiming another node identity 2026-10-23 23:17:01 +02:00
evgeny d9aede4138 Drain pending UDP traffic before snapshotting INIT validation state 2026-10-23 23:13:03 +02:00
evgeny 0860b1e06d Validate the complete UDP INIT request body before reading fields 2026-10-23 23:09:27 +02:00
evgeny 10927b1d08 Allow early wire rejection in INIT request boundary tests 2026-10-23 23:06:23 +02:00
evgeny 3a8cc8a326 Exercise encrypted UDP INIT request body boundaries 2026-10-23 23:03:48 +02:00
evgeny bf7464a4d9 Document connection ownership after a cancelled TCP accept 2026-10-23 22:53:58 +02:00
evgeny e51badebcb Preserve active owners and paths when an incoming TCP link is cancelled 2026-10-23 22:53:36 +02:00
evgeny d5d103148f Test ownership handoff while cancelling an incoming TCP path 2026-10-23 22:52:40 +02:00
evgeny d5721563fc Clarify NEW cancellation and subscription mutation semantics 2026-10-23 22:43:09 +02:00
evgeny 274554f7a5 Cover TCP creation cancellation and self-unsubscription on both roles 2026-10-23 22:42:20 +02:00
evgeny 8fe1011da2 Document constructor cancellation and link memory ownership 2026-10-23 22:41:42 +02:00
evgeny 21a4dd7a70 Exercise callback unsubscription and nested dispatch on all ETCP chains 2026-10-23 22:40:47 +02:00
evgeny 33912355d6 Keep removed ETCP subscriptions until the outer dispatch returns 2026-10-23 22:39:56 +02:00
evgeny 5178f7abc6 Define instance callback dispatch lifetime for safe unsubscription 2026-10-23 22:39:06 +02:00
evgeny d0ee0341c5 Verify cancelled link constructors and guard UDP handshake callbacks 2026-10-23 22:36:39 +02:00
evgeny a2861b0570 Keep cancelled links alive until their constructor returns 2026-10-23 22:35:14 +02:00
evgeny a51c080407 Return NULL when callbacks cancel ETCP connection creation 2026-10-23 22:34:46 +02:00
evgeny 2ea4fb94ab Test nested deletion of the current and neighboring ETCP paths 2026-10-23 22:03:58 +02:00
evgeny 986193055e Suppress stale connection UP after its callback removes the last path 2026-10-23 22:03:29 +02:00
evgeny a43d478495 Cover close from connection INIT and UP callbacks on both roles 2026-10-23 22:02:44 +02:00
evgeny bf5a128a05 Correct STCP close contracts and sanitizer issues in its test 2026-10-23 22:01:48 +02:00
evgeny 477d8f995b Document STCP adapter ownership, callbacks and reconnect lifecycle 2026-10-23 21:59:12 +02:00
evgeny 03d69cb772 Verify TCP retry timing, recovery and cancellation end to end 2026-10-23 21:58:27 +02:00
evgeny f5e4c78b8c Exercise repeated close during the server link teardown event 2026-10-23 21:57:10 +02:00
evgeny 425e1a527b Provide the peer snapshot for the incoming test owner 2026-10-23 21:56:35 +02:00
evgeny f1e3c2c08a Hold the incoming connection before exercising DOWN callbacks 2026-10-23 21:55:54 +02:00
evgeny bd9f1bb0c9 Flush cancelled timers in teardown tests and clarify adapter close 2026-10-23 21:54:49 +02:00
evgeny 1fb52d3f5b Report active resources when callback teardown is unbalanced 2026-10-23 21:54:15 +02:00
evgeny 4614577d19 Stop readiness publication after callback close and rescan teardown links 2026-10-23 21:49:11 +02:00
evgeny f37221107d Guard TCP link transitions across callbacks and unify reconnect backoff 2026-10-23 21:48:11 +02:00
evgeny 42e7b9b1b6 Own STCP adapter queues and report connection failures before READY 2026-10-23 21:46:12 +02:00
evgeny 7753a0f11e Include the raw STCP server API in adapter regression tests 2026-10-23 21:43:17 +02:00
evgeny 4bfec7257f Cover TCP callback teardown on both roles and guard link close reentry 2026-10-23 21:40:43 +02:00
evgeny 2cad81050d Exercise early STCP adapter failure and cancellation callbacks 2026-10-23 21:39:32 +02:00
evgeny 18f10b23b3 Record compact attempt validation and aggregation check 2026-10-23 19:34:46 +02:00
evgeny a3eeebbc52 Clarify attempt reservation ownership on epoch exhaustion 2026-10-23 19:33:11 +02:00
evgeny 85c92e35a8 Update multipath contract for compact attempt pairs 2026-10-23 19:30:22 +02:00
evgeny be95e93a36 Reject old encrypted ACKs when compact attempt pairs restart in a new epoch 2026-10-23 19:27:06 +02:00
evgeny 7c09bfca7f Stop packet construction when input callbacks restart the stream 2026-10-23 19:26:00 +02:00
evgeny 77a737fd71 Document compact attempt wire layout and epoch reuse protection 2026-10-23 19:25:20 +02:00
evgeny 9a9517deab Exercise ACK splitting beyond compact entry capacity 2026-10-23 19:24:52 +02:00
evgeny b73d6e91e2 Verify compact attempt isolation, wire boundaries and exhaustion recovery 2026-10-23 19:23:30 +02:00
evgeny 0987d9dd04 Adapt encrypted session and window fixtures to compact attempt fields 2026-10-23 19:20:49 +02:00
evgeny ccbd214c97 Encode 16-bit attempt numbers and guard pair reuse by session epochs 2026-10-23 19:19:54 +02:00
evgeny 6414cb15ef Define compact per-fragment ETCP attempt numbers 2026-10-23 19:19:07 +02:00
evgeny 8c3fcfd1c8 Log fragment bytes and session epochs on normalizer desync 2026-10-23 18:15:17 +02:00
evgeny 1b7d314d85 Record bandwidth-only correction validation and remaining speed limits 2026-10-23 16:48:47 +02:00
evgeny ea5a0b7d5a Describe receiver-clock bandwidth bounds and ACK wire format 2026-10-23 16:44:07 +02:00
evgeny 6fc964031c Check ACK receiver point diagnostics at buffer boundaries 2026-10-23 16:40:52 +02:00
evgeny 4bf47ca357 Exercise receiver bandwidth bounds and ACK progress framing 2026-10-23 16:39:12 +02:00
evgeny ea4a92125b Carry receiver progress only in ACKs with attempt confirmations 2026-10-23 16:23:20 +02:00
evgeny 5586528555 Bound ETCP bandwidth samples by receiver delivery progress 2026-10-23 16:22:13 +02:00
evgeny e8d9d683dc Record duplex plan trials and measured ACK bandwidth costs 2026-10-23 16:10:46 +02:00
evgeny cc01457957 Record verified INIT recovery and BGP regression results 2026-10-23 14:24:31 +02:00
evgeny 5b8aba9c8b Drive callback deletion through the public reinit transition 2026-10-23 14:19:53 +02:00
evgeny f5d67d5240 Test link deletion from INIT and keepalive callbacks 2026-10-23 14:19:24 +02:00
evgeny 35ef504423 Document INIT liveness and response validation contracts 2026-10-23 14:16:28 +02:00
evgeny a87d3116af Validate INIT response body before updating peer state 2026-10-23 14:14:53 +02:00
evgeny c5f743a09d Cover INIT liveness with an independent healthy link 2026-10-23 14:01:57 +02:00
evgeny b167a27e85 Document RX ownership and limit readiness wakeups to transitions 2026-10-23 13:48:09 +02:00
evgeny f00a5bd306 Apply INIT reachability before reply and publish guarded RX transitions 2026-10-23 13:45:23 +02:00
evgeny 608caf01c1 Initialize the liveness fixture crypto and drain cancelled timers 2026-10-23 13:41:59 +02:00
evgeny e801fce2c6 Add encrypted UDP INIT liveness and one-way recovery regression 2026-10-23 13:38:36 +02:00
evgeny e89948f5ed Include the lowest reverse solo rate in measurement summary 2026-10-23 13:20:44 +02:00
evgeny 3c7b59ef02 Report three-run multipath rates and confirmed INIT liveness failure 2026-10-23 13:20:15 +02:00
evgeny 54fb4185f1 Identify one-way INIT liveness defect behind intermittent BGP failure 2026-10-23 13:14:51 +02:00
evgeny ac264c254c Expose transport topology in BGP route exchange failures 2026-10-23 13:13:47 +02:00
evgeny 5403647aee Record full regression results after retransmission timeout change 2026-10-23 13:05:22 +02:00
evgeny 3a007fbb29 Record jitter control and the cost of a shorter retransmission margin 2026-10-23 13:02:18 +02:00
evgeny 92e9281623 Record timeout matrix improvements and outstanding latency thresholds 2026-10-23 13:01:37 +02:00
evgeny e0f28bf851 Document reduced RTT multiplier and jitter margin for RTO 2026-10-23 12:52:59 +02:00
evgeny 9dbef48eb3 Use RTT jitter and fixed margin for ETCP retransmission timeout 2026-10-23 12:52:35 +02:00
evgeny e84e17c396 Record successful proxy burst and load validation after OOM changes 2026-10-23 12:46:10 +02:00
evgeny 9cc921f615 Record full regression failures and diagnose active media temp deletion 2026-10-23 12:45:38 +02:00
evgeny 0d5e9332e3 Clarify connection references versus active callback lifetime 2026-10-23 12:43:26 +02:00
evgeny b1e94eff34 Describe OOM session interruption and normalizer ownership precisely 2026-10-23 12:42:05 +02:00
evgeny bbe43b9842 Record completed fail-fast OOM recovery and remaining multipath work 2026-10-23 12:41:14 +02:00
evgeny 3bba05d354 Give encrypted DATA fixture a valid keepalive receive timeout 2026-10-23 12:36:20 +02:00
evgeny d43716933f Test encrypted two-sided recovery after normalizer OOM 2026-10-23 12:33:58 +02:00
evgeny 3c18e594d1 Keep the session deadline unchanged across handshake retries 2026-10-23 12:32:57 +02:00
evgeny 23d426fd0c Document fail-fast OOM and session interruption contracts 2026-10-23 12:32:56 +02:00
evgeny f221fa5a89 Exercise normalizer waiter allocation failure on a busy ETCP queue 2026-10-23 12:30:38 +02:00
evgeny 6e33675503 Verify OOM session abort, recovery and reentrant callback teardown 2026-10-23 12:29:29 +02:00
evgeny 76398d204a Add deterministic normalizer and restart fault injection hooks 2026-10-23 12:28:13 +02:00
evgeny d2b195473d Test queue discard without allocation or stale callbacks 2026-10-23 12:27:38 +02:00
evgeny caf37e9541 Make stream restart terminate explicitly if session scheduling fails 2026-10-23 12:24:48 +02:00
evgeny 316180c276 Abort normalizer sessions on OOM and guard reentrant delivery callbacks 2026-10-23 12:24:48 +02:00
evgeny 5f085b2086 Discard failed ETCP streams without allocating or replaying fragments 2026-10-23 12:22:22 +02:00
evgeny 466a56b499 Add allocation-free queue discard for terminal stream resets 2026-10-23 12:20:34 +02:00
evgeny 0989eff73d Record complete proxy integration checks and normalizer ownership fix outline 2026-10-23 11:32:10 +02:00
evgeny 187aa02b49 Record passing full regression and eight sanitizer checks 2026-10-23 11:29:53 +02:00
evgeny 0aaf6dfea0 Check metrics aggregate ACK queues and RTO state across paths 2026-10-23 11:26:45 +02:00
evgeny 9c40b489cf Record implemented multipath foundation and remaining reliability blockers 2026-10-23 11:25:13 +02:00
evgeny ce798a193e Remove stale fixture pointer and verify full transmission IDs in packet dumps 2026-10-23 11:22:37 +02:00
evgeny ddba151aa5 Adapt control metrics fixture to per-link ACK ownership 2026-10-23 11:17:20 +02:00
evgeny 2fddf839af Show transmission identity and frontier queries in ETCP packet diagnostics 2026-10-23 11:13:26 +02:00
evgeny 7bb43c42d6 Document per-path attempt ACKs, loss timers, pacing and memory bounds 2026-10-23 11:12:40 +02:00
evgeny b442d131c9 Exercise bounded attempt and ACK storage after logical delivery 2026-10-23 11:10:19 +02:00
evgeny 783646a967 Apply attempt budgets before selecting DATA paths 2026-10-23 11:09:28 +02:00
evgeny b74ff2633d Bound path ACK queues and physical attempt memory 2026-10-23 11:09:28 +02:00
evgeny b97169de19 Verify DATA pacing independently of piggyback ACK size 2026-10-23 11:04:11 +02:00
evgeny 72f48d1f14 Keep DATA pacing units consistent and test startup delay exit 2026-10-23 10:59:30 +02:00
evgeny 227104e224 Leave BBR startup after a full round of excessive queue delay 2026-10-23 10:58:38 +02:00
evgeny d57a729b92 Preserve bounded pacing credit across millisecond timer rounding 2026-10-23 10:45:39 +02:00
evgeny 3ca9926c48 Release synthetic path queues in the session-timeout fixture 2026-10-23 10:42:43 +02:00
evgeny 4c9a32d865 Observe peer-window recovery after the deferred queue resume 2026-10-23 10:40:48 +02:00
evgeny 5e436975c6 Exercise retry rotation, late attempt ACKs and receiver admission faults 2026-10-23 10:38:23 +02:00
evgeny a4a9ae7b0a Return receive-window query replies on the requesting path 2026-10-23 10:35:38 +02:00
evgeny 34a31f63be Update encrypted session fixtures and multipath observation for attempt IDs 2026-10-23 10:29:20 +02:00
evgeny 75485bf7bf Aggregate pending path ACKs in chat transport status 2026-10-23 10:28:17 +02:00
evgeny 432017cc22 Verify logical delivery and physical window counters independently 2026-10-23 10:27:38 +02:00
evgeny 147648ffd0 Expose per-path attempts and ACK queues in transport diagnostics 2026-10-23 10:25:27 +02:00
evgeny 1b3c4552fb Account for transmission IDs in MTU and packet diagnostics 2026-10-23 10:24:29 +02:00
evgeny cc510fc74c Separate stream delivery from path ACKs and acknowledge retained DATA 2026-10-23 10:23:14 +02:00
evgeny abe3d4bc89 Rotate retransmissions per fragment and commit only accepted sends 2026-10-23 10:14:46 +02:00
evgeny 7bc2ac5a46 Implement per-path attempt accounting and isolated ACK delivery samples 2026-10-23 10:13:22 +02:00
evgeny b68e0ac3db Define independent logical fragments and per-path transmission attempts 2026-10-23 10:09:44 +02:00
evgeny 3c44c3d7bc Keep unknown BBR bandwidth from imposing a one-byte pacing stall 2026-10-23 02:32:36 +02:00
evgeny e21cd26511 Apply BBR pacing through one owner and keep burst estimates diagnostic 2026-10-23 02:28:45 +02:00
evgeny 7e413db1e1 Keep pacing in bytes per second and reprice outstanding shaper debt 2026-10-23 02:28:11 +02:00
evgeny b39a861a57 Initialize STARTUP pacing from the actual initial DATA window 2026-10-23 02:24:10 +02:00
evgeny b349d7b7b2 Document agreed retransmission bounds and verified timer behavior 2026-10-23 02:21:43 +02:00
evgeny c3ac337ef0 Clamp ETCP retransmission timeout to 10 ms through 1 second 2026-10-23 02:20:10 +02:00
evgeny 4aae1f6151 Specify retry path rotation and close ACK ownership gaps 2026-10-23 02:08:04 +02:00
evgeny 259dfa35d9 Record RTT feedback experiments and refine multipath recovery plan 2026-10-23 01:56:01 +02:00
evgeny 7e0a0905e5 Document BBR queue time policy and its physical delay limits 2026-10-23 01:03:27 +02:00
evgeny 34e3d9bba0 Verify queue budget saturation and the BBR fast path contract 2026-10-23 01:02:15 +02:00
evgeny 599d8ad421 Bound BBR probe windows by the steady-state queue time budget 2026-10-23 01:01:25 +02:00
evgeny 4035794631 Define and test the BBR steady-state queue time budget 2026-10-23 01:00:36 +02:00
evgeny 5f406c2a41 Document BBR queue budget conflict with the fixed latency target 2026-10-23 00:58:29 +02:00
evgeny 94d6dd05c8 Expose BBR bandwidth and ACK budget alongside physical queue peaks 2026-10-23 00:54:27 +02:00
evgeny 67ae4ecf11 Record full regression results and reproducible NAT profile failures 2026-10-23 00:51:44 +02:00
evgeny d0f122f4d8 Record verified RTT probe fixes and remaining multipath failures 2026-10-23 00:49:02 +02:00
evgeny 08b6b2e2a1 Document DATA MSS and shared BBR transport window contracts 2026-10-23 00:45:09 +02:00
evgeny 5298fe9f6a Keep BBR loss bounds above the DATA window minimum 2026-10-23 00:43:54 +02:00
evgeny aefa8ff680 Check BBR loss bounds against the minimum DATA window 2026-10-23 00:43:31 +02:00
evgeny 0f23076f48 Apply BBR DATA windows consistently during startup and RTT probes 2026-10-23 00:42:45 +02:00
evgeny 92a535e009 Cover BBR probe drain and transport DATA window consistency 2026-10-23 00:42:02 +02:00
evgeny 519d6e4eaf Record verified BBR fixes and remaining multipath contract conflicts 2026-10-23 00:25:55 +02:00
evgeny 716db3a820 Saturate BBR byte windows instead of wrapping at large BDP 2026-10-23 00:24:35 +02:00
evgeny 3d6f855d34 Keep BBR probing and aggregation counters dimensionally consistent in bytes 2026-10-23 00:17:14 +02:00
evgeny 229e992695 Cover probing byte budgets and ACK aggregation beyond 64KiB 2026-10-23 00:16:40 +02:00
evgeny a79c724110 Grow BBR probe inflight bounds in MSS-sized byte increments 2026-10-23 00:15:46 +02:00
evgeny 6b1298aa62 Reproduce byte-sized instead of MSS-sized BBR probing growth 2026-10-23 00:15:28 +02:00
evgeny 0364abb00e Log sampled BBR model state without flooding transport diagnostics 2026-10-23 00:12:28 +02:00
evgeny f8d532b04d Describe ACK handling and bounded DATA pacing in the ETCP protocol 2026-10-23 00:10:01 +02:00
evgeny 4dba742149 Document BBR byte units and flight-based delivery rate samples 2026-10-23 00:10:00 +02:00
evgeny 88eb0f2108 Require multipath goodput without inflating channel queues 2026-10-23 00:08:15 +02:00
evgeny ade0e93f7f Measure actual channel queueing delay independently of propagation RTT 2026-10-23 00:08:15 +02:00
evgeny addff8547e Keep the ACK compression fixture within its known retransmission deadline 2026-10-23 00:06:30 +02:00
evgeny 1c18bdc9ab Measure BBR delivery over packet flights instead of inter-ACK gaps 2026-10-23 00:06:30 +02:00
evgeny f3008b7aba Snapshot delivery and send intervals for per-link BBR rate measurement 2026-10-23 00:06:30 +02:00
evgeny c2f4f048d7 Reproduce inflated delivery-rate samples when ACKs arrive together 2026-10-23 00:05:48 +02:00
evgeny 3c38072b9b Send ACK independently of the local outgoing DATA congestion pacer 2026-10-23 00:03:29 +02:00
evgeny 4cfd0ec89f Identify DATA in local datagram metadata without changing the wire header 2026-10-23 00:03:29 +02:00
evgeny 08c42e62ef Reproduce ACK starvation behind the reverse-direction DATA pacer 2026-10-23 00:02:57 +02:00
evgeny dd153264e1 Verify byte-rate scaling at multiple MTUs and multi-gigabit rates 2026-10-23 00:00:40 +02:00
evgeny 6f87ea1426 Keep BBR pacing and BDP in byte units without a second MSS factor 2026-10-22 23:59:57 +02:00
evgeny 6fec5d6d98 Reproduce MSS-dependent BBR pacing and BDP with a dimensional regression 2026-10-22 23:59:20 +02:00
evgeny 9b9a9f2e00 Record final multipath fault results and sanitizer validation 2026-10-22 23:41:45 +02:00
evgeny 27a7b7bf25 Declare the multipath phony target without overlapping Automake conditions 2026-10-22 23:40:12 +02:00
evgeny c9ba018a0d Bound fault steady goodput by newly received unique DATA 2026-10-22 23:37:24 +02:00
evgeny df5f6263bd Measure short HOLD across resume and arm faults before propagation completes 2026-10-22 23:37:02 +02:00
evgeny 0b5a892a88 Document multipath acceptance criteria and reproduced protocol limitations 2026-10-22 23:36:21 +02:00
evgeny 076016f34a Expose multipath queue accounting and add a dedicated long-running check target 2026-10-22 23:34:20 +02:00
evgeny 8417b1955c Document emulator queue bounds and account for allocation failures 2026-10-22 23:33:14 +02:00
evgeny 67322582f6 Report delivery failures per scenario without hiding subsequent channel cases 2026-10-22 23:32:34 +02:00
evgeny 439361cb52 Exclude previously buffered data from multipath utilization results 2026-10-22 23:31:02 +02:00
evgeny 6e350760d1 Check active channel stalls, delivery deadlines and delayed packet recovery 2026-10-22 23:29:29 +02:00
evgeny 672f04b833 Exercise real ETCP multipath against independent bandwidth and RTT profiles 2026-10-22 23:23:05 +02:00
evgeny f5cab5a14e Calibrate dummynet bandwidth and verify stalled channel lifecycle 2026-10-22 23:19:01 +02:00
evgeny f5f6ff50af Make dummynet bandwidth timing cumulative and add bounded DROP/HOLD modes 2026-10-22 23:18:11 +02:00
evgeny 62f4850968 Record successful full ETCP regression validation 2026-10-22 23:04:00 +02:00
evgeny 082d9d3960 Record STCP SIGPIPE regression and sanitizer validation 2026-10-22 23:02:42 +02:00
evgeny 05a5b6c2a7 Exercise STCP writes with default SIGPIPE disposition 2026-10-22 22:59:45 +02:00
evgeny fdcf92be04 Prevent SIGPIPE from terminating embedded STCP transports 2026-10-22 22:59:44 +02:00
evgeny 11aa760d47 Document ETCP window ownership and completed connection fixes 2026-10-22 22:57:06 +02:00
evgeny bc27352710 Remove stale references to the legacy connect installer 2026-10-22 22:53:55 +02:00
evgeny 0a00bd1c90 Clarify readiness contracts and stop on corrupt accounting 2026-10-22 22:53:30 +02:00
evgeny e2ee0d81dd Wait for both ETCP streams before starting Merkle tests 2026-10-22 22:52:14 +02:00
evgeny 6afc57569a Test zero-delay sender wakeups and failure injection 2026-10-22 22:50:38 +02:00
evgeny ccb3821e2f Schedule ordinary ETCP sends without timer latency 2026-10-22 22:50:04 +02:00
evgeny 29f9a97552 Use valid global options in REALITY config fixtures 2026-10-22 22:48:53 +02:00
evgeny f37ac2c637 Verify deferred teardown when the TX timer cannot start 2026-10-22 22:47:50 +02:00
evgeny 0ef0553598 Coalesce useful TX wakeups and defer failure teardown safely 2026-10-22 22:45:58 +02:00
evgeny f1c88ad375 Document congestion configuration and protect key diagnostics 2026-10-22 22:40:47 +02:00
evgeny 17c92e1745 Diagnose adopted transports and isolate traffic test commands 2026-10-22 22:39:43 +02:00
evgeny 34e346463d Cover initial UP, pacing and bidirectional tight windows 2026-10-22 22:38:20 +02:00
evgeny ef11d3b677 Preserve initial readiness while sender waits for a session 2026-10-22 22:36:53 +02:00
evgeny 2689931826 Inject allocation failures through the actual pool symbol 2026-10-22 22:34:07 +02:00
evgeny 133ea8d120 Wake sender after preparing each new fragment 2026-10-22 22:33:43 +02:00
evgeny 75ff7c4ce3 Exercise DATA windows, ACKs, retries and sender wakeups 2026-10-22 22:33:11 +02:00
evgeny 86a0f041c7 Validate congestion limits and guard window arithmetic 2026-10-22 22:31:12 +02:00
evgeny c1f07ec6ae Enforce per-link DATA windows without blocking ACKs 2026-10-22 22:29:31 +02:00
evgeny 50d5486a9f Define per-link accounting and deferred sender contracts 2026-10-22 22:26:51 +02:00
evgeny 036cf43918 Build and document direct connections through NCD 2026-10-22 22:25:52 +02:00
evgeny fc5b3911f3 Detach ETCP core from obsolete connect contexts 2026-10-22 22:25:39 +02:00
evgeny 008f499367 Remove obsolete parallel ETCP connection installer 2026-10-22 22:25:39 +02:00
evgeny 8169322d56 Move direct transport tests to NCD ownership 2026-10-22 22:24:45 +02:00
evgeny d84ab69465 Verify link selection mismatch with valid congestion limits 2026-10-22 21:32:34 +02:00
evgeny 91bb95a981 Record ETCP and STCP header audit findings 2026-10-22 21:30:28 +02:00
evgeny 03f4cd7939 Record validated media supernode replication implementation 2026-10-22 21:21:43 +02:00
evgeny 71d3b017e6 Clarify local media catalog seeding and channel cleanup contracts 2026-10-22 21:20:21 +02:00
evgeny 8230eb4de7 Test final-page update races and replication read failures 2026-10-22 21:16:59 +02:00
evgeny bc149497f3 Document media replication protocol and resource ownership 2026-10-22 21:15:55 +02:00
evgeny 0bcdb9930a Make media send queue policy explicit and document announcement errors 2026-10-22 21:14:42 +02:00
evgeny f2283eff6b Wait for actual retry requests independently of change hints 2026-10-22 21:12:58 +02:00
evgeny a011c57237 Cover replication retries, source reset and router backpressure 2026-10-22 21:10:44 +02:00
evgeny 9e2a1e5208 Handle automatic SQLite rollback and complete CHAT integration setup 2026-10-22 21:09:16 +02:00
evgeny d6d8ffc4a8 Require role subscription and initialize real chat test contexts 2026-10-22 21:07:18 +02:00
evgeny 14b01964b1 Initialize media test dependencies and await committed sync 2026-10-22 21:06:15 +02:00
evgeny 7fb9b43e9e Validate media role changes through signed CHAT membership 2026-10-22 21:02:57 +02:00
evgeny 63c0b9a655 Run media integration replication in its actual CHAT group 2026-10-22 21:01:24 +02:00
evgeny 00502bf4d4 Exercise real media replication pages, rollback and catalog lifecycle 2026-10-22 21:00:48 +02:00
evgeny d3bc77922b Initialize replication router queues before waiting for room 2026-10-22 20:55:25 +02:00
evgeny b4b11d6086 Implement atomic group-scoped media catalog replication 2026-10-22 20:54:37 +02:00
evgeny b47a82e3ac Define bounded receiver-driven media replication protocol 2026-10-22 20:45:14 +02:00
evgeny 04dc53672a Add regression for media availability group isolation 2026-10-22 20:43:51 +02:00
evgeny 964a938437 Record reproduced media replication failures and correct cursor audit 2026-10-22 20:11:06 +02:00
evgeny e8843d9499 Track remaining media supernode replication audit separately 2026-10-22 19:53:31 +02:00
evgeny ecba318dec Record validated media block ownership and relay fixes 2026-10-22 19:48:47 +02:00
evgeny 1aec478787 Drive relay generation wakeup through actual queue consumption 2026-10-22 19:46:30 +02:00
evgeny 8f97cf8070 Verify old relay readers survive a new download generation independently 2026-10-22 19:45:45 +02:00
evgeny bd0260040c Replace obsolete media relay description with current attempt and buffer contracts 2026-10-22 19:43:59 +02:00
evgeny 3b24e7e207 Describe media source lifetime, callback ownership and publication ordering 2026-10-22 19:40:38 +02:00
evgeny 993e22085a Scope libc fault-injection regression target to Linux 2026-10-22 19:39:34 +02:00
evgeny 00e0421e56 Update media wire-size assertions and cover fortified read fault injection 2026-10-22 19:38:56 +02:00
evgeny fabeb770a7 Verify media source admission, redirect bounds and transmit I/O failure cleanup 2026-10-22 19:37:28 +02:00
evgeny 47addfeffc Cover terminal media registration failures and relay admission lifetime 2026-10-22 19:35:45 +02:00
evgeny 02e1526115 Remove redundant embedded download entry and update service-stop request 2026-10-22 19:33:49 +02:00
evgeny c81cc8b9ca Document media attempt validation and reserve before tracking outgoing writes 2026-10-22 19:33:12 +02:00
evgeny 99389f4bd3 Bound relay readers independently and redirect only to matching block recipients 2026-10-22 19:32:15 +02:00
evgeny b09a020855 Exercise media block identity, I/O failure, reentrancy and relay backpressure 2026-10-22 19:30:59 +02:00
evgeny 090e7db598 Expose transport queue control in media regression fixtures 2026-10-22 19:27:37 +02:00
evgeny 5215163a37 Share authenticated media fixtures for adversarial delivery regressions 2026-10-22 19:25:24 +02:00
evgeny 1c1ab24a27 Test real truncated-block assembly and balance media admission ownership 2026-10-22 19:24:17 +02:00
evgeny 353752165c Align media integration inputs with routed block identity 2026-10-22 19:23:53 +02:00
evgeny b1a3d0c6cd Scope media retry responses to active block attempts 2026-10-22 19:17:36 +02:00
evgeny 16333b989d Replace permissive media download fixtures with verified block inputs 2026-10-22 19:16:23 +02:00
evgeny d2bf40e11a Use authenticated block metadata in media query fixtures 2026-10-22 19:15:17 +02:00
evgeny d0a401e166 Validate received blocks and publish complete verified media atomically 2026-10-22 19:14:12 +02:00
evgeny 4edf72ea14 Create independent block attempts and cancel channel downloads safely 2026-10-22 19:11:44 +02:00
evgeny 9b4974f02d Unify media source and relay transmission with owned buffers 2026-10-22 19:11:26 +02:00
evgeny f15d53947e Define media block attempts and shared byte-source ownership 2026-10-22 19:08:14 +02:00
evgeny 68129c0151 Retain media downloads across reentrant progress callbacks 2026-10-22 19:07:04 +02:00
evgeny a3a8b5b1be Add checked UTF-8 opening of existing block files for update 2026-10-22 19:03:16 +02:00
evgeny 55d2f437a0 Clarify ownership requirements for media completion callbacks 2026-10-22 18:57:48 +02:00
evgeny 108dfae82b Record reproduced media block receive and completion defects 2026-10-22 18:45:19 +02:00
evgeny 3a703d1093 Record final media QUERY full-suite validation 2026-10-22 18:31:13 +02:00
evgeny 433a091db5 Record verified media QUERY fixes and regression coverage 2026-10-22 18:31:13 +02:00
evgeny 21ebc832e1 Verify holder selection and log download resource transitions 2026-10-22 18:31:12 +02:00
evgeny a3f3b2fff2 Document media lookup lifetimes and remove obsolete shared state 2026-10-22 18:31:12 +02:00
evgeny 093e537fed Check live QUERY deadlines and deferred CM failover 2026-10-22 18:31:12 +02:00
evgeny 6a14be05f5 Verify second-block SQL failure and propagate local download errors 2026-10-22 18:31:12 +02:00
evgeny fd6155aa4a Exercise media QUERY bounds, paging, failures and peer reuse 2026-10-22 18:31:12 +02:00
evgeny f47e80571d Assert media QUERY results and preserve transport ownership in tests 2026-10-22 18:31:12 +02:00
evgeny e02c9f7727 Cancel delayed CM UP before deferring failover 2026-10-22 18:31:12 +02:00
evgeny 05ace58589 Implement bounded media lookup with independent deadlines 2026-10-22 18:31:12 +02:00
evgeny 92c514ce18 Describe bounded media lookup and peer slot state 2026-10-22 18:31:12 +02:00
evgeny 6267973d62 Define bounded media QUERY batches and complete responses 2026-10-22 18:31:12 +02:00
evgeny cecbc682a0 Move stream start and stop into the title bar 2026-10-22 18:13:43 +02:00
evgeny 07cd150b47 Merge shared Android media audio routing and microphone fixes 2026-10-22 18:04:49 +02:00
evgeny 6a1c289ab2 Keep Bluetooth SCO active for radio reception without capture 2026-10-22 17:59:11 +02:00
evgeny bb380441a3 Fix shared microphone crackle from timestamp jitter 2026-10-22 16:53:25 +02:00
evgeny 94d1e245a5 Record reproduced media QUERY bounds and response truncation defects 2026-10-22 16:47:28 +02:00
evgeny 02265644ce Record verified media index fixes and remaining QUERY audit 2026-10-22 15:48:15 +02:00
evgeny c9a3a1334b Restore shared media playback to speaker after headset loss 2026-10-22 15:24:13 +02:00
evgeny 0835c3d3cc Describe media preparation records and deferred channel cleanup 2026-10-22 15:15:52 +02:00
evgeny 34959a9657 Share atomic media commits between downloads and backfill 2026-10-22 15:14:25 +02:00
evgeny 90fab54a4a Use one atomic commit API for authored and downloaded files 2026-10-22 15:14:22 +02:00
evgeny 439011ce60 Use real media schema and validate duplicate download ownership 2026-10-22 15:13:14 +02:00
evgeny ec70908733 Check relative path limits and binding failures without truncation 2026-10-22 15:11:23 +02:00
evgeny 2b63c680b2 Verify shared Android audio ownership on a physical device 2026-10-22 15:07:26 +02:00
evgeny 44a3b33c3a Expose shared audio devices in stream viewing and document them 2026-10-22 15:07:26 +02:00
evgeny 5d10a16baa Use shared audio controls in radio and stream publication 2026-10-22 15:07:25 +02:00
evgeny 33ccba1f3f Add a common audio device selector for radio and streams 2026-10-22 15:07:25 +02:00
evgeny bebc579180 Share media playback devices and await cleanup before calls 2026-10-22 15:07:25 +02:00
evgeny a893f6b4f2 Route radio and stream capture through the shared controller 2026-10-22 15:07:25 +02:00
evgeny ecada765a2 Own Android media devices in one shared audio controller 2026-10-22 15:07:25 +02:00
evgeny b72e51accb Keep shared microphone PCM continuous across device changes 2026-10-22 15:07:25 +02:00
evgeny f123a1bdeb Document preparation ownership and log exact media index failures 2026-10-22 15:06:00 +02:00
evgeny 3bb8f76b7d Reuse the database media base when restarting chat 2026-10-22 15:03:41 +02:00
evgeny 6b54b63986 Define download start ownership and avoid access after completion 2026-10-22 15:00:41 +02:00
evgeny c08c24140c Test reentrant media events and cleanup after hash failure 2026-10-22 14:58:15 +02:00
evgeny d4efdba44e Retain failed download destinations for later cleanup 2026-10-22 14:56:07 +02:00
evgeny 41057ee9d5 Keep media task ownership through synchronous submission events 2026-10-22 14:54:18 +02:00
evgeny 87724433b1 Exercise media index failure recovery and pending channel deletion 2026-10-22 14:52:27 +02:00
evgeny 9117813c4d Preserve indexed and unfinished files during orphan cleanup 2026-10-22 14:49:11 +02:00
evgeny 15fafc42c9 Use validated paths for media submission and backfill 2026-10-22 14:46:49 +02:00
evgeny 16a5e628fd Report incomplete media cleanup separately from channel deletion 2026-10-22 14:46:15 +02:00
evgeny 32d80110cb Drain outgoing media tasks before deleting their channel 2026-10-22 14:45:29 +02:00
evgeny b3a223521c Track unfinished media destinations for channel cleanup 2026-10-22 14:44:24 +02:00
evgeny 8cba8ef3e6 Propagate media schema and downloaded registration failures 2026-10-22 14:42:44 +02:00
evgeny 113f0259ea Preserve retryable media cleanup and validate served paths 2026-10-22 14:41:45 +02:00
evgeny 889dfc411a Validate media paths and register complete files atomically 2026-10-22 14:40:30 +02:00
evgeny ad62eee10c Record media index plan constraints and late completion race 2026-10-22 14:35:56 +02:00
evgeny d68ab90f23 Record reproduced media index path and database failures 2026-10-22 14:26:49 +02:00
evgeny ad0505bd31 Record verified media primitive fixes and remaining location audit 2026-10-22 13:40:39 +02:00
evgeny 64519c41a7 Restrict fortified stdio test hook to glibc 2026-10-22 13:35:25 +02:00
evgeny 0d55c98add Inject fortified fread failures in sanitizer regressions 2026-10-22 13:34:54 +02:00
evgeny 5549a69b1c Cover failed file stream creation and document media cancellation effects 2026-10-22 13:31:13 +02:00
evgeny 1ceb0beb8d Check generated media identifiers in integration fixtures 2026-10-22 10:28:32 +02:00
evgeny 386ea85fea Log precise copy and media indexing failure operations 2026-10-22 10:27:42 +02:00
evgeny 607de2a074 Bound recursive walker stack and report exact copy failure phases 2026-10-22 10:26:56 +02:00
evgeny bf3834b95d Use existing Linux condition for media fault injection 2026-10-22 10:24:16 +02:00
evgeny 3a00caef50 Add fault injection regressions for media files and worker ownership 2026-10-22 10:23:56 +02:00
evgeny 5505afd1f3 Keep thumbnail path bounds and check UUID status in stress test 2026-10-22 10:21:41 +02:00
evgeny 0e418c7fa9 Propagate RNG errors and preserve 64-bit media sizes through callers 2026-10-22 10:20:02 +02:00
evgeny f399ded5ba Preserve files on copy errors and check media primitive failures 2026-10-22 10:18:23 +02:00
evgeny 85d96fc857 Share UTF-8 Windows path conversion with file workers 2026-10-22 10:18:22 +02:00
evgeny 3100985e4c Specify media worker ownership errors and primitive contracts 2026-10-22 10:16:57 +02:00
evgeny 6355bd1d73 Use shared file publication in video processing 2026-10-22 10:15:39 +02:00
evgeny 3ed4b73ebf Extract checked file identity and temporary publication primitives 2026-10-22 10:15:13 +02:00
evgeny 41cc04bdab Record media async primitive and contract audit findings 2026-10-22 10:07:20 +02:00
evgeny d8621c7f27 Record bootstrap regression coverage and successful validation 2026-10-22 10:02:57 +02:00
evgeny 3c49c2a4aa Initialize callback epoch for pending transport shutdown test 2026-10-22 09:59:13 +02:00
evgeny 633f1d3044 Schedule pending deadline resets from probe creation 2026-10-22 09:47:23 +02:00
evgeny 34602e3e7e Separate cold bootstrap from controlled pending probe reset 2026-10-22 09:45:48 +02:00
evgeny 330e346a7b Record bootstrap test race in TCP session initialization 2026-10-22 09:40:08 +02:00
evgeny 6d39800351 Record completed video fixes and validation coverage 2026-10-22 00:20:40 +02:00
evgeny df2f90d1f5 Allow MP4 millisecond edit rounding in audio tail regression 2026-10-22 00:17:55 +02:00
evgeny 1d6596dc61 Check resampler tail duration and actual 1080p to 720p conversion 2026-10-22 00:17:16 +02:00
evgeny 0653121fc4 Document video publication and cancellation in service lifecycle 2026-10-22 00:15:54 +02:00
evgeny 3f603534e9 Log exact video failure phases and check buffered file seeks 2026-10-22 00:11:02 +02:00
evgeny 631cc924cf Measure AAC signal onset above verified codec pre-echo level 2026-10-22 00:09:49 +02:00
evgeny 9f1825dbf8 Verify decoded video content, AAC signal timing and FFmpeg-free stubs 2026-10-22 00:08:22 +02:00
evgeny 9163025912 Test video I/O failures, audio timing, cancellation and concurrent ownership 2026-10-22 00:05:30 +02:00
evgeny 5929bb4028 Normalize unspecified PCM channel layouts before AAC setup 2026-10-22 00:03:12 +02:00
evgeny fe313cf8f8 Match FFmpeg AVIO and codec capability APIs across supported versions 2026-10-22 00:02:29 +02:00
evgeny cb9102f381 Prepare video through checked temporary output and complete codec drains 2026-10-22 00:01:47 +02:00
evgeny 9a8bc9b461 Select video regression branch from configured FFmpeg support 2026-10-21 23:59:06 +02:00
evgeny c4bf292085 Specify video publication, remux and completion contracts 2026-10-21 23:58:55 +02:00
evgeny 8d9704036f Add real video fixtures and file preservation regressions 2026-10-21 23:56:55 +02:00
evgeny fb01a9bb2e Record video audit failures and missing API contracts 2026-10-21 23:28:53 +02:00
evgeny 1d7e12c562 Avoid random HEX substring collisions in config updater assertions 2026-10-21 23:12:12 +02:00
evgeny 7c473dd500 Reject signed NAT forward IDs hidden by leading whitespace 2026-10-21 23:11:32 +02:00
evgeny 1ccb266971 Describe NAT transport backpressure, ownership and mapping holds 2026-10-21 23:08:31 +02:00
evgeny 7de54ab3e4 Document NAT hash, expiry, mapping holds and bounded transport ownership 2026-10-21 22:58:28 +02:00
evgeny 2c10272e15 Add repeatable NAT benchmarks for table size, payload size and full-pool misses 2026-10-21 22:55:24 +02:00
evgeny 55cc721c9b Test held NAT mappings under TUN congestion and reject invalid provider input 2026-10-21 22:54:24 +02:00
evgeny dea768c21a Hold outgoing NAT mappings through asynchronous TUN delivery 2026-10-21 22:52:19 +02:00
evgeny 824c663ebd Protect NAT mappings referenced by pending TUN transmissions 2026-10-21 22:51:25 +02:00
evgeny 80b481827c Verify bounded global NAT pressure and retained destination across mapping reuse 2026-10-21 22:46:09 +02:00
evgeny 8360e48622 Exercise NAT backpressure, mapping reuse, copy ownership and shutdown 2026-10-21 22:44:46 +02:00
evgeny 41a68b86bc Close NAT channels after cancelling producers and quiet transient TUN backpressure 2026-10-21 22:43:54 +02:00
evgeny cfc0051088 Test NAT boundary validation, hash collisions, expiry and allocation failures 2026-10-21 22:34:06 +02:00
evgeny cecc5d7d1f Validate provider replies before client TUN delivery 2026-10-21 22:32:29 +02:00
evgeny 5e2e6c15cf Preserve NAT index invariants across transport and reuse tests 2026-10-21 22:31:55 +02:00
evgeny d0c83cfff1 Add bounded NAT backpressure and forward TUN packets without copying 2026-10-21 22:30:01 +02:00
evgeny 9d6d6454b0 Reject malformed NAT configuration and use mapping lifecycle APIs in tests 2026-10-21 22:27:16 +02:00
evgeny 94fa3207f6 Index NAT mappings by hash and maintain free ports in constant time 2026-10-21 22:24:17 +02:00
evgeny 5a837f903c Validate NAT packet boundaries and distinguish skipped packets 2026-10-21 22:21:37 +02:00
evgeny 00b4b92749 Align NAT documentation with node ownership and router framing 2026-10-21 22:01:08 +02:00
evgeny 19a5870d5f Document NAT ownership, packet formats and API contracts 2026-10-21 22:00:06 +02:00
evgeny bfa23afae9 Restrict NAT delivery to its group and configured provider 2026-10-21 21:58:10 +02:00
evgeny 909005c1a4 Complete standalone NAT forward configuration fixture 2026-10-21 21:57:23 +02:00
evgeny ddc163a2b7 Cover NAT forward owners, full port range and invalid rules 2026-10-21 21:56:20 +02:00
evgeny ca471d777e Route static NAT forwards to explicit owners on all ports 2026-10-21 21:55:13 +02:00
evgeny 2c4f107e63 Require a target node for NAT port forward configuration 2026-10-21 21:54:41 +02:00
evgeny 3d0c4ff0a5 Isolate NAT mappings by source node identity 2026-10-21 21:53:17 +02:00
evgeny a9820bea42 Preserve IPv4 UDP checksum encoding through NAT 2026-10-21 21:51:49 +02:00
evgeny 52a73ea6a7 Correct incremental NAT checksum arithmetic 2026-10-21 21:50:59 +02:00
evgeny 6e9bacd843 Fix NAT flow lookup at the maximum port boundary 2026-10-21 21:48:28 +02:00
evgeny fbbd62acc4 Fix NAT packet ownership after router send failures 2026-10-21 21:43:00 +02:00
evgeny ef673eb3b7 Merge branch 'master' of http://git.syqiq.tech/jeka/utun2 2026-10-21 21:22:49 +02:00
evgeny 05b0927619 Fix TUN packet ownership, lifecycle and backend consistency
Unify TUN initialization and raw-IP writes, preserve socketpair directions, and bound reader delivery with queue backpressure. Make deferred packet ownership safe across close and uasync teardown, and apply per-interface MTU limits to lwIP MSS.

Validation: full check.sh passed 172 tests, 9 proxy integration checks, burst and load. TUN ASAN/UBSAN regressions passed 30 cases; TCP exchange passed with asymmetric and minimum MTUs. Sanitized NAT integration exposed an existing unaligned uint16_t read in eim_nat.c; that separate defect remains unchanged. Native Windows and FreeBSD validation remains outstanding.
2026-10-21 21:19:42 +02:00
evgeny e3f2feb480 Merge branch 'master' of http://git.syqiq.tech/jeka/utun2 2026-10-21 20:26:26 +02:00
evgeny a65b0c8786 Fix queue extraction in NTP regression cleanup 2026-10-21 20:18:49 +02:00
evgeny 6c84e680af Improve NTP failure diagnostics and source transition coverage 2026-10-21 20:18:17 +02:00
evgeny d6eed66926 Document NTP contracts and verify corrected wall time 2026-10-21 20:16:38 +02:00
evgeny 88bf65e07d Cover NTP correction, retries, faults and peer lifecycle 2026-10-21 20:15:17 +02:00
evgeny 13c1831baa Fix peer time cache ownership and callback teardown 2026-10-21 20:12:04 +02:00
evgeny 5609d57947 Fix NTP offset and bound asynchronous polling cycles 2026-10-21 20:11:04 +02:00
evgeny 488be0edd1 Record recurring attachment test timeout during control validation 2026-10-21 20:06:51 +02:00
evgeny e620567804 Finalize control server regression fixture cleanup 2026-10-21 20:04:30 +02:00
evgeny fdf2912427 Cover control server regressions and correct API documentation 2026-10-21 20:04:30 +02:00
evgeny 51922cd18e Fix control server buffer bounds and client resource ownership 2026-10-21 20:04:19 +02:00
evgeny 79946bb07c Keep firewall reload regression on the selective path 2026-10-21 20:00:28 +02:00
evgeny 41c32cf063 Set router packet length in firewall integration fixture 2026-10-21 19:59:34 +02:00
evgeny 4f627bddff Align firewall examples with symmetric destination filtering 2026-10-21 19:53:50 +02:00
evgeny 2a2a0ee267 Cover firewall parsing, bidirectional TUN filtering and reload failures 2026-10-21 19:52:51 +02:00
evgeny 5de0eb5115 Filter both directions of the main TUN and remove obsolete firewall guide 2026-10-21 19:49:32 +02:00
evgeny cf6c3c99e8 Reject invalid firewall configuration and preserve policy on reload failure 2026-10-21 19:48:54 +02:00
evgeny c3a7f77b54 Make firewall rule replacement atomic and validate IPv4 packets 2026-10-21 19:48:19 +02:00
evgeny 4f5651906c Report precise INI read failures and identity repair reasons 2026-10-21 19:09:03 +02:00
evgeny 2df21efc22 Bound edited INI growth and cover effective duplicate values 2026-10-21 19:06:59 +02:00
evgeny b0f5055c31 Describe shared INI parsing and distribute its internal header 2026-10-21 19:04:54 +02:00
evgeny 80eb331d6a Document config identity and publication guarantees 2026-10-21 19:04:53 +02:00
evgeny bb87a0845b Verify config updates preserve files and allocations under OOM 2026-10-21 19:03:39 +02:00
evgeny f7c8b95af6 Bound INI buffers to allocator limits and log allocation errors 2026-10-21 19:03:38 +02:00
evgeny 33130c1d74 Test identity preservation and failed config publication 2026-10-21 19:02:20 +02:00
evgeny f61c0c3e38 Preserve config identity and publish verified updates safely 2026-10-21 19:00:06 +02:00
evgeny 2410c39b71 Share INI parsing and validate complete configuration values 2026-10-21 18:54:43 +02:00
evgeny 5a57b59b50 Test emulator forwarding and partial initialization cleanup 2026-10-21 18:14:03 +02:00
evgeny ff3eb02bff Update emulator uasync lifecycle and document forwarding rules 2026-10-21 18:12:06 +02:00
evgeny 56d5fcd75a Test broadcast UUID lifetime and callback registration contracts 2026-10-21 18:10:43 +02:00
evgeny 484b620016 Preserve broadcast UUIDs until TTL and document callback contracts 2026-10-21 18:08:50 +02:00
evgeny 16f81575ce Consolidate TCP queue and resource lifecycle documentation in header 2026-10-21 17:59:08 +02:00
evgeny 10b039686c Consolidate queue usage and backpressure documentation in header 2026-10-21 17:57:34 +02:00
evgeny 194bf7bb45 Consolidate debug configuration documentation in header 2026-10-21 17:56:13 +02:00
evgeny a288afd8ce Consolidate socket lifecycle and wrapper documentation in header 2026-10-21 17:55:27 +02:00
evgeny 4f3f6d3e60 Consolidate platform adapter documentation in header 2026-10-21 17:55:27 +02:00
evgeny 1c4010ebed Consolidate serialization format and ownership documentation in header 2026-10-21 17:54:20 +02:00
evgeny ace9cb70d0 Consolidate timeout heap ownership and complexity documentation 2026-10-21 17:54:20 +02:00
evgeny af3c7a4217 Consolidate pool lifecycle and diagnostics documentation in header 2026-10-21 17:53:00 +02:00
evgeny e403a2f689 Consolidate memory ownership and allocation documentation in header 2026-10-21 17:53:00 +02:00
evgeny 3944459c29 Consolidate sliding minimum documentation in header 2026-10-21 17:52:02 +02:00
evgeny 48383ef95e Consolidate outgoing IP documentation in header 2026-10-21 17:52:02 +02:00
evgeny f020a6f62b Clarify CPU indicator polling and pending responses 2026-10-21 17:48:35 +02:00
evgeny d23a96a08e Document uasync scheduling, ownership and lifecycle contracts 2026-10-21 17:40:20 +02:00
evgeny dffcd19d21 Clarify queue callback scheduling and ownership contracts 2026-10-21 17:27:04 +02:00
evgeny 9079ab5c24 Fix debug timestamps, default output and log reopening 2026-10-21 17:10:44 +02:00
evgeny 7e39f16e0f Fix allocation size overflow, zero-size OOM and debug category parsing 2026-10-21 17:07:11 +02:00
evgeny 00819654cf Document POSIX descriptor zero exception in socket close 2026-10-21 16:57:44 +02:00
evgeny 346c06f31d Record TCP lifecycle validation and IPv6 selection caveat 2026-10-21 16:52:11 +02:00
evgeny ba7c2873c5 Initialize proxy timers before notifying connected owners 2026-10-21 16:48:25 +02:00
evgeny 0e15234ce1 Clarify TCP lifecycle and platform address selection contracts 2026-10-21 16:46:45 +02:00
evgeny e5fafacf99 Cover TCP timer replacement failures and pending FIN cleanup 2026-10-21 16:45:53 +02:00
evgeny d467d3aa8f Fix TCP connect timer ownership and pending FIN state 2026-10-21 16:44:19 +02:00
evgeny d5e489598d Clarify DNS lifetimes and parser and video plane limits 2026-10-21 16:33:16 +02:00
evgeny 1ff02188ca Fix Silero model cleanup and document runtime configuration 2026-10-21 16:29:44 +02:00
evgeny f31b34d0f0 Clarify serialization limits, cleanup and error codes 2026-10-21 16:18:51 +02:00
evgeny ecd5b9fc9d Clarify memory pool counters and strbuf error documentation 2026-10-21 16:14:48 +02:00
evgeny aca6364378 Correct call routing, media channels and API documentation 2026-10-21 16:11:36 +02:00
evgeny 88e930cc0a docs: correct member file protocol and media download contracts 2026-10-21 16:01:30 +02:00
evgeny be235e01aa docs: correct DM payload, history and transfer cancellation contracts 2026-10-21 15:53:28 +02:00
evgeny 66f004f725 docs: describe actual DM identity derivation and crypto modules 2026-10-21 15:52:45 +02:00
evgeny cbe22899c2 docs: align chat invite and member sync descriptions with code 2026-10-21 15:47:56 +02:00
evgeny df36e504fc docs: remove completed and obsolete project tasks 2026-10-21 15:47:30 +02:00
evgeny 7c814f5cb9 Route desktop codec diagnostics through the shared log
Quiet capability probes, preserve runtime errors, and register the desktop portal application. Add logging and desktop launcher regression coverage.
2026-10-21 15:45:40 +02:00
evgeny 9ba78c084b docs: correct topology API and router timeout descriptions 2026-10-21 15:40:56 +02:00
evgeny d644126cac docs: align ETCP handshake and epoch descriptions with code 2026-10-21 15:37:01 +02:00
evgeny f59f64ea48 docs: remove reviewed header backup files 2026-10-21 15:22:42 +02:00
evgeny f5f106c682 docs: clarify header APIs and preserve originals in .h1 2026-10-21 15:21:27 +02:00
evgeny abf342534f Encrypt P2P call media and validate service IDs
Require signed and encrypted call audio/video using static identity keys. Fix attachment service ID collisions and media download handle lifetime; isolate LAN tests and audit the shared service registry.
2026-10-21 15:06:27 +02:00
evgeny 8e46cf9254 docs: refresh project file descriptions in AGENTS.md 2026-10-21 14:54:06 +02:00
evgeny 15b037aef7 Scroll to latest chat messages with End key 2026-10-21 13:31:50 +02:00
evgeny 69e534304b Darken desktop chat backgrounds and lighter panels 2026-10-21 13:26:05 +02:00
evgeny 4dbc543cd3 Increase contrast of chat scrollbar thumb only 2026-10-21 13:21:30 +02:00
evgeny 4f6808156f Add chat history keyboard scrolling and remove unused PTT setting 2026-10-21 13:05:31 +02:00
evgeny 5870efdf04 Default stream redundancy to zero on Android and desktop 2026-10-21 12:42:23 +02:00
evgeny 34db81c5cf Show green stream watch buttons only for active streams 2026-10-21 12:30:08 +02:00
evgeny a31436e421 Expand chat README and add stream image 2026-10-21 02:31:00 +02:00
evgeny 550f384c69 Tidy stream video worker source formatting 2026-10-21 01:40:04 +02:00
evgeny a6b909b70e Add device regression for independent stream audio and video workers 2026-10-21 01:38:50 +02:00
evgeny fcb0768052 Protect SoundTouch lookahead when catching up live stream audio 2026-10-21 01:38:50 +02:00
evgeny 5d9da0c6b1 Preserve exact source EOF across tempo changes and expose DSP lookahead 2026-10-21 01:38:50 +02:00
evgeny e8ee608ba9 Document independent audio preparation and live stream catchup 2026-10-21 01:28:20 +02:00
evgeny 4e5530aca0 Isolate Android live video decoding from audio preparation 2026-10-21 01:23:35 +02:00
evgeny 4fa3b18cc2 Feed Android stream mixer from prepared PCM without holding DSP lock 2026-10-21 01:18:56 +02:00
evgeny 50cf61e9cd Mix partial stream PCM without copying stale buffer tails 2026-10-21 01:11:35 +02:00
evgeny c6ed01110e Exercise audio continuity during video stalls and generation changes 2026-10-21 01:03:19 +02:00
evgeny 1764603977 Isolate live video decoding from audio preparation in ChatGUI 2026-10-21 01:00:31 +02:00
evgeny 6c619d5f2a Recover stream playback stalls with bounded proportional catchup 2026-10-21 00:56:47 +02:00
evgeny 58c0fc6bba Separate stream audio DSP from bounded PCM consumption 2026-10-21 00:54:30 +02:00
evgeny 32e648ea9e Release audio holds at video EOF and stop Android batches promptly 2026-10-21 00:15:34 +02:00
evgeny b2efab9806 Document per-track completion and Android batch wakeups 2026-10-21 00:13:01 +02:00
evgeny a342316e7a Verify delayed decoders with unequal audio and video tails 2026-10-21 00:13:01 +02:00
evgeny c887780022 Consume Android batches with independent track completion 2026-10-21 00:13:01 +02:00
evgeny a538f99bf0 Expose viewer wake signals and complete audio independently on Android 2026-10-21 00:13:01 +02:00
evgeny 9fb4823df0 Batch Android viewer records and wake readers on data or stop 2026-10-21 00:13:01 +02:00
evgeny a2fb562a22 Drain desktop tracks independently and cancel obsolete media on reset 2026-10-20 23:54:55 +02:00
evgeny 852c05590d Expose track completion to desktop and retain the longer media tail 2026-10-20 23:53:58 +02:00
evgeny a54031081b Deliver independent track ends and invalidate batches on gaps 2026-10-20 23:53:23 +02:00
evgeny 23c4e4efbc Diagnose delayed decoder completion with buffered audio 2026-10-20 23:46:59 +02:00
evgeny 5bc0c1ece4 Document bounded desktop stream batches and worker wakeups 2026-10-20 23:42:28 +02:00
evgeny fbab6eae5d Anchor batch regression at a single initial keyframe 2026-10-20 23:41:12 +02:00
evgeny 0d9c728efe Test bounded batches, track budgets, notifications and FIN 2026-10-20 23:34:57 +02:00
evgeny 039c4a9ed4 Consume desktop media batches with per-track backpressure and wakeups 2026-10-20 23:31:50 +02:00
evgeny 179f248db8 Deliver bounded media batches to desktop consumers 2026-10-20 23:30:10 +02:00
evgeny 589cdcc81f Add bounded viewer batches and readiness notifications 2026-10-20 23:29:23 +02:00
evgeny 6997a82af1 Enable publisher timing logs in diagnostic test runs 2026-10-20 22:32:33 +02:00
evgeny c6f7d50838 Measure stream viewer request and worker delays 2026-10-20 22:30:04 +02:00
evgeny 7ce60ca0f2 Trace desktop stream stalls across playback and preview UI 2026-10-20 22:20:31 +02:00
evgeny b1112981fa Timestamp Android decoder output at the observation point 2026-10-20 21:44:10 +02:00
evgeny a4419b4c89 Validate Android surface timestamps in stream diagnostics 2026-10-20 21:40:20 +02:00
evgeny 1b036e4ea9 Trace stream startup decoding and presentation delays 2026-10-20 21:34:24 +02:00
evgeny f38aa3b08d Log stream startup key age and buffer handoff delays 2026-10-20 21:27:51 +02:00
608 changed files with 51825 additions and 22429 deletions

No files matched your search

+173 -34
View File
@@ -299,6 +299,9 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
│ ├── dm/ # Личные сообщения, E2E-вложения и offline-mailbox
│ ├── call/ # Звонки и общий голосовой стек
│ ├── radio/ # Групповая PTT-рация
│ ├── stream/ # Прямые трансляции: подписанные RS-блоки, выбор поставщиков, аудио/видео
│ ├── member_files/ # Файловое хранилище участников CHAT-группы и асинхронные передачи
│ ├── local_transport/ # Обнаружение устройств и доставка приглашений в локальной сети по UDP
│ ├── proxy/ # SOCKS5, TCP/UDP/ICMP прокси
│ ├── lwip_tcp/ # Встроенный lwIP TCP стек
│ ├── media_delivery/ # Блоки медиа каналов и передача неизменяемых файлов
@@ -336,13 +339,13 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `tun_linux.c` `tun_freebsd.c` `tun_windows.c` - Platform-specific TUN implementations
- `tun_route.c/h` - TUN routing table sync
- `config_parser.c/h` - INI-style config file parsing
- `config_updater.c/h` - Config file modification utilities
- `config_updater.c/h` - Проверка и сохранение согласованных X25519-ключей и node_id в INI-конфиге
- `control_server.c/h` - Control/monitoring server (etcpmon backend API)
- `firewall.c/h` - Firewall rules (per-interface filtering)
- `eim_nat.c/h` - Endpoint-Independent Mapping NAT engine
- `nat_transport.c/h` - NAT transport layer (packet relay)
- `ntp_time.c/h` - NTP time synchronization
- `ntp_node_time.c/h` - Inter-node time synchronization
- `nat_transport.c/h` - Передача IPv4 между отдельным TUN и удалённым NAT-провайдером через UTUN-router; локальный провайдер использует eim_nat
- `ntp_time.c/h` - Асинхронная NTP-синхронизация: поправка времени instance без изменения часов ОС
- `ntp_node_time.c/h` - Получение временной поправки от синхронизированных ETCP-пиров и диагностика расхождения часов
- `broadcast.c/h` - Широковещательная рассылка данных по topo-группе (UUID-дедупликация, TTL)
- `video/video.c/h` - Пробинг и транскодирование видео (FFmpeg, опционально; H.264/MP4 ≤720p)
- `dnsmasq/` - Архив dnsmasq-2.93.tar.xz для встроенной сборки
@@ -353,47 +356,54 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `etcp_connections.c/h` - Socket and link management, INIT handshake, keepalive, PING/PONG
- `etcp_session.c/h` - Согласование эпох ETCP-сессии через HELLO/CHALLENGE/CONFIRM и проверка принадлежности DATA сессии
- `etcp_loadbalancer.c/h` - Multi-link load balancing with traffic shaper
- `etcp_debug.c/h` - ETCP packet dump/formatting
- `etcp_debug.c/h` - Диагностический разбор секций ETCP_DGRAM и форматирование IPv4-адресов
- `etcp_dump.c/h` - ETCP дамп/декодирование пакетов
- `etcp_bbr.c/h` - BBR congestion control for ETCP
- `etcp_connect.c/h` - ETCP outbound connections
- `etcp_keepalive.c/h` - Keepalive-пакеты, адаптивный период, смена режима standby/normal (мобильные узлы)
- `auto_socket.c/h` - Автосоздание UDP+TCP сокетов на всех интерфейсах и линков к ETCP-соединениям (`auto_sockets=yes`)
- `pkt_normalizer.c/h` - Packet fragmentation/reassembly (packer/unpacker)
- `packet_dump.c/h` - Packet hex dump utility
- `dummynet.c/h` - Network emulator integrated into utun (for testing)
- `secure_channel.c/h` - AES-CCM encryption with X25519 key exchange, pubkey obfuscation
- `packet_dump.c/h` - Текстовая сводка IPv4-пакета: адреса, поля транспорта и короткий hex-фрагмент
- `dummynet.c/h` - Двунаправленный UDP-эмулятор между заданными endpoint: задержка, ограничение скорости и случайные потери
- `secure_channel.c/h` - X25519, пакетное AES-128-CCM, потоковое AES-CTR, подписи Ed25519 и обфускация публичного ключа
- `etcp_padding.c/h` - Выбор длины случайного padding и периодическое обновление диапазона для instance/соединения
- `etcp_tx_stats.c/h` - Общая статистика исходящих очередей, inflight и подтверждённых за секунду байтов ядра
- `crc32.c/h` - CRC32 checksums
- `stcp.c/h` - STCP протокол (Secure TCP)
- `stcp_server.c/h` - STCP сервер
- `stcp_client.c/h` - STCP клиент
- `stcp_link.c/h` - STCP линк-уровень
- `stcp.c/h` - Зашифрованные кадры поверх TCP: согласование ключей/эпох, AES-CTR, CRC, очереди и закрытие соединения
- `stcp_server.c/h` - Приём входящих STCP-соединений, handshake и передача готового соединения владельцу
- `stcp_client.c/h` - Исходящее STCP-подключение, в том числе через SOCKS5 и REALITY
- `stcp_link.c/h` - Подключение STCP к ETCP_LINK: передача пакетов, backpressure и жизненный цикл TCP-линка
- `socks_client.c/h` - Исходящий SOCKS5-транспорт: TCP CONNECT и UDP ASSOCIATE с переподключением control-сокета
- `node_conn_direct.c/h` - Общий прямой транспорт к пиру с отдельным handle каждого владельца; обновление линков по адресам узла
- `socket_monitor.c/h` - Socket state monitoring
- `socket_monitor.c/h` - Наблюдение за адресами интерфейсов и маршрутами ОС, обновление сетевого состояния ETCP-сокетов
- `reality.c/h` - REALITY-style TLS ClientHello/ServerHello камуфляж для STCP
- `reality_fingerprint.c/h` - Статические отпечатки (cipher suites, groups, ALPN) для REALITY-камуфляжа
- `reality_io.c/h` - Внешний REALITY-handshake и поток защищённых записей STCP, Finished, replay-защита и выбор relay
- `reality_record.c/h` - TLS-подобные AES-128-GCM записи: вывод ключей, счётчики направлений, padding и KeyUpdate
- `reality_relay.c/h` - Релей неавторизованных клиентов на реальный HTTPS-сайт
- `stun_nat.c/h` - Проверка mapping/filtering NAT через внешние STUN-серверы, RFC 5780 и кеш результатов по IP/интерфейсу
- `call_ring.c/h` - Кольцевой трейс контрольных точек освобождения ETCP/STCP-ресурсов для диагностики teardown/UAF
**BBR (src/transport_layer/BBR/)**
- `bbr_v3.c` - BBR congestion control algorithm v3
**Routing Layer (src/routing_layer/)**
- `routing.c/h` - Routing table (local)
- `route_lib.c/h` - Routing library
- `route6_lib.c/h` - IPv6 routing
- `routing.c/h` - Обмен IPv4-пакетами UTUN между локальным TUN и сервисом DATA ETCP-router
- `route_lib.c/h` - Локальная таблица IPv4-подсетей UTUN: добавление/удаление маршрутов и поиск узла назначения
- `route6_lib.c/h` - Локальная таблица IPv6-подсетей на radix tree с поиском по самому длинному префиксу
- `route_ping.c/h` - Пинг цели через явно выбранного BGP-посредника для NAT-проверки
- `route_connectivity.c/h` - Route connectivity checks
- `route_connectivity.c/h` - Прямые UDP/TCP-пробы адресов узла для оценки достижимости и RTT при выборе пиров
- `topo_group.c/h` - Сессии UTUN/CHAT (JOIN/ACCEPT, эпохи, READY), групповые запросы, NODEINFO/WITHDRAW и владение NCD
- `topo_group_connect.c/h` - Подбор пиров CHAT-группы по роли: суперузлы → desktop → mobile; история/RTT внутри класса, backoff и standby burst
- `topo_group_invite.c/h` - Получение описания канала через INVITE_INFO_REQ/RESP; добавление мембера реализовано в chat_join/chat_sync
- `topo_node.c/h` - Node management (модель узла: TOPO_NODE/TOPO_GROUP_NODE, сериализация, node_registry)
- `topo_node_sqlite.c/h` - SQLite persistence for nodes
- `topo_node.c/h` - Модель TOPO_NODE/TOPO_GROUP_NODE, подписанные снимки узлов, сериализация и общий node_registry
- `topo_node_sqlite.c/h` - Хранение подписанных снимков узлов, адресов, каналов и блоков мемберов в общей SQLite
- `topo_recovery.c/h` - Восстановление потерянных маршрутов группы через READY-сессии, отмена попыток при stop
- `conn_mgr.h` + `conn_mgr_core.c`/`conn_mgr_indirect.c`/`conn_mgr_monitor.c` + `conn_mgr_priv.h` — Connection Manager (handle-based API, 3-phase DIR/REV/IND)
- `etcp_router.c/h` - ETCP router/multiplexer (мультиплексирование каналов, transit forwarding)
- `nat_detection.c/h` - NAT type detection
- `etcp_router.c/h` - Надёжные логические каналы сервисов поверх BGP-маршрутов: ACK, окна, транзит и защита от циклов
- `route_tx_stats.c/h` - Скользящее окно подтверждённых payload-байтов логического канала для оценки скорости доставки
- `multicast.c/h` - Рассылка списку узлов группы с разделением маршрута на ветви, локальными подписками и отчётами о потерях
- `nat_detection.c/h` - Проверка mapping и доступности входящих рабочих сокетов через ETCP-пиров с учётом предварительных STUN-измерений
- `address_profile.h` - Профиль endpoint: изменение адреса, доступность входящих и источник измерения ETCP/STUN
- `sock_match.c/h` - Классификация локальных сокетов и адресов пира, отбор совместимых пар для прямого подключения
- `route_crypto.c/h` - Пред/пост-обработка SVC_ROUTE пакетов (encrypt-then-sign)
@@ -408,9 +418,15 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `member_sync.c/h` - Синхронизация участников каналов поверх merkle_sync. Два подписанных блока (мембер/владелец), по-блочное сравнение версий, верификация подписей. Удаление — только битая запись
- `merkle_sync.c/h` - Универсальный движок синхронизации на Merkle-деревьях (5 уровней × 32 бакета, SHA256). Пиры обмениваются хешами и передают только различия; per-session out-очередь с backpressure. Не привязан к конкретному типу данных (коллбэки `data_ops`)
- `merkle_tree.c/h` - Производный индекс Merkle-хешей в SQLite; восстанавливается из данных модели, обновляется с ними в одной транзакции
- `chat_profile.c` - Собственный профиль: имя узла, сетевые адреса, сохранение UI-состояния (часть chat_core)
- `chat_profile.c` - Подписанный профиль мембера: имя, аватар и опции сервисов; локальное UI-состояние (часть chat_core)
- `chat_channel.c` - Создание и настройка каналов: таблицы, криптоключи, CHAT-группа и планировщик подключения (часть chat_core)
- `chat_msg.c` - Отправка и приём сообщений: запись в локальную БД, автоматическая рассылка всем участникам канала (часть chat_core)
- `chat_feedback.c/h` - Подписанные реакции и ответы: версии состояния, сохранение отмен и Merkle-синхронизация между участниками
- `chat_poll.c/h` - Создание опросов, голосование, реакции и JSON-снимки результатов для UI/headless
- `chat_poll_codec.c` - Проверка и wire-кодеки опросов, ответов и реакций; общий каталог эмодзи (API в chat_poll.h)
- `chat_attachment.c/h` - Зашифрованные вложения канала: ключи, запросы доступа, ручное одобрение автора и сохраняемый outbox
- `chat_local.c/h` - Обнаружение устройств чата и обмен готовыми invite-ссылками через локальный UDP-транспорт
- `group_avatar.c/h` - Подписанная аватарка канала: хранение версии в SQLite, проверка подписи и передача блоков через router
- `chat_status.c` - Сбор диагностики: текущее время, активные соединения, типы NAT. Отправляется в GUI (часть chat_core)
- `chat_member.c/h` - Единая структура мембера для отображения в GUI (десктоп/headless/Android), фиксированная сериализация на проводе
- `chat_admin.c/h` - Передача канального приватного ключа (прав админа) другому узлу
@@ -422,7 +438,8 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
**Личные сообщения (src/dm/)**
- `dm_core.c/h` - Беседы двух узлов, подписанные E2E-сообщения, SQLite/outbox, доставка через router/mailbox и квитанции
- `dm_crypto.c/h` - Вывод ID беседы и ключа X25519, AES-256-CCM сообщений и потоковое шифрование/проверка медиа
- `dm_crypto.c/h` - Вывод ID беседы и ключа X25519, AES-256-CCM сообщений и E2E-контейнеры медиа через media_crypto
- `dm_history.c/h` - Локальное удаление истории и скрытие бесед; поколения задач и отпечатки удалённых сообщений против повторов
- `dm_mailbox.c/h` - Offline-очередь суперузла: хранение подписанных сообщений и повтор доставки до квитанции получателя
- `dm_media.c/h` - Подготовка, доставка и публикация E2E-вложений; ciphertext-хранилище и отдельные квитанции файлов
- `dm_media_priv.h` - Внутренние SQL/control-операции для dm_media и dm_mailbox_media
@@ -430,11 +447,25 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
**Звонки (src/call/)**
- `call.c/h` - Сигналинг и медиа P2P-звонка через ETCP-router, состояния звонка и оптимизация пути через conn_mgr
- `call_proto.h` - Wire-формат сигналинга и медиа, треки/кодеки, причины завершения
- `call.c/h` - P2P аудио/видеозвонок: подписанный E2E-сигналинг, состояния, политика занятости и оптимизация пути через conn_mgr
- `call_priv.h` - Общее внутреннее состояние сессии для call.c и call_video_transport.c; доступ только из uasync
- `call_proto.h` - Wire-формат сигналинга, аудиокадров и управления видеотреком; треки/кодеки и причины завершения
- `call_audio.c/h` - Общий аудио-движок: кодирование/декодирование Opus, jitter-буфер и тоны, API feed/pull PCM
- `call_jitter.cpp/h` - FIFO кодированных Opus-кадров, адаптивный запас и SoundTouch time-stretch при воспроизведении
- `call_jitter.cpp/h` - FIFO Opus, адаптивный сетевой запас, декодирование на pull и восстановление воспроизведения после пауз
- `call_jitter_window.h` - Окно из десяти секундных интервалов для оценки глубины очереди и изменения задержки доставки
- `call_clock.h` - Метки времени захвата и снимок реально проигранного аудио для синхронизации видеотрека
- `call_delay.cpp/h` - Уменьшение накопленной задержки PCM прямым ускорением и SoundTouch с сохранением исходной шкалы времени
- `call_timeline.cpp/h` - Карта исходного PTS через обработку SoundTouch до позиции аудиоустройства
- `call_video.c/h` - Общий формат H.264/H.265 видеотрека, согласование возможностей и сборка кадров из фрагментов
- `call_video_bitstream.c/h` - Проверка конфигурации и IDR без reorder, профиля/уровня и геометрии H.264/H.265; KEY-пробы кодеков
- `call_video_transport.c/h` - Отдельные router-каналы control/audio/video, backpressure, привязка видео к аудио и запросы KEY
- `call_video_rate.c/h` - Регулятор битрейта и допуска новых видеокадров по скорости доставки, RTT и долгу очереди
- `call_video_port.c/h` - Потокобезопасный обмен кадрами и командами между платформенными video workers и uasync-транспортом
- `call_video_playout.c/h` - Расписание показа по аудиочасам, ожидание/отбрасывание кадров и подтверждение фактического показа
- `call_video_fixtures.h` - Встроенные H.264/H.265 KEY-пробы для проверки базовых режимов кодека
- `call_video_fast_fixtures.h` - KEY-пробы видеорежимов с частотой 60 fps
- `call_video_fullhd_fixtures.h` - KEY-пробы видеорежимов Full HD
- `call_video_portrait_fixtures.h` - KEY-пробы видеорежимов в портретной ориентации
- `call_tones.c/h` - Генерация PCM-тонов паузы и завершения звонка
- `call_headless.c/h` - Отдельный TCP-аудиосокет headless-клиента: обмен PCM; сигналинг остаётся на control-сокете
@@ -445,14 +476,17 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `radio_audio.c/h` - Общий аудио-движок: независимые RX/TX, Opus, микширование источников, ручной PTT и VAD-автомат
- `radio_jitter.cpp/h` - Буфер кодированных кадров каждого источника с pre-roll и SoundTouch time-stretch на pull
- `radio_vad.h` - Ресемплинг 48→16 кГц и автомат авто-PTT с подтверждением речи, задержкой завершения и учётом занятого канала
- `radio_auto.h` - Общий автомат автопередачи по VAD/уровню звука, без захвата аудио и сетевого I/O
- `radio_call.c/h` - Адресное приглашение участника в рацию канала: сигналинг вызова, занятость, принятие и отмена
- `radio_headless.c/h` - TCP-аудиосокет headless-рации: PCM и команды начала/конца PTT
**Прокси (src/proxy/)**
- `proxy_protocol.h` - Общий TCP-протокол и управление потоком: CONNECTED, DATA, кредит WINDOW, упорядоченный FIN
- `socks_proxy.c/h` - Клиентские SOCKS5 CONNECT и HTTP proxy/CONNECT, DNS и передача потока выбранному exit-узлу
- `udp_proxy.c/h` - UDP прокси
- `icmp_proxy.c/h` - ICMP прокси
- `http_proxy.c/h` - Потоковый HTTP/1.x кодек: проверка заголовков, разбор тела/chunked и учёт потреблённых байтов для WINDOW
- `udp_proxy.c/h` - Передача UDP из TUN через exit-узел, сопоставление ответов и удаление неактивных потоков
- `icmp_proxy.c/h` - Проксирование IPv4 ICMP echo через raw socket exit-узла с восстановлением исходных id/seq
- `tcp_proxy_server.c/h` - Exit-сторона TCP-прокси: реальные исходящие сокеты, потоки по (peer, stream_id), окна и half-close
- `tcp_proxy_client.c/h` - Клиентская сторона TCP-прокси: адаптер TUN/lwIP и обмен с настроенным exit через ETCP-router
@@ -474,6 +508,45 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `attachment.c/h` - Общие метаданные file/voice/video, проверка и компактная wire-сериализация
- `voice_file.c/h` - Worker-кодирование PCM в контейнер Opus с длительностью и waveform для desktop/Android
- `attachment_send.c/h` - Асинхронная подготовка голоса/видео и отправка в зафиксированный канал или DM
- `media_crypto.c/h` - Общие AES-256-CCM примитивы и порционное шифрование/проверка файлов для DM и групповых вложений
- `encrypted_file.c/h` - Контейнер группового вложения: шифрование метаданных/файла, проверка SHA256 и durable-публикация результата
**Трансляции (src/stream/)** — live-аудио/видео CHAT-группы с подписью источника, Reed–Solomon и раздачей через участников.
- `stream.c/h` - Жизненный цикл трансляции, локальные handles издателя/читателей, события и публикация подписок
- `stream_priv.h` - Внутренний доступ к сессиям, приём проверенных DATA/END и взаимодействие с планировщиком relay
- `stream_proto.h` - Форматы подписанной пачки и RS-частей, дескриптор источника, размеры и коды результата
- `stream_block.c/h` - Подпись пачки Ed25519 до RS-кодирования, разбор частей и проверка восстановленных данных
- `stream_buffer.c/h` - Ограниченное кольцо RS-частей: дедупликация, восстановление пачек, проверка подписи и вытеснение старых данных
- `stream_source.c/h` - Упаковка исходных байтов в подписанные RS-пачки, сброс неполной пачки по таймеру и завершение источника
- `stream_member.c/h` - Объявления источников и подписок в опциях подписанного мембера; разбор и проверка принадлежности источника
- `stream_wire.c/h` - Протокол выдач и relay: кодограммы управления, пути, метрики и подписанное завершение END
- `stream_network.c/h` - Выдача блоков через прямой ETCP, NCD-владение, управляющие очереди, аренда подписок и reverse-запросы
- `stream_optimizer.c/h` - Автоматический выбор и перенос поставщиков субпотоков, пробы доступности и привлечение relay
- `stream_metrics.c/h` - Оценка относительного отставания субпотока и истории проблем по часам источника без NTP
- `stream_load.c/h` - Учёт логической очереди DATA и её роста для обнаружения перегрузки раздающего узла
- `stream_media.c/h` - Формат uAV2: упаковка/проверка видеокадров, конфигурации кодека, PTS и Opus внутри FEC-пачек
- `stream_media_buffer.c/h` - Медиаиндекс общего RS-кольца и независимые курсоры audio/video; сборка кадров и восстановление с KEY
- `stream_publisher.c/h` - Общий потокобезопасный publisher desktop/Android: очередь готовых кадров, публикация и завершение с FIN
- `stream_viewer.c/h` - Независимые viewers: запрос ограниченных батчей по бюджетам треков, уведомления готовности и конца audio/video
- `stream_playback_audio.cpp/h` - Opus/SoundTouch и очередь готового PCM; ускорение без изменения высоты и привязка к позиции устройства
- `stream_playback_clock.c/h` - Общие часы воспроизведения и плавный догон live-потока по непрерывному запасу готового материала
**Файлы участников (src/member_files/)**
- `member_files.c/h` - Сервер и асинхронный клиент хранилища CHAT-группы: права доступа, запросы и события через защищённый router
- `protocol.c/h` - Wire-кодек файловых команд, снимки версий каталогов/файлов и токены временных загрузок
- `storage.c/h` - Серверные каталоги участников, квоты, файловые операции, временные загрузки и их очистка
- `transfer.c` - Передача файлов с CM handle, backpressure, дисковым worker, проверкой хеша и публикацией результата
- `disk.c/h` - Дисковые примитивы POSIX/Windows: безопасное разрешение путей без ссылок и I/O по 64-битному смещению
- `worker.c/h` - Единственный дисковый pthread с ограниченной очередью и возвратом результатов в uasync
- `private.h` - Внутреннее состояние запросов, передач, серверного хранилища и дискового worker
**Локальная сеть (src/local_transport/)**
- `local_transport.c/h` - Открытый UDP-транспорт LAN: обнаружение устройств, доставка сообщений с ACK/повторами и ограничения входящего потока
- `platform.c/h` - Снимок IPv4/IPv6 интерфейсов, broadcast/multicast-адреса, приём метаданных UDP и проверка локальности источника
- `protocol.h` - Wire-формат объявлений, сообщений и ACK: ID/эпохи устройств, ID запроса и проверка UTF-8 имени
**Libraries (lib/)**
- `u_async.c/h` - Async event loop (epoll/poll/select, timers via timeout_heap)
@@ -484,6 +557,7 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `mem.c/h` - Memory wrappers with leak tracking (`u_malloc`/`u_free`/`u_calloc`/`u_strdup`)
- `serialize.c/h` - Binary serialization utilities
- `reed_solomon.c/h` - Кодирование и восстановление пачки данных Рида—Соломона с настраиваемым числом субблоков и избыточностью
- `video_plane.c/h` - Копирование YUV-плоскости с поворотом 0/90/180/270°, учётом stride и проверкой границ без выделения памяти
- `swm_min.c/h` - Sliding window minimum (for RTT min tracking)
- `getmyip.c/h` - Локальный IP, который ОС выберет для UDP к заданному адресу (bind/connect/getsockname)
- `myip.c` - My IP helper (no header)
@@ -509,6 +583,16 @@ UASYNC/LL_QUEUE/MEMORY/TIMING объединены в SYS, NORMALIZER/BBR — в
- `soundtouch-master/` - SoundTouch: изменение темпа аудио в jitter-буферах звонка и рации
- `wintun.h` - Wintun API header for Windows TUN driver
**Проверки (tests/)**
- `Makefile.am` - Состав тестовых целей, зависимости и отдельные проверки runner, service IDs и скрипта сборки
- `run_tests.py` - Запуск тестов ограниченным пулом с отдельными логами и последовательными барьерами для CPU/LAN-проверок
- `test_run_tests.py` - Проверки пула runner, эксклюзивного запуска, обработки ошибок и сигналов
- `test_service_ids.py` - Проверка уникальности ID транспортных и router-сервисов
- `test_build_script.py` - Регрессии выбора платформы, конфигурации и инкрементальной работы build.sh
- `stream_netns/` - Linux network namespaces: топологии, NAT/STUN-сценарии и проверка трансляций в изолированной сети
- `local_transport_ipv6.sh` - Проверка IPv6 link-local обнаружения и доставки в изолированной локальной сети
**Memory Pools in UTUN_INSTANCE:**
- `data_pool` — для данных пакетов (payload), используется input_queue/output_queue фрагментами
- `pkt_pool` — для struct ETCP_DGRAM (сетевые пакеты на отправку/приём)
@@ -685,11 +769,18 @@ cmake .. && make -j4
| Файл | Назначение |
|------|-----------|
| `main.cpp` | Точка входа, инициализация QApplication |
| `mainwindow.h/cpp` | Главное окно, списки каналов/DM/участников, сообщения, трей, события ядра и окна звонков |
| `chatview.h/cpp` | QListView с фоновым изображением, hover-сигнал, drag-to-select |
| `main.cpp` | Точка входа, QApplication, язык интерфейса и запуск единственного экземпляра профиля |
| `single_instance.h/cpp` | Блокировка каталога профиля и передача запроса показа окна уже работающему экземпляру |
| `gui_language.h` | Выбор языка интерфейса независимо от регионального формата дат и чисел |
| `mainwindow.h/cpp` | Главное окно, каналы/DM, трей, события ядра, звонки, локальные приглашения и окна трансляций |
| `chatview.h/cpp` | Список сообщений: фон, hover, выделение текста и прокрутка истории мышью/клавиатурой |
| `messagelist.h/cpp` | Модель сообщений из БД, обновления доставки/медиа, анимации, состояние просмотра канала |
| `messagedelegate.h/cpp` | QStyledItemDelegate: бабблы, текст, цитаты, реакции, оверлей анимаций |
| `messagefeedback.cpp` | Действия MessageList: создание опроса, голосование, выбор реакции и показ результатов |
| `attachment_key_ui.cpp` | Список запросов на расшифровку групповых вложений и решения автора о выдаче ключа |
| `message_links.h/cpp` | Разбор границ utun:// и HTTP/HTTPS ссылок для подсветки и обработки нажатий в сообщении |
| `message_notification.h/cpp` | Уведомление о сообщении без захвата фокуса, переход в беседу и скрытие после активности пользователя |
| `user_activity_monitor.h/cpp` | Ожидание первой активности через системный idle time либо события приложения |
| `emoji.h/cpp` | EmojiData/EmojiCategory/EmojiType, 4 категории, `g_animatedEmojiMap` |
| `emojipanel.h/cpp` | Эмодзи-пикер: QTabWidget + QGridLayout, EmojiButton с hover-анимацией |
| `emojitabbar.h/cpp` | Кастомный QTabBar для эмодзи-панели |
@@ -705,11 +796,13 @@ cmake .. && make -j4
| `invitedialog.h/cpp` | Диалог приглашения пользователя |
| `invite_link.h/cpp` | Логика invite-ссылок |
| `inviteby.h/cpp` | Диалог приглашения по invite-ссылке (отправка через узел) |
| `dminvitedialog.h/cpp` | Выбор канала для приглашения собеседника из DM с сопоставлением результата по request_id |
| `settingsdialog.h/cpp` | Окно настроек |
| `creategroupdialog.h/cpp` | Диалог создания группы |
| `channelsettingsdialog.h/cpp` | Диалог настроек канала |
| `renamedialog.h/cpp` | Диалог переименования |
| `networksettingspage.h/cpp` | Страница сетевых настроек |
| `addressprofiletext.h` | Читаемое представление mapping, доступности входящих и источника NAT-измерения |
| `nodespage.h/cpp` | Таблица узлов + дамп в настройках |
| `statuspage.h/cpp` | Страница статуса (NTP + соединения + живые часы) |
| `databasesettingspage.h/cpp` | Страница обслуживания БД в настройках |
@@ -718,14 +811,32 @@ cmake .. && make -j4
| `audiodevicesettingspage.h/cpp` | Страница настроек аудиоустройств |
| `sound_manager.h/cpp` | Общий miniaudio-контекст, уведомления/PCM, микширование голоса, маршруты устройств и восстановление |
| `audio_device.h/cpp` | Жизненный цикл miniaudio-устройства, прогресс callbacks, пауза и повтор запуска после сбоя |
| `audio_buffer_tuning.h` | Подбор размера аппаратного аудиобуфера по повторным XRUN с сохранением настройки при reopen |
| `audio_capture.h/cpp` | Совместное использование одного микрофона трансляцией и рацией, подписки PCM и смена профиля захвата |
| `audio_capture_lease.h` | Владение захватом, исключающее одновременную работу publisher, звонка и записи голосового сообщения |
| `audio_event_queue.h` | Ограниченная очередь PCM/команд с несколькими producers и резервом для управляющих событий |
| `audio_frame_ring.h` | Предвыделенное SPSC-кольцо кадров переменной длины под mutex; при переполнении удаляет старейший |
| `voice_audio_io.h/cpp` | Workers захвата/AEC и воспроизведения; упорядочивание PCM/PTT/mute и итоговый render-reference |
| `voice_capture_clock.h` | Непрерывные PTS захваченного PCM по позиции сэмплов с плавной поправкой аппаратного времени |
| `voice_playback_buffer.h` | Адаптивный запас готового PCM: увеличение после недостачи и уменьшение при стабильном выводе |
| `audiorecorder.h/cpp` | Захват голосовых сообщений, worker обработки PCM, компрессор, индикатор уровня и передача записи задаче |
| `voicemessageencoder.h/cpp` | Выбор Opus preset и создание медиа-каталога; кодирование выполняет общий voice_file |
| `voiceplayback.h/cpp` | Воспроизведение голосовых сообщений |
| `callwindow.h/cpp` | Окно звонка: состояния, управление, путь соединения и статистика аудио |
| `callwindow.h/cpp` | Окно аудио/видеозвонка: управление, выбор камеры, локальное/удалённое видео и статистика |
| `call_audio_engine.h/cpp` | Desktop-контур звонка: захват, VoiceAudioIo, общий выход SoundManager и завершение аудиосессии |
| `call_video_engine.h/cpp` | Desktop-видеозвонок: камера, TX/RX workers, обмен с ядром и показ кадров по аудиочасам |
| `call_video_encoder.h/cpp` | Кодер звонка: согласованный формат, смена битрейта с drain прежнего кодера и подтверждение настройки |
| `call_video_decoder.h/cpp` | FFmpeg-декодер H.264/H.265 с выбором GPU/CPU и сохранением PTS при задержанном выходе |
| `call_camera_capture.h/cpp` | Управление отдельным процессом камеры, каталог устройств/режимов и получение кадров |
| `call_camera_worker.cpp` | Процесс Qt-захвата камеры: настройка устройства, отбор кадров и выдача пикселей через shared memory |
| `call_camera_source.h` | Интерфейс backend камеры для процесса захвата и подстановки устройств в тестах |
| `call_camera_protocol.h` | Локальный протокол управляющих команд, каталога устройств и FRAME/ACK процесса камеры |
| `call_camera_buffer.h` | Владение shared-memory буфером пикселей между процессами захвата и GUI |
| `call_camera_rate.h` | Временное отключение снижения FPS из-за автоэкспозиции V4L2 с восстановлением настройки камеры |
| `call_video_cadence.h` | Отбор исходных кадров под заданный FPS без накопления ошибки дробного периода |
| `call_video_capture_clock.h` | Привязка времени камеры к монотонным часам с коррекцией задержки первого callback |
| `call_video_queue.h` | Ограниченная SPSC-очередь кадров между video workers без mutex и выделений памяти |
| `call_video_wake.h` | Пробуждение video worker по событию или абсолютному монотонному deadline на POSIX/Windows |
| `radio_audio_engine.h/cpp` | Desktop-контур рации: независимые захват/прослушивание, PTT/VAD и подключение к общему выходу |
| `radiopanel.h/cpp` | Панель активной рации: выбор аудиоустройств, VAD, PTT и состояние передачи из ядра |
| `radiosettingspage.h/cpp` | Настройки рации и запись глобальных сочетаний PTT |
@@ -736,6 +847,17 @@ cmake .. && make -j4
| `imageviewer_window.h/cpp` | Окно просмотра изображений |
| `videoplayer_engine.h/cpp` | Движок декодирования видео |
| `videoplayer_window.h/cpp` | Окно просмотра видео |
| `live_video_encoder.h/cpp` | Общий FFmpeg-кодер live-видео для звонка/трансляции, GPU/CPU, KEY-конфигурация и capture PTS |
| `video_frame_image.h` | Выделение выровненного RGB-буфера FFmpeg и передача владения QImage без копирования |
| `stream_publish_engine.h/cpp` | Публикация камеры/экрана и Opus: workers кодирования, локальный preview и завершение через общий publisher |
| `stream_publish_window.h/cpp` | Настройки и управление трансляцией; закрытие окна сохраняет работающий engine |
| `stream_portal_capture.h/cpp` | Захват экрана Wayland через ScreenCast portal/PipeWire, выбор монитора и режима курсора |
| `stream_screen_cursor.h/cpp`, `stream_screen_cursor_win.cpp` | Наложение системного курсора на кадр выбранного экрана средствами X11/Windows |
| `stream_video_modes.h` | Выбор размеров/FPS камеры и пропорционального масштабирования экрана для трансляции |
| `stream_qp_control.h` | Плавная корректировка потолка QP по фактическому выходному видеобитрейту |
| `stream_sources.h` | Разбор снимка активных источников канала из CHAT_EVT_STREAM_SOURCES |
| `streamplayer_window.h/cpp` | Просмотр live-потока: отдельные audio/video workers, смешивание PCM и синхронизация по часам устройства |
| `streamgrid_window.h/cpp` | Сетка трансляций канала и разворачивание плитки с сохранением плееров; своя плитка использует локальный preview |
| `connmonitorwindow.h/cpp` | Окно мониторинга ETCP-соединений (standalone) |
| `qrcode_utils.h/cpp` | Утилиты для QR-кодов |
| `debug_ui.h` | Отладочный UI |
@@ -745,9 +867,14 @@ cmake .. && make -j4
- `db/` — `db_manager.h/cpp`: доступ GUI к данным nodes, channels, msg_<channel>, peers_<channel> и состоянию интерфейса;
копия `db/sqlite3.c/h` не используется текущей CMake-целью, SQLite берётся из `lib/`
- `transport/` — интеграция GUI с ядром; файлы перечислены ниже
- `src/avatars/` — `avatarmanager.h/cpp`: загрузка/публикация аватаров участников через member_files, подписанные ссылки и очередь передач;
`avatarcache.h/cpp`: дисковый кеш, подготовка изображений и очистка; `avatarcrop.h/cpp`: выбор области;
`avatargallery.h/cpp`: просмотр аватаров пользователя; `groupavatarmanager.h/cpp`: подготовка и кеш подписанных аватарок каналов
- `src/member_files/` — `browser.h/cpp`: браузер серверных файлов группы; `batch.h/cpp`: последовательные операции над файлами/деревьями,
фоновый локальный обход, загрузка/скачивание и отмена
- `libutun/CMakeLists.txt` — состав и зависимости CMake-сборки общего ядра из `src/` и `lib/`
- `CMakeLists.txt` — приложение vibechat, Qt-зависимости, ресурсы и цели GUI-тестов
- `tests/` — проверки аудиоустройств и восстановления, голосового тракта, jitter-буферов, PTT, кодирования и GUI-компонентов
- `tests/` — проверки аудиотракта, PTT, видеозвонка, захвата камеры/экрана, трансляций, файлового браузера и GUI-компонентов
- `resources/` — `bg.jpg`, `chatgui.qrc` (встраивает 5 TGS в бинарник), `animations/*.tgs`
- `zxing-cpp/` — встроенная библиотека QR-кодов; `third_party/qhotkey/` — исходники QHotkey
@@ -761,6 +888,7 @@ cmake .. && make -j4
| `config_updater.h/cpp` | Изменение настроек в конфиге |
| `miniaudio_impl.c` | Реализация single-header miniaudio в отдельной единице трансляции |
| `audio_diagnostics.h/cpp` | Диагностика этапов аудио и событий backend; записывает метаданные без PCM |
| `stream_bridge.h/cpp` | Потокобезопасный мост к общим publisher/viewers: session IDs, каталог источников, запросы батчей и пробуждение workers |
### Система анимированных эмодзи
@@ -864,6 +992,13 @@ void lottie_animation_destroy(Lottie_Animation *anim);
- `src/routing_layer/topo_group.h` — протокол групповой сессии и критерий READY
- `tests/test_chat_join_e2e.c`, `tests/test_topo_recovery.c`, `tests/test_services.c` — проверки join, recovery и жизненного цикла
- `doc/dm_arch.md`, `doc/chat_call_arch.md` — личные сообщения и звонки
- `doc/call_video.txt` — видеотрек звонка: возможности кодеков, транспорт, битрейт, аудиочасы и показ кадров
- `doc/stream.txt` — трансляции: RS-пачки, выдачи/relay, выбор поставщиков, медиакурсоры и платформенные плееры
- `doc/stun_nat.md`, `doc/nat_detection.txt` — STUN-измерения и определение доступности рабочих UDP-сокетов
- `doc/member_files.txt` — файловое хранилище участников, права, wire-команды и передачи
- `doc/group_avatar.txt` — подписанная аватарка канала, хранение и репликация
- `doc/encrypted_attachments.md` — контейнер группового вложения, запросы ключа и ручная выдача доступа
- `doc/pm_invites.md`, `doc/message_links.md` — приглашения из личной беседы и ссылки внутри сообщений
- `doc/proxy_protocol.md` — TCP/UDP/ICMP-прокси, управление окнами, half-close и проверки
- `doc/lwip_nuances.md` — особенности адаптированного lwIP TCP-стека
- `doc/linux_audio_recovery.md` — диагностика аудиотракта Linux, контракты владения и восстановление устройств
@@ -906,7 +1041,11 @@ C-библиотека `utun_lite` собирает `lib/` и `src/` по `libut
| `libutun_lite/photo_sender.c/h` | Отправка фотографий в канал с MIME-типом и размерами изображения |
| `libutun_lite/video_sender.c/h` | Отправка видео через общий механизм подготовки вложений |
| `libutun_lite/voice_recorder.c/h` | Накопление PCM записи, компрессор и отправка голоса через общий attachment_send |
| `libutun_lite/stream_bridge.c/h` | Android-мост к publisher/viewers: session IDs, готовые access units/Opus и ограниченные батчи треков |
| `jni_bridge/android_jni_bridge.c/h` | JNI-команды и callbacks между Kotlin и C-ядром, события и аудио API |
| `jni_bridge/android_call_output_jni.c` | Вывод PCM звонка через AAudio с аппаратным тактированием и снимками позиции устройства |
| `jni_bridge/android_call_video_jni.c` | JNI видеозвонка: capabilities/формат, обмен кадрами с call_video_port, playout и поворот YUV |
| `jni_bridge/android_stream_jni.c` | JNI трансляций: publisher/viewer API, Opus/SoundTouch, готовый PCM и часы воспроизведения |
| `jni_bridge/android_udp_log.c/h` | Потокобезопасный буфер UDP-логов с flush-таймером в потоке ядра |
| `headless/headless_main.c` | Отдельный каркас control-loop; сам не запускает ядро/чат |
| `headless/headless_control.c/h` | Каркас control-сокета; часть команд — заглушки, рабочий API — src/chat/chat_headless_control.c/h |
+9 -3
View File
@@ -153,10 +153,14 @@ general=info # общая информация
```ini
[firewall]
allow=all # разрешить всё (bypass)
allow=192.168.1.0/24 # разрешить подсеть
allow=192.168.1.10 # разрешить весь IP
allow=10.0.0.1:80 # разрешить конкретный IP:порт
```
Фильтр основного TUN проверяет IP и TCP/UDP-порт назначения одинаково в обоих направлениях.
Без разрешений весь трафик этого интерфейса запрещён; ответы требуют отдельных разрешений.
CIDR-подсети не поддерживаются. Ошибочное правило отменяет загрузку нового конфига.
### 3.7. `[nat]` — EIM NAT
```ini
@@ -553,11 +557,13 @@ dump=trace
```ini
[firewall]
allow=all # всё разрешено
allow=10.0.0.0/8 # разрешить подсеть
allow=10.0.0.5 # разрешить весь IP
allow=192.168.1.5:22 # разрешить IP:порт
```
По умолчанию без секции `[firewall]` — фильтрация выключена.
Без секции `[firewall]` весь входящий и исходящий трафик основного TUN запрещён.
`allow=IP` и `allow=IP:0` разрешают все протоколы IP; `allow=IP:port` — TCP/UDP указанного порта.
Для фрагментов IPv4 нужен весь IP либо `allow=all`. NAT и прокси используют отдельные TUN интерфейсы.
### 7.3. Проброс портов
+128
View File
@@ -0,0 +1,128 @@
Chat: исправление приглашений и проверка контрактов — 26.10.2026
Граница работы
В начале аудита по /home/user/utun-doc-audit-checklist.txt установлены два дефекта:
успех invite_link_decode без узла подключения и разыменование непроверенного
выделения cm_invite_wrap. После согласования архитектуры исправлен весь путь
команды JOIN; добавлены тесты контрактов, отказов и жизненного цикла.
Полный аудит всех модулей src/chat не завершён. chat_sync.h остаётся [~].
Реализация
1. invite_link.c/.h — единый C-кодек для headless, Qt и Kotlin/JNI.
Только версия 0x03, строгий канонический base64 с padding, полный разбор,
одинаковый ключ во всех блоках, 1..32 адреса, family=4/6, proto=1/2/6,
ненулевой порт, согласованный REALITY. Канал — 1..INT64_MAX, join_key —
любой ненулевой uint64. Нулевой первый байт публичного ключа допустим.
Строки ограничены, превышение длины отклоняется; скрытый NUL не принимается.
Успех decode полностью определяет InviteData; ошибка оставляет out прежним.
Encode и сериализация адресов также не меняют выход при отказе.
Максимум с NUL — INVITE_LINK_MAX=1552: 32 IPv6, REALITY и пароль127 байт.
Из frontend удалены отдельные реализации декодирования; Qt/Kotlin только
преобразуют результат C-кодека в своё представление.
2. chat_sync_join_link/offer_link выполняются только в uasync и принимают ссылку.
Промежуточные chat_invite/addrs_data/cm_invite_wrap удалены. NCD bootstrap
строится целиком, включая REALITY; ошибка освобождает все временные адреса.
Общий объект локальной операции хранит роль, фазу, request/attempt, NCD и deadline.
PREPARE не реагирует на UP до установки handle; затем уже работающий транспорт
запускает обмен без ожидания повторного UP. Другой владелец сохраняет свой NCD.
BUSY относится к локальной операции; чужие JOIN обслуживаются независимо.
3. INFO_REQ/RESP, REQUEST, READY и ERROR несут attempt_id:8BE сразу после type.
JOIN_READY содержит также ровно8 байт ожидаемого group_id. Ответ сопоставляется
с peer/group/attempt/phase; повторное использование ссылки создаёт новый attempt.
Connection-узел хранит отдельные waiters попыток; повторный запрос не затирает
токен прежнего waiter. Есть deadline и ограничение8 waiters на peer; просроченные
удаляются при новых запросах и штатном обновлении кеша.
CHANNEL_INVITE переносит предложение и join_key; принимающий создаёт свой attempt.
Регистрация ключа A↔C и подписанные записи участников не используют attempt_id.
Сетевой формат JOIN изменён без поддержки старых узлов, согласно devel-контракту.
4. chat_join_port — отдельная удерживаемая ссылка frontend на вход команд.
Submit копирует ссылку, резервирует весь posted_task до принятия команды и
публикует без дополнительного выделения памяти. Закрытие и публикация защищены
mutex. Закрытый порт не содержит instance/uasync и не оживает при restart.
Close отменяет ожидающие команды; уже извлечённый uasync batch исполнит только
освобождение. Arg встроен в выделение task, повторного освобождения нет.
Qt и instance_lite публикуют/удерживают порт под собственным mutex; GUI/JNI
не читают chat_sync из внешнего потока. Chat stop закрывает приём до workers.
Request интерфейса не равен сетевому attempt. Headless использует request=0,
сохраняя собственную корреляцию JSON-команд отдельно от GUI.
5. Завершение: отделение локальной операции, снятие таймеров, освобождение своего
handle, затем единое событие peer:8/result:4/channel:8/request:8 (native).
Обработчик события может сразу начать новую попытку. Ранний отказ core не даёт
события; мост завершает свою уже принятую команду этим отказом. Принятая core
попытка получает один результат, включая cancel/delete/stop.
Cancel не откатывает членство, которое уже сохранил другой узел, и не удаляет
принятый NCD snapshot или рабочие линки других владельцев.
6. JOIN_INFO_RESP: подписанное описание и member/Merkle записываются в одной SQL
транзакции. Проверяется привязка connection-member к транспортному peer и
неизменность уже известных канальных ключей. Ошибка откатывает SQL и не создаёт
CHAT-группу. После commit публикуются runtime-ресурсы и уведомления member_sync.
Предыдущая семантика отложенной проверки неизвестного родителя сохранена;
успешный apply не называется подтверждением всей цепочки приглашений.
Ensure_channel_ready возвращает ошибку и сохраняет необходимый порядок:
группа → подписка → db_sync. При отказе убираются только новые runtime-ресурсы.
Проверенные данные после commit сохраняются для повторной подготовки.
Ключи приглашения, пароль и исходная ссылка не печатаются в новых логах.
Тесты
- test_invite_codec: 116342 проверки. Roundtrip1..32 адресов IPv4/IPv6/mixed/REALITY,
пароль127, SNI63, максимальная ссылка1551 байт; точный буфер и буфер на байт меньше,
сохранность выходов, усечения, алфавит всех256 байтов, старые версии, конфликт
ключей блоков, неизвестные/отсутствующие поля и границы значений.
- test_chat_join_faults: настоящий controller/NCD/SQLite, перехват только доставки
пакетов и точек отказа. Каждая аллокация подготовки, включая REALITY, отказы
transport/info/ready deadlines, send и NCD; повтор после отказа и чужое владение.
Запоздавшие ERROR/READY, чужой group/короткий READY, повтор результата, BUSY,
вложенный UP во время PREPARE и новый JOIN из обработчика завершения.
SQL-trigger и неверная подпись member откатывают канал; отказы group/db_sync/
подписки после commit сохраняют проверенные данные и убирают runtime-ресурсы.
Узел обслуживает чужой JOIN во время своей попытки. Проверены queued/active
дубликаты request, producer другого потока, старый порт после restart,
stop между копированием аргументов и публикацией (condvar-барьер), stop внутри
уже извлечённого batch. После teardown число u_malloc-блоков возвращается к исходному.
- test_chat_join_e2e: настоящий обмен A!=C, A==C, неверный ключ.
- test_dm_invite: приглашение из DM и реальное вступление.
- test_services: независимый lifecycle сервисов.
- test_chat_sync_schedule: фикстуры приведены к наличию активной попытки/waiter;
подтверждается синхронизация пары после JOIN_READY без нового транспортного UP.
- Qt test_invite_links/test_local_invite_ui: ссылки и UI на общем кодеке.
Результаты окончательных прогонов и ограничения приводятся ниже.
Итоговые результаты
Linux check.sh: 196/196 PASS, 0 failed, 0 skipped; proxy9/9, burst1/1,
load8/8, 134217728 проверенных байт, 190 секунд.
Лог: /tmp/chat-join-complete-check.log.
Отдельный финальный test_chat_join_faults с condvar-гонкой и вложенным UP PASS;
лог: /tmp/chat-join-race-final.log. Тот же вариант включён в окончательный check.sh.
ASAN/UBSan/LSan: 6/6 PASS, detect_leaks=1, halt_on_error=1.
Кодек, отказы JOIN, настоящий E2E, DM invite, services и планировщик.
В изолированной копии /tmp/utun-chat-join-sanitizers согласованно инструментированы
lib/src/Opus. Правка флагов Opus выполнена только в копии: рабочая сборка не смешивается
с sanitizer-объектами. Сводка: /tmp/chat-join-sanitizer-results.log;
по каждому набору: /tmp/chat-join-sanitizer-test_*.log.
Desktop CMake build PASS: /tmp/chat-join-desktop-verified-build.log.
Qt invite/local_invite: 2/2 PASS, /tmp/chat-join-desktop-verified-tests.log.
Android assembleDebug --offline PASS (JNI, Kotlin и APK; без установки на устройства):
/tmp/chat-join-android-verified-build.log.
Дополнительный старый Linux CMake-каркас Android headless не собрался из-за отсутствия
include speex/speex_echo.h в его собственном списке путей; этот отдельный каркас
не заменяет успешную APK-сборку и native Linux-фикстуры основного набора.
Windows/FreeBSD native build/runtime не выполнялись. git diff --check PASS.
Промежуточные отказы проверок устранены: размер size_t и порядок _GNU_SOURCE в
instance_lite, старое объявление InviteDataC в JNI; затем фикстура планировщика
обновлена под обязательные waiter/активную попытку. Тесты runtime-отката выявили
зависимость db_sync от группы; требуемый порядок восстановлен, добавлена очистка
новой группы при отказе истории/подписки. Это покрыто отрицательными сценариями.
Исходные изменения аудита и итоговые правки не коммитились и не публиковались.
+79 -27
View File
@@ -1,17 +1,18 @@
# P2P аудио-звонок пользователю — архитектура
# P2P аудио- и видеозвонок пользователю — архитектура
Статус: реализовано. Звонок использует роутер, CM подготавливает оптимальный путь.
## 1. Область
Голос 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)** — модуль
не привязан к типу группы: он оперирует `group_id` + `peer_node_id` (для DM `group_id` —
DM-группа, маршрутизация через `etcp_router` идентична). Протокол расширяем под видео
(track-модель), реализуется только аудио. Работает desktop↔desktop и desktop↔android
Аудио и видео 1:1. Звонок можно инициировать **из CHAT-группы или из p2p-чата (DM)**.
Модуль оперирует `group_id` + `peer_node_id`. Для `dm:<conv_id>` функция `dm_call_route`
выбирает общую CHAT-группу с живым BGP-маршрутом к собеседнику; отдельная DM-группа не создаётся.
Аудио и видео используют отдельные каналы `etcp_router`; возможности видеокодеков
согласуются в активном звонке (см. [call_video.txt](call_video.txt)). Работает desktop↔desktop и desktop↔android
(обе стороны компилируют общий стек `etcp_router` / `topo_group` (BGP) / `conn_mgr`).
Полный цикл: проверка онлайна (BGP) → INVITE → звонок → ACCEPT/DECLINE →
аудиопоток через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова.
аудиопоток и опциональное видео через роутер → HANGUP. CM инициируется вызывающим при старте, вызываемым при принятии вызова.
### Входящие вызовы и телефонная занятость
@@ -42,22 +43,41 @@ Android обычного звонка показывает своё уведом
| Файл | Язык | Сборки | Назначение |
|---|---|---|---|
| `src/call/call_proto.h` | C | везде | wire-формат, subcmd, svc_id `ETCP_RT_ID_CALL 0x36` |
| `src/call/call_proto.h` | C | везде | wire-формат сигналинга (`0x36`) и аудио (`0x3F`), subcmd |
| `src/call/call.h` / `call.c` | C | везде (autotools + GUI) | сигналинг, реестр сессий, таймеры, watchdog (20с по приёму), релей медиа с backpressure, счётчики |
| `src/call/call_audio.h/c` | C | только GUI-сборки | Opus encode/decode, фрейминг, pull-обвязка с тонами; регистрация через `call_audio_ops` |
| `src/call/call_tones.h/c` | C | только GUI-сборки | генератор тонов (glitch/ended), синус |
| `src/call/call_jitter.h/cpp` | C++ | только GUI-сборки | SoundTouch джиттер-буфер, C API |
| `src/call/call_video.h/c`, `call_video_transport.h/c` | C | везде | формат видеотрека, согласование возможностей и передача кадров отдельными каналами |
Autotools-сборка демона остаётся C-only (`.cpp` не компилируется); сигналинг `call.c`
присутствует и в демоне, аудиопроцессинг — только там, где есть GUI.
Диагностика: категория `call` (`DEBUG_CATEGORY_CALL`); транспортная доставка — `etcp_route`.
Desktop направляет сообщения FFmpeg и Qt в общий `debug_file` (по умолчанию `chatgui.log`).
Начальная проверка codec пишет краткий итог на info, детали и ожидаемые отказы — на debug:
`[gui] debug_categories=call=debug,video=debug`. Повторы FFmpeg внутри пробы сворачиваются со счётчиком;
рабочие ошибки параллельного звонка сохраняют error/warn. Информационный stderr libva отключён по умолчанию;
для диагностики драйвера можно явно запустить с `LIBVA_MESSAGING_LEVEL=2`.
Отсутствующая desktop-запись приложения создаётся в XDG data applications для регистрации в portal.
## 3. Протокол (сигналинг svc_id 0x36, первый байт payload = subcmd)
Формат приёма: `entry->dgram[0]=svc_id`, служебный заголовок роутера до
## 3. Протокол
Каналы роутера независимы:
| svc_id | Константа | Назначение |
|---|---|---|
| `0x36` | `ETCP_RT_ID_CALL` | сигналинг звонка |
| `0x3D` | `ETCP_RT_ID_CALL_VIDEO_CONTROL` | согласование и восстановление видеотрека |
| `0x3E` | `ETCP_RT_ID_CALL_VIDEO` | видеокадры |
| `0x3F` | `ETCP_RT_ID_CALL_AUDIO` | аудиокадры `CALL_MEDIA` |
Формат приёма сигналинга и аудио: `entry->dgram[0]=svc_id`, служебный заголовок роутера до
`ROUTER_SVC_PAYLOAD_OFF=26`, затем subcmd и тело команды.
Сигналинг (`0x36`):
```
CALL_INVITE 0x01 {call_id:8, group_id:8, num_tracks:1, force:1, expires_us:8, tracks:[{type:AUDIO, codec:OPUS, sr:4, ch:1, bitrate:4}]}
CALL_RINGING 0x02 {call_id:8}
@@ -68,14 +88,27 @@ CALL_CANCEL 0x06 {call_id:8}
CALL_HANGUP 0x07 {call_id:8, reason:1}
CALL_END_ACK 0x08 {call_id:8}
CALL_PHONE_BUSY 0x09 {call_id:8}
```
Аудио (`0x3F`):
```
CALL_MEDIA 0x10 {call_id:8, track_id:1, seq:2, ts_ms:4, capture_pts_us:8, capture_epoch:8, codec_delay_samples:4, payload}
```
- `call_id` — случайный uint64, генерит вызывающий; идентифицирует сессию на обеих сторонах.
- `group_id` — CHAT-группа или DM-группа (общая маршрутизация).
- `group_id` — CHAT-группа; для DM-звонка выбирается общая группа участников с живым BGP-маршрутом.
- Сигналинг: `etcp_route_send(..., ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)`.
- Медиа: без подписи (уже внутри шифрованного канала etcp_router).
- `CALL_MEDIA` — общий контейнер; `track_id` оставляет место видео (`type=VIDEO`).
- Сигналинг, аудио и видео: обязательные `ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT` между конечными узлами.
- AES-128-CCM шифрует тело, Ed25519 подписывает заголовок роутера и шифртекст. Посредники не имеют ключа для расшифровки.
- Ключ получается из постоянных X25519-идентичностей собеседников; forward secrecy отсутствует.
- Маска защиты задаётся через `etcp_router_require_crypto`: открытые/частично защищённые данные отбрасываются
на конечном узле до изменения состояния приёма, очередей и `receive_ready`; отправка без требуемых флагов возвращает ошибку.
Пустые служебные пакеты роутера не являются медиаданными. Перехода на открытое медиа при ошибках нет.
- Каждый пакет получает случайный nonce(13) от OpenSSL CSPRNG; ошибка RNG прекращает шифрование.
Надбавка к сервисным данным: nonce(13) + CRC(4) + CCM tag(16) + Ed25519(64) = 97 байт.
- `CALL_MEDIA` переносит только аудио (`track_id=CALL_TRACK_AUDIO`); остальные значения приёмник отклоняет.
Видео имеет отдельный формат и транспорт: `call_video.h`, `call_video_transport.h`, [call_video.txt](call_video.txt).
- Аудио: Opus 48kHz mono, 20ms (960 сэмплов), ~24–32 kbps. ~50 кадров/с.
- Заголовок медиа — 36 байт с subcmd. `ts_ms` — время отправки для оценки сетевого джиттера;
`capture_pts_us` — время первого входного сэмпла в монотонной шкале отправителя, `INT64_MIN` при отсутствии метки.
@@ -361,27 +394,45 @@ CHAT_EVT_CALL_ERROR 38 [call_id:8][err:1][text:var]
CHAT_EVT_CALL_AUDIO_ROUTE 39 (Android: текущий маршрут/наличие гарнитуры)
```
## 10. Публичный API (call.h)
## 10. Основные функции публичного API
```
int call_start(ch_id, peer_node_id, out_call_id); // caller (post в uasync)
void call_accept(call_id); // callee
void call_decline(call_id);
void call_hangup(call_id);
int call_is_online(ch_id, peer_node_id); // BGP-проверка для UI
// аудио-поток (общий процессинг в C-ядре):
int call_encode_pcm(call_id, pcm, n, opus_out, cap); // single-writer: audio thread
int call_send_encoded(call_id, opus, len); // post → uasync → tx_q + flush
int call_pull_pcm(call_id, pcm_out, max); // single-writer: audio thread (ajb+tones)
void call_release(call_id); // post → uasync: освободить аудио-объекты
`call.h`: сигналинг и отправка готового Opus выполняются в uasync-потоке.
Из других потоков команды передаются через `uasync_post`/мост GUI и соответствующие `call_*_trampoline`.
`ch_id` — идентификатор CHAT-группы либо `dm:<conv_id>`; `out_call_id` — значение, заранее сгенерированное вызывающим.
```c
void call_start(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id);
void call_accept(struct UTUN_INSTANCE* inst, uint64_t call_id);
void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id);
void call_hangup(struct UTUN_INSTANCE* inst, uint64_t call_id);
int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id);
int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t* opus, int len,
const struct call_capture_time* timing);
```
`call_init / call_destroy` — вызываются из `chat_core_init / chat_core_destroy`.
`call_audio.h`: движок сам кодирует PCM и ставит отправку Opus в uasync.
Захват и воспроизведение используют по одному аудиопотоку на направление.
```c
int call_audio_feed_pcm(uint64_t call_id, const int16_t* pcm, int count, int64_t capture_pts_us);
int call_audio_feed_prepared_pcm(uint64_t call_id, const int16_t* pcm, const uint8_t* mute_mask, int64_t capture_pts_us);
int call_audio_pull_pcm(uint64_t call_id, int16_t* out, int max_samples);
void call_audio_release(uint64_t call_id);
```
`feed_pcm` принимает ровно 960 отсчётов; `feed_prepared_pcm` — 960 отсчётов после платформенного AEC
и маску mute. `call_audio_release` вызывается в GUI-потоке после остановки обоих направлений I/O.
Создание движка — `call_audio_start`, для подготовленного PCM — `call_audio_start_prepared`.
Видео настраивается через `call_video_configure`, кадры отправляются через `call_video_send_frame`
в uasync-потоке; полный контракт — в `call_video_transport.h`.
`call_init / call_destroy` вызываются из `chat_service_start / chat_service_stop` в `utun_instance.c`:
инициализация после `dm_core_init`, освобождение перед `dm_core_destroy`.
## 11. Платформенный I/O
- **Desktop:** `tools/chatgui/src/call_audio_engine.cpp` — miniaudio full-duplex
(capture → `call_encode_pcm`, `call_pull_pcm` → playback) + окно звонка (UI)
(capture через VoiceAudioIo → `call_audio_feed_prepared_pcm`, `call_audio_pull_pcm` → playback) + окно звонка (UI)
+ события в `gui_bridge_impl.cpp`.
- **Android:** JNI `nativeCall*` + Kotlin `AudioRecord`/`AudioTrack`.
**Переключение источника:** `AudioManager` (API31+ `setCommunicationDevice` +
@@ -398,7 +449,8 @@ void call_release(call_id); // post → uasync: о
mmx_optimized, sse_optimized) как C++. `call.c` попадёт через GLOB `src/*.c`.
- `tools/chatgui-android/libutun_lite/CMakeLists.txt` + `utun_sources.cmake`:
`LANGUAGES C CXX`, те же файлы.
- `src/transport_layer/etcp_api.h`: `#define ETCP_RT_ID_CALL 0x34`.
- `src/transport_layer/etcp_api.h`: `ETCP_RT_ID_CALL=0x36`, `ETCP_RT_ID_CALL_VIDEO_CONTROL=0x3D`,
`ETCP_RT_ID_CALL_VIDEO=0x3E`, `ETCP_RT_ID_CALL_AUDIO=0x3F`.
- `lib/debug_config.h`: `DEBUG_CATEGORY_CALL 31`, `DEBUG_CATEGORY_COUNT` → 32.
## 13. Порядок задач
+55
View File
@@ -0,0 +1,55 @@
Аудит config_parser — 26.10.2026
Полностью прочитаны config_parser.c/.h, config_parser_doc.md и общий config_ini.h.
Для согласования строгого разбора также повторно сверены config_updater.c/.h/_doc.md.
Разбор сохраняет модель и владение free_config; запуск ресурсов остаётся у instance.
Исправления
- IPv6 /32 больше не подменяется /128; префиксы проверяются до сужения к uint8_t.
Control allow=/0 задаёт нулевую маску без сдвига на 32; IPv6 control allow отклоняется.
- Ошибки известных обработчиков и выделения памяти отклоняют всю модель.
Все частичные списки освобождаются, включая client links и не завершённую секцию.
- Числа проверяются целиком до преобразования: знаки, хвост, диапазон и overflow.
Keepalive interval помещается в uint16_t; таймеры ограничены до умножения.
Bool унифицирован: 0/1, no/yes, false/true. Версия — ровно три десятичных октета.
- Адресные порты проверяются как decimal 0..65535, SOCKS — 1..65535.
Control ip/port даёт одинаковый результат в обоих порядках; ошибки getaddrinfo не теряются.
- Storage sizes проверяют UINT64_MAX, множитель M/G и полный хвост; -1 не становится безлимитом,
переполнение не становится нулевым лимитом. Слишком длинные строковые настройки отклоняются.
- Network ID ограничен 56 битами; требуются оба 32-byte HEX ключа.
Предоставленные ключи идентичности/пиров проверяются целиком; пустые необязательные остаются незаданными.
- CFG_CLIENT_LINK сохраняет имя local_srv. Повторный обычный addr сбрасывает режим dynamic IPv6.
- Флаг tcp_proxy_client enabled=no не включается снова следующей опцией той же секции.
Наличие service-секции задаёт её default enable и при отсутствии опций.
- Print_config не печатает private/signing keys; Networks выведены за пределы firewall-цикла.
Устаревший append ключей проверяет вход, запись и fclose; не объявляется атомарным updater.
- Updater выбирает итоговую идентичность/REALITY по последнему вхождению, но исправляет также
синтаксически плохие ранние дубли выбранными значениями. Из-за такого дубля ключи не генерируются заново.
Это нужно, поскольку строгий парсер проверяет каждое вхождение. Булевы REALITY enabled согласованы.
Контракты и ограничения
Обновлены заголовок и документ: порядок списков, необъединяемые named-секции, remote-only links,
синхронный DNS и первый getaddrinfo-адрес, Windows socket init, default-значения и границы,
повторные опции, поведение неизвестных секций/debug/gui и независимость parser/updater/start.
Bootstrap invite printer использует конфигурационные адреса и нулевые channel_id/join_key;
это не снимок фактических bind/NAT-адресов и не invite конкретного канала.
Нативные сборки/runtime Windows и FreeBSD в этом этапе не выполнялись.
Проверки
- Полный check.sh: 195/195 PASS, 0 failed/0 skipped, proxy9/9, burst1/1, load8/8,
134217728 байт, 169 секунд; /tmp/config-instance-audit-first-check.log.
- Профиль 6/6 PASS: config_parser, config_updater, config_debug, reality_config, ncd_config, services.
- Test_config_parser: 53 ошибочных конфигурации отвергнуты одинаково из файла/буфера;
10 отказов аллокаций с полным rollback, IPv4/IPv6 CIDR и control order, предельные размеры/порты,
скрытие секретов и /dev/full при append; баланс памяти сохранён.
- ASAN/UBSan/LSan: те же 6/6 PASS с detect_leaks=1/ halt_on_error=1.
Все встроенные lib/src/Opus инструментированы согласованно в отдельной копии
/tmp/utun-config-instance-asan-20261026; рабочие объекты не смешивались с sanitizer-объектами.
Первый updater sanitizer-прогон обнаружил обход test mock через glibc __fread_chk;
checked wrapper добавлен, повторный прогон всех файловых и allocation faults PASS.
- Логи: /tmp/config-parser-audit-contract.log, /tmp/config-parser-audit-updater.log,
/tmp/config-parser-audit-debug.log, /tmp/config-parser-audit-reality.log,
/tmp/config-parser-audit-ncd.log, /tmp/config-parser-audit-services.log.
Санитайзеры: соответствующие /tmp/config-parser-audit-asan-*.log,
сборка /tmp/config-parser-audit-asan-build.log и /tmp/config-parser-audit-asan-tests-build.log.
Общая сборка: /tmp/config-instance-audit-full-build.log.
+8 -2
View File
@@ -19,12 +19,17 @@
## Идентичность и шифрование
```text
conv_id = SHA256("utun_dm_v1" || min(node_a,node_b) || max(node_a,node_b))[..63]
conv_hash = SHA256("utun_dm_v" || min(node_a,node_b) || max(node_a,node_b))
conv_id = native_u64(conv_hash[0:8]) & 0x7FFFFFFFFFFFFFFF
content_key = SHA256(X25519(my_private, target_public) || "utun_dm_content")
msg_nonce = SHA256(conv_id || author || seq)[..13]
media_nonce = SHA256("utun_dm_media" || author || media_id || part)[..13]
```
Префикс conv_hash — 9 ASCII-байт без NUL. ID узлов подаются в хеш в порядке
байтов хоста; native_u64 читает первые 8 байт хеша в том же порядке.
Нулевой conv_id заменяется на 1.
Сообщения и медиа используют один content_key target, без ключей отдельных файлов.
AES-256-CCM: nonce 13 байт, tag 16 байт. Максимальная порция plaintext — 65535 байт;
порция медиа — 32768 байт. UUID медиа новый для каждой подготовки. Повтор доставки
@@ -62,7 +67,8 @@ tags, точный размер и хеш; владелец публикует
- `dm_core`: беседы, история, outbox и подтверждение получателя.
- `dm_mailbox`: промежуточные сообщения суперузла и квитанции.
- `dm_crypto`: вывод ключей, nonce, CCM и файловые примитивы без БД/сети.
- `dm_crypto`: вывод conv_id, content_key и nonce сообщений без БД/сети.
- `media_crypto`: общие CCM и потоковые файловые примитивы для DM и групповых вложений.
- `dm_media`: файлы endpoint, загрузки и отдельная custody M.
- `dm_mailbox_media`: durable задания S, выбор M и повтор удаления до DELETE_ACK.
- `file_transfer`: подписанная передача байтов, backpressure и отдельный CM handle каждой операции.
+43 -7
View File
@@ -9,8 +9,9 @@
Android показывает зашифрованное вложение отдельной карточкой со статусом запроса
ключа. После одобрения карточка обновляется на исходный файл, изображение, видео
или голосовое. В интерфейс передаётся только расшифрованный путь; контейнер `.uenc`
нельзя открыть или сохранить как готовое вложение. Цитаты и список чатов показывают
название «Зашифрованное вложение» вместо manifest.
нельзя открыть или сохранить как готовое вложение. До расшифровки цитата показывает
название «Зашифрованное вложение», после — обычный preview исходного вложения.
Список чатов сохраняет общее название зашифрованного вложения вместо manifest.
Автор генерирует независимые случайные crypto_UUID и ключ AES-256 на каждую
подготовку. Ключ записывается в локальную SQLite до шифрования. Готовый контейнер
@@ -38,6 +39,13 @@ encrypted_file_body:variable
```
Метаданные содержат MIME в 32 байтах и сериализованный `attachment_info`.
MIME — непустая NUL-строка длиной1..31 байт: ASCII33..126 без двойной кавычки
и обратной косой черты. Общий `encrypted_file_validate_type` применяется в send,
write и read; это проверка представления поля, без полного разбора MIME-грамматики.
Send отклоняет неверное значение до создания задачи/ключа/worker, write — до файловых
операций. Read проверяет расшифрованное аутентифицированное поле и при ошибке сохраняет
прежние info/type. Сырые неверные строки в диагностику не выводятся.
Каждая часть шифруется AES-256-CCM с тегом 16 байт и nonce 13 байт.
Nonce метаданных использует `part=UINT64_MAX`; файл — порции 32768 байт
с номерами от нуля. Общие примитивы находятся в `media_async/media_crypto`;
@@ -45,8 +53,26 @@ Nonce метаданных использует `part=UINT64_MAX`; файл —
Получатель проверяет подписанный SHA256 полного контейнера, UUID, tag метаданных,
все tags файла, точный размер и hash тела. Результат пишется во временный файл
и публикуется после успешной проверки и durable rename. Все длительные файловые
операции и криптография выполняются в media_async.
и публикуется после успешной проверки и durable rename. Подготовка файлов
и криптография выполняются в media_async worker.
`encrypted_file_write/read` возвращают закрытый проверенный `file_io_temp`,
не изменяя конечное назначение. Задача владеет им до publish/discard; запись идёт
в эксклюзивно созданный соседний файл с правами 0600 на POSIX. Одинаковые файлы,
включая `/./`, hardlink и конечную ссылку назначения, отклоняются до подготовки;
публикация повторно проверяет идентичность. При ошибке read не выдаёт новые метаданные.
Режим `FILE_IO_DURABLE` действует весь срок temp: flush/sync открытого файла/close,
затем rename и сохранение каталога. После успешного rename ошибка directory sync
не откатывает установленный результат; temp уже отсоединён от владельца.
Completion не удаляет файлы по заранее вычисленному имени: очистка учитывает,
какие артефакты задача действительно создала или опубликовала.
Реестр `file_io` защищает активные temp от очистки кеша, включая время после close.
Восстановление до запуска workers и очистка после их завершения удаляют оставшиеся
`<UUID>.uenc.tmp.<UUID>` / `<UUID>.file.tmp.<UUID>` в `media/decrypted`.
Конечные файлы и имена других форматов этот проход сохраняет; активные temp другого
instance того же процесса остаются защищены общим реестром.
## Хранение и разрешения
@@ -69,14 +95,15 @@ Nonce метаданных использует `part=UINT64_MAX`; файл —
создаёт; ручная пользовательская загрузка на таком узле запрашивает доступ.
Сетевая классификация `peers.storage` для этого не используется.
В шапке канала кнопка «Запросы расшифровки» показывает число запросов без решения.
В шапке desktop-клиента кнопка «Запросы расшифровки» видна при наличии запросов без решения
и показывает их число.
Список содержит участника, вложение, дату и отдельные кнопки выдачи/отказа.
Двойной клик переводит к исходному сообщению. После выдачи видны ожидание доставки
и подтверждённая доставка ключа; отказ также сохраняется.
## Адресный протокол
Router-сервис `ETCP_RT_ID_ATTACHMENT_KEYS=0x40`. Каждый пакет обязан иметь
Router-сервис `ETCP_RT_ID_ATTACHMENT_KEYS=0x42`. Каждый пакет обязан иметь
`ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED`; промежуточные узлы ключа не видят.
Протокол не создаёт личную беседу и не публикует служебные сообщения в канал.
@@ -110,9 +137,18 @@ backpressure и повтором через 5 секунд; restart продол
`test_encrypted_file`: границы порций, пустой файл, metadata roundtrip,
неверные ключ/автор/UUID/hash, подмена метаданных с пересчитанным внешним hash,
лишние байты и отсутствие частичного plaintext после отказа.
лишние байты, alias обоих API, сохранность существующего назначения/метаданных,
23 сценария отказов I/O с контролем fd/temp, включая ошибку после rename.
Для MIME проверяются все256 значений первого байта, NULL и границы поля,
восемь ранних отказов writer с сохранением файлов/output, три допустимых roundtrip
и семь аутентифицированных некорректных wire-полей с сохранением info/type/target.
`test_media_primitives` проверяет оба режима temp и границу durable-публикации;
`test_attachment_send` проверяет восемь отказов MIME до ключей/worker/файлов,
затем останавливает сервис после настоящего crypto rename до completion.
`test_encrypted_attachment`: реальные A–S–B/C, скачивание с S при выключенном A,
перезапуски автора с запросами и получателя с ключом, выдача B и отказ C,
отсутствие ключа на S/C, отрицательные control-пакеты, повтор GRANT,
восстановление удалённого plaintext без нового согласия и удаление канала.
При startup/restart дополнительно проверяются оба дерева: stale temp удаляются,
открытый и закрытый активные temp, конечные файлы и чужие имена сохраняются.
+5 -1
View File
@@ -1,4 +1,8 @@
Архитектура etcp:
> Исторический документ: описания NAT, модулей и ограничения обмена маршрутами ниже не отражают текущую реализацию.
> Актуальные правила группового обмена маршрутами — в [topo_group.h](../src/routing_layer/topo_group.h),
> модель узла — в [node_snapshot.md](node_snapshot.md), протокол роутера — в [etcp_router_doc.md](../src/routing_layer/etcp_router_doc.md).
Архитектура etcp:
ETCP устанавливает только одно подключение между двумя узлами. Это подключение может добавлять-убирать каналы связи (динамически) без разрыва соединения.
Также каналы связи мониторятся и трафик распределяется между доступными каналами. Если канал определяется как недоступен - он исключается из обмена.
File diff suppressed because it is too large. Load diff
+289 -101
View File
@@ -1,81 +1,193 @@
Формат кодограммы:
------------------
пакеты идут по следующему пути:
пакет кем-то помещается в входную очередь (ETCP input_queue).
Когда load balancer готов отправить следующий пакет он делает вызов etcp_request_pkt в etcp.c/h и etcp либо формирует следующую кодограмму или встаёт в состояние async ожидания (возвращая null):
- либо wait ETCP input_queue (если нечего передавать)
- либо wait SACK + wait timeout (если sack очередь полная и нет необслуженных запросов ретрансмиссий). после истечения wait timeout он помечает используя round-robin очередной пакет как "нужна ретрансмиссия".
из async ожидания он может вызвать etcp_loadbalancer input_from_etcp и передать пакет на отправку.
как формируется пакет:
1. выбираем канал передачи (etcp_loadbalancer_select_link)
2. если есть место в rwin (объём inflight данных) то берем очередной пакет из ETCP input_queue и его отправляем
3. если пакет найден или есть неподтверждённые ACK:
1. вызываем функцию формирования опциональных секций (ACK, channel timestamp) - они записываются в начало
2. в конец добавляем секцию 0x00 с данными пакета
при отправке пакета помечаем в inflight списке с какого интерфейса он отправлен
Функция прикрепления опциональных секций:
- запрашивает выбор канала передачи.
- добавляет накопившиеся ACK
Кодограмма сервиса через etcp_send попадает в normalizer; его фрагменты поступают в
input_queue, затем в indexed input_send_q с назначенным seq.
Очереди, ACK/RTO, изменение окна и готовность линка вызывают etcp_conn_wake_sender.
Один call_soon обслуживает накопленные события без рекурсивного входа и задержки таймером.
При OOM выделения сетевого пакета очередь сохраняется; retry назначается через 10ms.
close/reset отменяет ожидающий запуск. Не подтверждённая сессия не передаёт DATA/ACK;
HELLO/CHALLENGE/CONFIRM и keepalive передаются своим независимым путём.
Формирование пакета:
1. Просмотреть input_send_q без извлечения: первый готовый повтор, иначе первый готовый новый DATA.
Заблокированная запись пропускается; порядок остальных записей не меняется. Поиск не резервирует окно,
не выдаёт номер попытки и не меняет историю/RR. Если кандидата нет, можно подгрузить один новый фрагмент
и повторить просмотр. Общий бюджет учитывает DATA-байты send_q+wait_ack; приёмное окно по till сохраняется.
После callback подгрузки проверяются reset_count/close/сессия, включая удаление прежнего burst-линка.
2. Выбрать инициализированный UP-линк с готовым shaper и местом для целого DATA:
inflight_bytes + длина DATA <= inflight_lim_bytes. Проверка выполняется без переполнения.
Среди допустимых выбирается минимум inflight_bytes, при равенстве — round-robin.
Для повтора обход начинается после last_link самого фрагмента: сначала неиспользованные допустимые пути.
История — битовая карта256 локальных слотов,32 байта на логический пакет; wire-формат не меняется.
Пока остался неиспользованный путь, занятый по cwnd/pacing, этот пакет ждёт, а send_q просматривается дальше.
Когда все допустимые пути использованы, разрешён следующий обход. Очистка истории и новый бит фиксируются
только после принятия send; отказ send не расходует обход. Перед удалением линка очищаются его курсор/бит
во всех логических записях до освобождения слота и callbacks, которые могут повторно выдать тот же local_link_id.
При наличии незастрявшего initialized+UP пути застрявшие исключены из повторов, даже если альтернатива занята.
Если застряли все, доступны все UP-пути. Прежний путь разрешён лишь при отсутствии других допустимых;
иначе ждём альтернативу. DOWN последней альтернативы пробуждает отправителя.
3. Наступивший по сроку или срочный ACK обслуживает тот же линк, который принял DATA,
без ограничения DATA-cwnd/pacing. Готовый DATA другого линка уступает этому ACK;
DATA того же линка забирает ACK piggyback. Если DATA не помещается, он остаётся в очереди.
Кумулятивный till можно отправить по любому рабочему пути. ACK-only не увеличивает нагрузку DATA-shaper;
физическая очередь канала остаётся общей, её накопление ограничивают DATA-cwnd/pacing BBR.
4. Выделить ETCP_DGRAM до удаления DATA из очереди и изменения счётчиков.
Выделить физическую попытку с новым attempt_no для этого seq и зарезервировать окно выбранного линка.
Только успешное принятие транспортом фиксирует время, last_link, историю/RR и перенос DATA в input_wait_ack.
Ошибка send снимает резерв; логический DATA остаётся готовым без сетевого loss sample.
Пакет содержит ACK, опциональные timestamp/измерительные секции и последнюю секцию PAYLOAD.
5. В активной ограниченной burst-серии pacing обходится; DATA всё равно должен
помещаться в окно, иначе серия отправляет FILLER. ACK/FILLER окно DATA не занимают.
Если готовый повтор относится к другому пути, серия идёт FILLER: новые DATA его не обгоняют.
При приёме пакета (от etcp_connections):
- последовательно сканируем секции и отдаём их на обработку нужным обработчикам:
- ack (0x01): помечаем в inflight пакеты как подтверждённые и проставляем время подтверждения. также снимаем блокировку отправки
- timestamp (0x06): обновляем last RTT, пересчитываем RTT avg10 и jitter по каждому линку; обновляем recv_dt_avg для раздельного измерения времени передачи/приёма
- [0x00] payload: добавляем пакет в нужное место сборочного linked-list (сверяем по ID если дубликат - игнорируем). также добавляем ACK запись в ack_q для отправки.
- ack (0x01): пара seq+attempt_no подтверждает физическую попытку только принявшего ACK линка;
seq отдельно подтверждает логическую доставку. till освобождает логические записи, без BBR/снятия чужого flight.
- timestamp (0x06): обновляем RTT и EWMA |ΔRTT| пути; recv_dt_avg хранит поправки времени TX/RX в Q16.
У conn rtt_last — среднее RTT линков, rtt_avg_10 — их максимум (имя историческое).
BBR использует отдельные DATA RTT/jitter по ACK физических попыток; RTO также учитывает rtt_last своего линка.
- [0x00] payload: сохраняем фрагмент в recv_q либо распознаём уже сохранённый/доставленный seq.
Только после этого ставим ACK с seq+attempt_no в ack_q принявшего линка; OOM до сохранения не подтверждается.
после сканирования проверяем появились ли в linked-list собранные данные которые можно переместить в выходную очередь. и перемещаем если есть.
пересканирование inflight:
- запускается при добавлении пакета в очередь ожидания ACK (wait_ack_cb). устанавливается таймер на первый неистекший пакет.
- если время последней отправки > RTT_avg10*K1+jitter*K2 (коэффициенты K1=32, K2=32, деление на 16) то пакет заново отправляется
- очередь FIFO (сортирована по timestamp добавления = времени отправки), сканирование прекращается на первом неистекшем пакете
- по истечению таймера (ack_timeout_cb) снова вызывается проверка очереди
Таймер попыток каждого линка (etcp_tx.c):
- RTO = clamp(ceil(1.2*RTT_for_RTO + 1.5*data_jitter_us + 3000), 10000, 1000000) мкс,
RTT_for_RTO=max(data_rtt_us,100*rtt_last): DATA EWMA и последний timestamp/keepalive RTT своего линка.
Целочисленный расчёт использует uint64 и округляет вверх до целой микросекунды.
RTT использует собственный DATA ACK минус receiver_delay; EWMA RTT=1/8, jitter=1/4 |RTT-EWMA|.
Первый DATA RTT задаёт jitter=RTT/2. Timestamp-RTT задаёт нижнюю границу срока, чтобы после роста задержки
попытки доживали до ACK и DATA-измерения продолжались; BBR RTT/min_rtt по этому сигналу не обновляются.
Без обоих измерений RTO=1с.
- FIFO содержит физические попытки в порядке принятия транспортом. Таймер назначается по старейшей;
при изменении RTO проверяется новый срок. Timestamp handler пересчитывает таймер сразу после обновления RTT,
включая сокращение без новых DATA/ACK. Допускается оставить уже запланированную более раннюю проверку.
uasync округляет задержку вверх до миллисекунды; callback может выполниться позже расчётного срока.
- Истечение снимает только собственный физический flight и сообщает loss собственному BBR.
Если логический seq ещё не доставлен и других активных копий нет, он возвращается в input_send_q.
Уже доставленный seq не возвращается, даже если его физическая копия истекла позже.
Обычный ACK-only ждёт received_at_us старейшей записи своего пути + 2000мкс.
Piggyback на готовом DATA передаёт ACK сразу. Готовый ACK может включать более свежие записи:
срок проверяется по голове пакета, а не отдельно для каждой записи внутри него.
Среди готовых обычных ACK выбирается самый старый срок; срочный ответ на ACK_REQUEST имеет приоритет.
Короткая ограниченная измерительная burst-серия завершается до перехода к ACK другого пути.
Продвижение логической границы также задаёт срок 2мс от первого неподтверждённого продвижения;
следующие DATA его не отодвигают. Любой принятый пакет с актуальным till снимает этот запрос.
Один ack_resp_timer соединения обслуживает минимум сроков очередей рабочих путей и логической границы.
После piggyback/частичной отправки срок пересчитывается по оставшимся головам. Новый RX между
timer и отложенным sender не задерживает старые ACK: готовность проверяется по абсолютному времени.
При DOWN очередь пути не создаёт таймер; после UP её старый наступивший срок действует сразу.
OOM ACK-таймера логируется и включает существующий срочный ответ, сохраняя received_at_us для RTT.
Reset/close отменяют таймер и очищают сроки; DATA/RTO/BBR и wire-формат этим механизмом не меняются.
inflight - две очереди (ll_queue):
- input_send_q: список пакетов ожидающих отправку
- input_wait_ack: список неподтвержденных пакетов ожидающих ack
При отправке пакет перемещается из input_send_q в input_wait_ack.
При ретрансмиссии пакет перемещается из input_wait_ack обратно в input_send_q.
При принятии отправки транспортом пакет перемещается из input_send_q в input_wait_ack.
При необходимости повтора пакет перемещается из input_wait_ack обратно в input_send_q.
доп. свойства пакетов (struct INFLIGHT_PACKET):
- указатель на ETC_LINK последней отправки (last_link)
- timestamp последней отправки (last_timestamp)
- число отправок (send_count)
- send_hist[8] - история через какие линки отправлялся (индекс линка)
- inflight_bytes/inflight_packets per-link отслеживается в ETCP_LINK
Размер буфера inflight лимитируем как сумму по всем активным линкам optimal_inflight.
- send_hist[8] — local_link_id последних попыток, только диагностика
- last_attempt_id — внутренний составной ключ (seq<<16)|attempt_no последней принятой отправки
- attempt_count — выданные номера попыток данного seq, общие для всех каналов, включая неудачные send
- active_attempts — число ещё учитываемых физических копий
- Payload принадлежит только логической записи. Попытка ETCP_TX_ATTEMPT хранит seq и неизменные снимки, без payload.
inflight_bytes/inflight_packets линка равны сумме DATA-длин/числу записей в его attempts, включая резерв send.
ACK/till может уже удалить логический объект, пока другая физическая копия продолжает занимать своё окно.
Повторный ACK неизвестной/истёкшей попытки не вычитает flight и не создаёт BBR sample.
ACK известного ID с другим seq отклоняется целиком; неизвестный ID допускает только логическое подтверждение seq.
Удаление линка обнуляет last_link в обеих очередях до освобождения его памяти.
Общий optimal_inflight равен сумме лимитов всех линков, с насыщением UINT32_MAX;
это дополнительный backpressure подачи новых фрагментов, не замена индивидуальных окон.
Отдельно ограничиваем размах sequence относительно кумулятивного ACK: новый seq выдаётся
только если (uint32_t)(next_tx_id - rx_ack_till) <= MAX_INFLIGHT_SIZE (16384).
Selective ACK освобождает отдельную inflight-запись, но не продвигает эту границу:
приёмник пока не может освободить пакеты за дыркой. Кумулятивный ACK возобновляет
input_queue даже если подтверждённые записи уже удалены по selective ACK.
При заполненном sequence-окне собственный таймер запрашивает актуальный till секцией ACK_REQUEST.
Получатель отвечает через запрашивающий путь, даже без ожидающих DATA/ACK-элементов.
При продвижении frontier, reset/close таймер отменяется. Это восстанавливает поток при потере последнего till.
Память физических попыток ограничена: 16384 на линк, 32768 на соединение; исчерпание блокирует DATA до освобождения.
Очередь ACK каждого линка ограничена 16384 записями; переполнение логируется, DATA остаётся сохранённым для повтора/till.
ACK ограничивается последним назначенным seq (next_tx_id - 1); сравнения учитывают uint32 wrap.
optimal_inflight расчитывается из RTT и bandwidth линка.
bandwidth по каждому линку адаптивно подстраиваем:
Периодически в stats_timer_cb собираем статистику окна (rtt_avg10, ретрансмиссии, переданные пакеты).
BW рассчитывается как: inflight_lim_bytes * 8 / rtt_min (в секундах) * 8 / 1024 * 1.3 (Kbits/sec).
Размер inflight_lim_bytes плавно подстраивается вверх/вниз в зависимости от того, растёт ли RTT.
Burst-измерения bandwidth (0x07, 0x08) зарезервированы, но на данный момент не реализованы.
BBR каждого линка получает ACK/loss samples и возвращает cwnd и pacing rate.
Скорость — доставленные байты за max(интервал отправки, интервал доставки), по снимкам при отправке.
Интервал между соседними ACK не используется: ACK одной пачки могут обрабатываться в один tick.
ACK с подтверждениями также возвращает точку приёма своего пути: накопленные DATA-байты и монотонное время
последнего увеличения счётчика. В счётчик входят допустимые сохранённые/уже доставленные DATA, включая копии;
OOM до сохранения и отвергнутые DATA его не меняют. Считаются байты полезной нагрузки секции, без заголовков.
При отправке попытка запоминает известную точку. ACK действующей попытки продвигает точку пути;
повторный/чужой ID её не меняет, переставленные ACK не откатывают. Противоречивая пара логируется и игнорируется.
При положительных дельтах interval=max(send_us,ceil(delivered*delta_rx_us/delta_rx_bytes)).
Приёмный интервал заменяет ACK clock: обратная очередь больше не завышает BW сжатием ACK
и не занижает его растяжением. Send-интервал сохраняет защиту от сжатия приёмного времени.
Без достоверной приёмной базы/положительных дельт используется max(send_us,ack_us).
Счётчики и времена — uint64, часы двух сторон не сравниваются; нужны только дельты одних часов.
Если интервал не представим в uint32, sample не используется для BW вместо насыщения, завышающего скорость.
После пустого физического flight база начинается заново, чтобы простой не занижал скорость следующей передачи.
Reset потока обнуляет точки приёма; исчерпание локального 64-битного счётчика закрывает соединение с ERROR.
RTT/min_rtt, формула RTO, политика окна и дедлайн ACK не корректируются приёмной точкой.
Внутренняя полоса — байты/мкс Q16; pacing — байты/с, BDP — байты, без повторного множителя MSS.
Минимальное окно BBR — четыре DATA-фрагмента normalizer; максимальное — bbr_max_cwnd (по умолчанию 1048576).
8192 — нижняя допустимая граница настройки bbr_max_cwnd; транспорт не повышает до неё текущее окно.
PROBE_RTT осушает flight до max(4*MSS, 0.5*BDP), выдерживает >=200мс и раунд ACK, затем восстанавливает окно.
RTT-фильтры обновляются при обработке ACK. Короткий минимум принимает меньший достоверный RTT
либо текущее измерение после истечения 5с; RTT=0 означает отсутствие измерения.
Долгий min_rtt принимает короткий минимум, если он не больше прежнего, либо истекли его 10с.
Вместе со значением копируется метка короткого фильтра: повторное использование кеша не задаёт текущее время.
Завершение PROBE_RTT возобновляет короткое окно. Окна истекают строго после своей границы;
hold завершается при достижении срока и завершённом ACK-раунде. Метки RTT и срок hold — полные uint64 TB (100мкс).
MSS — размер DATA-фрагмента normalizer без заголовков; начальное окно UDP и STCP равно 4*MSS с верхним лимитом конфига.
В PROBE_BW действует дополнительный целевой бюджет очереди 20мс: всё окно, включая резервы ACK и PROBE_UP,
ограничивается max(4*MSS, BDP + BW*20мс) по min_rtt. STARTUP/DRAIN не ограничивают окно этой формулой,
но целый раунд RTT выше min_rtt+бюджет завершает STARTUP и переводит его в DRAIN.
Shaper списывает только DATA-байты: единицы совпадают с delivery sample и pacing BBR.
Это цель по оценкам BBR; минимум окна, изменение полосы и очереди обратного пути могут давать большую задержку.
Отказ из-за нехватки окна сохраняет cwnd_limited до следующего ACK: вычитание ACK
не должно скрывать этот факт от BBR. app_limited учитывает ожидающие очереди и
pending/частичный буфер normalizer; ограничение окном не означает отсутствие данных.
Потеря для BBR учитывается при RTO, фактическая повторная передача — при новой попытке.
Три RTO физических DATA-попыток без ACK-прогресса ставят локальный флаг tx_stalled, в том числе в одном вызове таймера.
До установки флага действующий ACK своего пути сбрасывает серию RTO. После установки сброс возможен только по ACK
попытки, принятой транспортом после момента установки флага; старый/чужой/уже снятый attempt не доказывает восстановление.
tx_stalled ограничивает выбор повторов при наличии альтернативы; UP, новые DATA, ACK/keepalive и формула RTO сохраняются.
Reset линка снимает флаг и серию RTO. Новых очередей, копий payload и таймеров детектор не создаёт.
При уменьшении окна уже отправленные пакеты остаются в учёте; новые DATA ждут ACK/RTO.
Измерительные секции burst 0x07/0x08 и FILLER поддерживаются реализацией.
**** Формат кодограмм для etcp.c/h ****
Каждая кодограмма состоит из обязательного заголовка и опциональных секций:
1. Обязательный заголовок всего пакета (3 байта) добавляется при передаче, есть во всех пакетах:
[Timestamp high][Timestamp low][flags/flag_up]
- Timestamp: uint16_t, время отправки в единицах 0.1 мс (циклическое). при переповторах время обновляется
[Timestamp:2][flags/flag_up:1]
- Timestamp: uint16_t, время отправки в единицах 0.1 мс (циклическое), при повторе обновляется.
Копируется из памяти в порядке байтов хоста; на используемых little-endian платформах передаётся LE.
- flag_up: uint8_t, bit0 = up/down (recv_keepalive) — признак живой приёмной стороны
шифруется строго ВСЁ включая ВСЕ заголовки (кроме обфусцированного publickey в INIT-пакетах).
timestamp вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ (скрыто в шифрованном заголовке).
Итого шифруется: 3 байта заголовка + (data_len - noencrypt_len) байт данных.
2. Опциональные секции (одна или несколько) добавляются в etcp.c (внутри data[], без доп. заголовка):
Для DATA/ACK после этих 3 байт добавляется обязательный заголовок сессии:
[0x09][sender_epoch(8 BE)][receiver_epoch(8 BE)], затем идут секции из etcp.c.
Итого шифруется: 3 + 17 + data_len байт для DATA/ACK;
для остальных кодограмм — 3 + (data_len - noencrypt_len) байт.
Формат и подтверждение эпох описаны в разделе «Подтверждение транспортной сессии» ниже.
2. Опциональные секции (одна или несколько) добавляются в etcp.c внутри DATA/ACK после заголовка сессии:
а) Подтверждения (ACK) - заголовок 0x01:
[0x01][count][last_delivered_id(4 байта LE)][rx_dup_count(2 байта LE)][[seq(4 LE)][recv_ts(2 LE)][dly(2 LE)] × count]
- count: количество ACK записей (1 байт; до 32 ограничено размером пакета)
[0x01][count][last_delivered_id(4 байта LE)][rx_dup_count(2 байта LE)][[seq(4 LE)][attempt_no(2 LE)][delay_us(4 LE)] × count]
- count: количество ACK записей (1 байт; до 255, дополнительно ограничено MTU и оставшимся местом)
- last_delivered_id: uint32_t — последний ID, доставленный получателю (по этот ID всё собрано подряд)
- rx_dup_count: uint16_t — счётчик принятых дубликатов (локальный, для вычисления tx_dup_count на удалённой стороне)
- seq: uint32_t — номер подтверждаемого пакета
- recv_ts: uint16_t — локальный timestamp приёма пакета (в единицах 0.1 мс)
- dly: uint16_t — задержка между приёмом пакета и отправкой ACK (для расчёта RTT)
Каждая запись = 8 байт.
- attempt_no: uint16_t — номер физической попытки данного seq, 1..65535; возвращается только тем же путём
- delay_us: uint32_t — задержка между приёмом DATA и формированием ACK, мкс; UINT32_MAX исключает RTT sample
При count>0 после всех записей обязательно добавляется [rx_data_bytes(8 LE)][rx_data_time_us(8 LE)].
Это точка приёма всего пути на момент формирования ACK, а не отдельной подтверждаемой попытки.
Время — монотонные микросекунды получателя при последнем положительном приросте DATA-байтов; 0/0 — точки нет.
Каждая запись = 10 байт, база ACK = 8 байт; полный размер = 8+10*count+(count>0 ? 16 : 0).
count=0 допустим для передачи till, точки приёма не содержит. До обработки проверяется вся длина ACK.
ACK_REQUEST: [0x0a], одна секция длиной 1 байт, запрос повторной отправки актуального till.
в) channel timestamp (добавляется после выбора канала передачи, если есть свежие данные):
[0x06] [RET_Timestamp(2 байта LE)] [RECV_Rel_Timestamp(2 байта LE)]
- RET_Timestamp: uint16_t = last_recv_timestamp + (now - last_recv_local_time)
@@ -83,12 +195,20 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
- RECV_Rel_Timestamp: uint16_t = last_recv_local_time - last_recv_timestamp
(относительное смещение между локальным и удалённым временем)
Всего 5 байт. Добавляется только если last_recv_updated=1 (был принят пакет с момента последней отправки)
и если прошло не более ~100ms (dt < 1000000 таймбаз).
и если прошло менее 100с (dt < 1000000 таймбаз). Результат rtt_last задаёт нижнюю границу RTT для RTO;
DATA RTT и min_rtt BBR обновляются только ACK действующих физических попыток.
3. Полезная нагрузка - заголовок 0x00 (одна секция и всегда последняя):
[0x00] [ID 4 байта LE] [данные...]
- ID: uint32_t, 32-битный циклический номер пакета
[0x00] [ID 4 байта LE] [attempt_no 2 байта LE] [данные...]
- ID: uint32_t, номер фрагмента 1..UINT32_MAX в пределах одной эпохи потока.
- attempt_no: uint16_t, номер 1..65535, общий для всех каналов одного ID.
Номер выдаётся при создании попытки и не возвращается при отказе send; исходный seq при повторе сохраняется.
После номера65535 следующая попытка аварийно меняет эпоху через REINIT; счётчик не оборачивается.
Перед выдачей seq=0 после UINT32_MAX поток также меняет эпоху: пара seq/attempt_no не повторяется внутри эпохи.
Смена эпохи отбрасывает прежний поток, владельцы восстанавливают свои операции по REINIT, как при фатальном OOM.
Старые DATA/ACK отклоняются заголовком сессии до разбора. Заголовок PAYLOAD занимает7 байт.
Внутренние хеш-очереди сохраняют64-битный ключ (seq<<16)|attempt_no; он не передаётся по сети целиком.
Пакет может содержать несколько опциональных секций (например, ACK + channel_timestamp + данные),
всегда в порядке: ACK, channel_timestamp, payload; payload всегда последний.
Порядок: ACK, возможный ACK_REQUEST, измерительные/временные секции и FILLER, затем payload последним.
Разбор секций — последовательное сканирование с известными размерами (нет явного поля длины секции).
цель протокола:
@@ -99,71 +219,100 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
При прохождении трафика через этот модуль подсчитываются метрики (для каждого канала отдельно):
- RTT последнего пакета и время отправки последнего пакета
- общее количество отправленных/принятых байт
- количество перезапросов (по каждому каналу отдельно) в пакетах. когда etcp модуль принял запрос ретрансмиссии он смотрит с какого интерфейса был отправлен этот пакет (это запоминается при отправке) и увеличивает счетчики.
- число фактических повторных отправок; потери для BBR учитываются отдельно при истечении физических попыток.
- обновление текущего количества неподтвержденных данных inflight (в пакетах и байтах) - сколько по этому каналу отправленных и неподтвержденных на текущий момент
Планировщик/балансир каналов (etcp_loadbalancer.c/h):
для каждого канала ограничение полосы с тестированием отклика:
- полосу ограничиваем по объему данных - полному числу байт ethernet пакта (добавляем размер заголовков)
Ограничение реализуем следующим образом:
- в структуре есть счётчик времени (квантизация стандартная 0.1ms) по которое канал загружен данными. И суб-разряды (nanotime) этого счетчика 0.1ns - 0.1ms (считает по модулю 1000000 и переполнения добавляет в основной счетчик) чтобы при высоких скоростях не было потери точности.
Этим счётчиком контролируется полоса: при передаче к нему добавляется расчётное время передачи текущего ethernet пакета (используя текущий bandwidth считаем время передачи одного байта (float) и умножаем на число байт, переводим в таймбазу nanotime). А если при передаче время меньше current_time - delta_time (delta_time - константа в define, x0.1ms) то он устанавливается как current time-delta_time. (при неактивности обновляем время)
счетчик нужен для контроля можно ли передавать следующий пакет ("расчётное время передачи");
когда etcp пытается отправить пакет через линк, то если в линке не инициализировано подключение и он клиент - то он отбрасывает этот пакет и запускает процесс установки соединения.
также процесс установки соединения инициируется при добавлении канала (в etcp_link_new) если это client.
Окно и pacing проверяются до извлечения DATA из очереди; без допустимого UP-линка фрагмент остаётся в очереди.
У каждого линка собственный shaper по точной bbr_pacing_rate в байтах/с:
- После успешного send списываются только payload_len DATA-байтов, без заголовков и ACK.
- Основной счётчик времени имеет единицу 0.1 мс; sub-разряды — 0.1 нс, модуль 1000000.
Прибавка равна ceil(payload_len * 10000000000 / bbr_pacing_rate), вычисляется целочисленно в uint64.
- Накопленный кредит после простоя ограничивается 1 мс. При смене ставки пересчитывается оставшийся долг;
таймер снимается и при необходимости назначается заново. При нулевой ставке pacing не ограничивает отправку.
- Положительный долг должен быть строго меньше 1 мс. Таймер ждёт первого допустимого TB:
shaper_load_time_tb - SHAPER_BURST_DELAY_TB + 1; дробь меньше TB не меняет эту строгую границу.
uasync округляет срок вверх до миллисекунды; callback повторно проверяет готовность перед уведомлением.
- ACK/control и измерительная burst-серия не списывают DATA-pacing. Окно DATA проверяется и в burst.
Конструктор исходящего UDP-линка запускает INIT сразу; TCP использует отдельный STCP connect/handshake.
процесс установки соединения (рукопожатие):
- клиент отправляет init запрос и выставляет таймаут
- по таймауту повторяет init запросы с экспоненциально растущим интервалом (начальный 500ms, макс 50s, +25% каждые 10 попыток), ведется счетчик запросов init.
- по таймауту повторяет INIT: начальный интервал 500 TB = 50 мс, максимум 50000 TB = 5 с;
каждые 10 отправок интервал увеличивается на interval/4 + 1 TB. TB здесь равен 0.1 мс.
- когда клиент получает init подтверждение — снимает таймаут, выставляет link_state=connected, запускает keepalive.
- сервер при получении init запроса: если линк существует и session_id совпал — отвечает без сброса (0x05); если session_id новый — выполняет etcp_conn_reinit и отвечает со сбросом (0x03).
При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT со сбросом (0x02); если link_state=2 (reconnect) — без сброса (0x04).
- каждая сторона сообщает в INIT собственный reset_id (64 бита, BE); ответ не повторяет эпоху запроса.
- первая неизвестная эпоха пира сохраняется из handshake. Смена уже известной эпохи запускает challenge;
только подходящий CONFIRM сбрасывает поток и заменяет эпоху пира, сохраняя собственную.
- повтор INIT и коллизия встречных подключений сами по себе не сбрасывают поток.
При добавлении нового канала: если link_state=1 (handshake) — посылаем INIT_REQUEST (0x02);
если link_state=2 (reconnect) — INIT_REQUEST_NOINIT (0x04). Коды INIT не заменяют подтверждение смены эпохи.
link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
**** Формат кодограмм для etcp_connections.c/h ****
Кодограммы с этими секциями обрабатываются в etcp_connections (в этих кодограммах всегда одна секция-тип пакета). В обязательном заголовке (timestamp+flag_up) ID не используется.
Типы пакетов:
0x02 — ETCP_INIT_REQUEST (со сбросом etcp сессии)
0x03 — ETCP_INIT_RESPONSE (подтверждение со сбросом)
0x04 — ETCP_INIT_REQUEST_NOINIT (без сброса)
0x05 — ETCP_INIT_RESPONSE_NOINIT (подтверждение без сброса)
0x02 — ETCP_INIT_REQUEST (начальный handshake линка)
0x03 — ETCP_INIT_RESPONSE (вариант ответа INIT)
0x04 — ETCP_INIT_REQUEST_NOINIT (восстановление линка, также разрешение коллизии)
0x05 — ETCP_INIT_RESPONSE_NOINIT (вариант ответа INIT)
0x06 — ETCP_PING (проверка доступности)
0x07 — ETCP_PONG (ответ на пинг)
0x08 — ETCP_KEEPALIVE
0x09 — ETCP_SESSION_DATA (обёртка секций DATA/ACK с эпохами)
0x0a — ETCP_SESSION_HELLO
0x0b — ETCP_SESSION_CHALLENGE
0x0c — ETCP_SESSION_CONFIRM
0x0d — ETCP_TCP_ADDRESS (STCP: code + family(4/6) + observed IP(16), без порта)
Варианты ответа 0x03/0x05 не определяют смену эпохи: клиент в обоих случаях обрабатывает reset_id одинаково.
1) Init запрос (0x02 / 0x04):
[code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [keepalive_interval(2)]
[recovery_interval/100(2)] [local_link_id(1)] [sock_id(1)] [only_local(1)] [type(1)]
[code(1)] [node_id(8 BE)] [reset_id(8 BE)] [mtu(2 BE)] [keepalive(2 BE)]
[recovery(2 BE)] [link_id(1)] [socket_id(1)] [only_local(1)] [type(1)]
[src_ipv4(4)] [src_port(2 BE)] [collision(1)] [ed25519_pubkey(32)] [device_type(1)]
[random_padding...] [salt(SC_PUBKEY_ENC_SALT_SIZE)] [obfuscated_pubkey(SC_PUBKEY_SIZE)]
- code: 0x02 (со сбросом) или 0x04 (без сброса)
Фиксированное тело до padding — 67 байт (ETCP_INIT_REQUEST_PKT).
- code: 0x02 или 0x04, вариант handshake линка
- node_id: 64 бита, идентификатор узла (big-endian)
- session_id: 32 бита, случайный ID сессии генерируется клиентом (big-endian)
- mtu_local: uint16_t локальный MTU
- keepalive_interval: uint16_t интервал keepalive в мс
- recovery_interval: uint16_t = recovery_interval/100 (в таймбазах 0.1мс / 100)
- local_link_id: uint8_t (0-255), назначается отправителем
- sock_id: uint8_t, уникальный ID сокета отправителя
- reset_id: uint64_t, собственная локальная эпоха потока клиента (big-endian)
- mtu: uint16_t локальный MTU
- keepalive: uint16_t интервал keepalive в мс
- recovery: uint16_t = link->recovery_interval/100 (исходная единица — 0.1 мс)
- link_id: uint8_t (1-255), назначается отправителем; ноль запрещён
- socket_id: uint8_t, уникальный ID сокета отправителя
- only_local: uint8_t, флаг "только локальные подключения"
- type: uint8_t, тип сокета (CFG_SERVER_TYPE_UNKNOWN=0/PUBLIC=1/NAT=2/PRIVATE=3/LOCAL=4)
- type: uint8_t, конфигурационный тип сокета CFG_SERVER_TYPE_*
- src_ipv4/src_port: адрес интерфейса отправителя; для IPv6 src_ipv4=0, порт сохраняется;
при SOCKS5 оба поля нулевые. Это заявленный адрес, а не наблюдение внешнего NAT-адреса.
- collision: 1 — отправитель разрешает встречное подключение, заявляя роль master для линка
- ed25519_pubkey: публичный ключ Ed25519 отправителя, 32 байта
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
- random_padding: случайные байты до размера handshake (защита от fingerprinting)
- salt + obfuscated_pubkey: публичный ключ с обфускацией (не шифруется AES,
но скрывается XOR с salt и собственным публичным ключом). Передаётся в конце пакета
- salt + obfuscated_pubkey: ключ отправителя XOR SHA256(salt || публичный ключ получателя).
Этот блок не шифруется AES, передаётся в конце пакета
в noencrypt_len байтах (не попадает под AES-шифрование)
Инициирует новый connection для tcp instance. Если tcp instance нет (первое подключение) - создаёт.
Между нодами только одно подключение, но можно добавлять каналы.
INIT устанавливает UDP-линк: создаёт или переиспользует общий ETCP_CONN к пиру.
Между узлами одно ETCP-соединение с несколькими линками; TCP-линк использует STCP handshake.
2) Init подтверждение (0x03 / 0x05):
[code(1)] [my_node_id(8 BE)] [session_id(4 BE)] [mtu_local(2)] [local_link_id(1)]
[remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2)]
[random_padding...]
- code: 0x03 (со сбросом) или 0x05 (без сброса)
[code(1)] [node_id(8 BE)] [reset_id(8 BE)] [mtu(2 BE)] [link_id(1)]
[remote_socket_id(1)] [only_local(1)] [type(1)] [peer_ipv4(4)] [peer_port(2 BE)]
[ed25519_pubkey(32)] [device_type(1)] [keepalive(2 BE)] [random_padding...]
Фиксированное тело до padding — 64 байта (ETCP_INIT_RESPONSE_PKT).
- code: 0x03 или 0x05, вариант ответа INIT; сам по себе не сбрасывает поток клиента
- node_id: 64 бита, big-endian
- session_id: 32 бита, big-endian (должен совпасть с запросом клиента; если нет — клиент игнорирует)
- mtu_local: uint16_t локальный MTU сервера
- local_link_id: uint8_t идентификатор канала сервера
- reset_id: uint64_t, собственная локальная эпоха потока сервера (big-endian), не эхо запроса
- mtu: uint16_t локальный MTU сервера
- link_id: uint8_t идентификатор канала сервера, 1..255
- remote_socket_id: uint8_t (ID серверного сокета; ID клиентского сокета известен серверу из запроса)
- only_local: uint8_t флаг сервера
- type: uint8_t тип сокета сервера
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер — для NAT traversal)
- peer_port: 2 байта (порт клиента как его видит сервер)
- peer_ipv4: 4 байта (IPv4 адрес клиента как его видит сервер, в сетевом порядке)
- peer_port: uint16_t (порт клиента как его видит сервер, big-endian)
При IPv6 оба поля нулевые.
- ed25519_pubkey: публичный ключ Ed25519 сервера, 32 байта
- device_type: CLIENT_TYPE_SERVER/DESKTOP/MOBILE
- keepalive: uint16_t интервал keepalive сервера в мс
- random_padding: случайные байты
3) PING (0x06):
@@ -183,16 +332,34 @@ link_state: 0=just init, 1=handshake, 2=reconnect, 3=connected.
ADDRESS_CHECK (0x08) — аутентифицированное входящее испытание endpoint.
Модель NAT и алгоритм владельца: doc/nat_detection.txt.
При получении init получатель пакета должен:
- reset ETCP_LINK с этим ip_port если он есть
- создать новый ETCP_CONN с этим node_id. если уже существует подключение с этим node_id - проверить ключи на совпадение.
приём кодограмм из UDP выглядит так:
uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue
До изменения транспорта INIT_REQUEST проверяет полное 67-байтовое тело и соответствие node_id
аутентифицированному X25519-ключу. Оба направления INIT отклоняют link_id=0 и MTU меньше
ETCP_MIN_UDP_MTU(family): 191 байт для IPv4, 211 для IPv6. MTU больше 1500 ограничивается сверху.
Получатель создаёт или переиспользует ETCP_CONN и подходящий линк;
для существующего соединения проверяет совпадение ключа пира. Эпоха из INIT учитывается
через etcp_conn_apply_peer_reset_id, без немедленного сброса потока при смене известной эпохи.
Принятый INIT обновляет recv_keepalive, время последнего RX и входной remote_keepalive=flag_up
до переиспользования буфера для ответа. INIT с flag_up=0 снимает прежний UP UDP-линка:
получение запроса доказывает только входящую достижимость, обратный путь ещё не подтверждён.
Ответ и keepalive отправляются и при DOWN, чтобы холодное подключение и восстановление
могли завершить двустороннюю проверку. При DOWN одного линка соединение остаётся UP,
если есть другой работоспособный линк. Повтор неизменного состояния не создаёт нового DOWN/UP.
INIT_RESPONSE должен содержать полное обязательное тело (64 байта без timestamp/flag_up)
и node_id, соответствующий аутентифицированному ключу пира. Усечённый ответ или чужой node_id
отклоняются до изменения живости, эпохи и параметров линка. Дополнительный padding разрешён.
Входящий conn с известным ключом индексируется по node_id до callback создания линка.
Отмена входящего линка из callback не закрывает conn, если у него появился другой линк или NCD-владелец.
Close немедленно отключает линк и ресурсы, но память удерживается active_users до выхода внутренних стеков RX/переходов/callbacks.
Приём UDP: uasync -> etcp_connections_read_callback_socket -> etcp_process_packet ->
etcp_packet_decrypted (проверки и снятие SESSION_DATA) -> etcp_conn_input -> normalizer/output_queue.
Keepalive: [0x08] [flags(1)] [period_ms(2 LE)] [sleep_fields?:4] [padding...].
При KA_FLAG_SLEEP добавляются active_remaining(2 LE) и sleep_duration(2 LE), оба в единицах 0.1 с.
Полезное тело занимает 4 байта без SLEEP или 8 байт с SLEEP; остальные байты — padding.
keepalive пакеты шлют и клиент и сервер с заданным интервалом если нет полезного трафика.
Обычные keepalive периодически шлют обе стороны; полезный TX сбрасывает адаптивный период к базовому.
Сон одной из сторон останавливает обычный таймер. UDP-анонс сна подтверждается флагом SLEEP_ACK,
повторяется до ACK/исчерпания попыток; TCP отдельного подтверждения сна не требует.
RX watchdog работает отдельно в normal и standby и не продлевается собственными отправками.
Сервер прекращает слать keepalive если линк потерян (ждёт keepalive от клиента).
flag_up в заголовке устанавливается в значение recv_keepalive (признак что локальная сторона принимает пакеты).
@@ -206,7 +373,8 @@ Padding keepalive, PING/PONG и STCP-handshake:
user_data_len/resp_data_len обозначают только полезные данные. PONG не копирует padding запроса.
- STCP: salt(8)+pubkey(32) открыты, затем 51 байт зашифрованного handshake и padding_size байт
нулевого padding, зашифрованного продолжением того же CTR. Приём продвигает CTR через padding;
padding_size=0 сразу завершает handshake. Параметры берутся у ETCP_CONN, иначе у instance.
padding_size=0 сразу завершает handshake. При ETCP_CONN клиент читает актуальный диапазон при отправке;
без ETCP_CONN сохраняет выбранную длину при запуске. Сервер выбирает длину независимо по пиру/instance.
- Каждый STCP-поток использует соль своего отправителя: клиентскую для client→server, серверную для server→client.
CTR IV = первые 12 байт SHA256(session_key || stream_id_LE || salt(8) || "uTun3-stream") + четыре нулевых байта.
Отправка инициализируется после генерации своей соли, приём — после чтения соли пира, до расшифровки тела.
@@ -217,14 +385,14 @@ Padding keepalive, PING/PONG и STCP-handshake:
Клиент: если все линки =down то начинается процедура восстановления связи:
- клиент посылает init без сброса (0x04), link_state=2
- сервер как получает init проверяет session_id:
- если session_id совпадает — шлёт подтверждение без сброса (0x05)
- если session_id изменился (клиент перезапустился) — шлёт подтверждение со сбросом (0x03)
- сервер восстанавливает физический линк и отвечает INIT_RESPONSE/INIT_RESPONSE_NOINIT со своей эпохой.
- прежняя эпоха пира не меняет поток; смена известной эпохи подтверждается через CHALLENGE/CONFIRM.
Код ответа 0x03/0x05 не является подтверждением смены эпохи.
**** Connection session confirmation (2026-09) ****
**** Подтверждение транспортной сессии (etcp_session.c/h) ****
Physical UDP INIT / STCP handshake establishes an authenticated link. A stream reset
uses the same control protocol on every initialized UDP or TCP link, bypassing the
normalizer and reliable DATA/ACK queues. A physical reset does not generate DOWN/UP.
normalizer and reliable DATA/ACK queues. A stream reset does not generate physical DOWN/UP.
Encrypted DATA/ACK plaintext after timestamp(2) + flag_up(1):
09 | sender_epoch:u64be | receiver_epoch:u64be | existing ACK/DATA sections
@@ -237,7 +405,8 @@ Control frames (all 25 bytes after timestamp/flag):
0c CONFIRM | sender_epoch | receiver_epoch | echoed_cookie:u64be
Each connection owns one pending peer epoch, fresh challenge, and retry timer.
HELLO and a changed epoch advertised by INIT only request a challenge. Only a
The first nonzero peer epoch from a physical handshake is stored directly if unknown.
HELLO and a change to an already known epoch advertised by INIT only request a challenge. Only a
CONFIRM for the current local epoch and pending random challenge commits a peer
epoch. Changing the peer epoch resets the stream but preserves our own epoch.
A local fatal reset generates a new local epoch and blocks DATA until confirmation.
@@ -251,3 +420,22 @@ only after STCP handshake. It completes the same session confirmation before INI
REINIT notifies stream consumers without tearing down physical ownership. The
router retains end-to-end inflight; BGP repeats JOIN_GROUP and TABLE_REQUEST because
its previous control messages may have been discarded by the stream reset.
Отказы памяти и прерывание потока:
- OOM до сохранения RX DATA в recv_q не создаёт ACK; отправитель повторяет DATA.
- ACK означает принятие в текущий транспортный поток, а успешный etcp_send —
передачу владения транспорту. Оба не гарантируют завершение операции сервиса
после прерывания сессии. Надёжность операции через REINIT обеспечивает её владелец.
- OOM после принятия данных в normalizer или при подготовке TX inflight вызывает
fatal REINIT: новая локальная эпоха, блокировка DATA, отбрасывание старых очередей,
частичных кодограмм и физических попыток. Старые payload не воспроизводятся.
Физические линки сохраняются; очистка очередей/reset не выделяет память.
- Активный обработчик освобождает собственный извлечённый фрагмент ровно один раз;
после reset/close из callback он прекращает разбор старой кодограммы/пакета.
- Согласование новой эпохи выполняется после очистки. OOM control-пакета оставляет
повтор активным, без продления исходного срока 10с. Отказ обязательного session-
таймера или генерации локальной эпохи закрывает соединение с диагностикой.
Если невозможно выделить даже обязательный deferred-close token внутри callback,
процесс завершается через abort: соединение не оставляется в зависшем состоянии.
- После подтверждения эпох владельцы получают REINIT и восстанавливают операции
своего протокола; при невозможности согласования соединение закрывается.
+125 -305
View File
@@ -1,329 +1,149 @@
> Исторический документ. Актуальная реализация и wire-формат описаны в [ETCP Router](../src/routing_layer/etcp_router_doc.md).
# Архитектура ETCP Router
# ETCP Router Architecture
Маршрутизатор связывает сервис с удалённым узлом через группу, не привязывая логическую
сессию к одному физическому ETCP. Ключ канала — `(group_id, remote_node_id, svc_id)`.
Wire-формат и сервисный API подробно описаны в [etcp_router_doc.md](../src/routing_layer/etcp_router_doc.md),
объявления — в [etcp_router.h](../src/routing_layer/etcp_router.h).
## Структуры данных
## Отправка и приём
```
ETCP_ROUTER_CONN (один на пару remote_node_id + svc_id)
┌─────────────────────────────────────────────────────┐
│ ll_entry (хеш-индекс: remote_node_id[8] + svc_id[1]) │
│ remote_node_id: u64 │
│ svc_id: u8 │
│ last_dgram_ts: u16 (timestamp последней датаграммы) │
│ │
│ tx_seq: u32 следующий seq для отправки │
│ rx_seq: u32 ожидаемый seq для сборки │
│ tx_acked: u32 сколько наших пакетов подтвердил remote │
│ last_sent_ack_seq: u32 последний отправленный ACK │
│ │
│ recv_q: ll_queue* очередь reorder (хеш по seq[4]) │
│ send_q: ll_queue* очередь при переполнении inflight │
│ │
│ ack_timer: void* периодический 100ms │
│ idle_ack_timer: void* idle таймаут 500ms │
│ send_waiter: queue_waiter_handle waiter на send_input_q │
│ watchdog_timer: void* проверка инварианта 500ms │
│ send_blocked: u8 inflight полон │
└─────────────────────────────────────────────────────┘
SVC_ROUTE_HDR (22 байта)
┌──────┬────────────────┬────────────────┬───────┬────────┬──────────┐
│ cmd │ dst_node_id │ src_node_id │ seq │ svc_id │ payload │
│ u8 │ u64 │ u64 │ u32 │ u8 │ ... │
└──────┴────────────────┴────────────────┴───────┴────────┴──────────┘
seq в data-пакетах = tx_seq (счетчик отправленных)
seq в ACK-пакетах = rx_seq (ожидаемый следующий)
CLOSE-флаг = 0x80000000 в seq, RST-флаг = 0x40000000
```mermaid
flowchart TD
Producer["Сервис: payload + mode"] --> Send["send_q: plaintext, seq, mode"]
Send --> Session{"peer_reset_id подтверждён?"}
Session -- нет --> Hello["HELLO / challenge / confirm"]
Hello --> Session
Session -- да --> Route["Текущий маршрут: conn_mgr → BGP, либо прямой глобальный ETCP"]
Route --> Wait["Waiter физической send_input_q"]
Wait --> Encode["route_crypto_encode: encrypt → sign"]
Encode --> Inflight["inflight_q: неизменяемое тело без visited-list"]
Inflight --> Path["Добавить source ID"]
Path --> ETCP["etcp_send / normalizer"]
Inflight -- RTO 300 ms --> Wait
ETCP --> Network["Сеть / транзитные узлы"]
Network --> Check["Проверить visited-list"]
Check --> Destination{"dst=self?"}
Destination -- нет --> Transit["Добавить self один раз, BGP next hop / transit FIFO"]
Transit --> Network
Destination -- да --> Decode["Crypto policy + verify/decrypt + проверка эпох"]
Decode --> Incoming["incoming_q: plaintext"]
Incoming --> Reorder["recv_q: reorder по seq"]
Reorder --> Ready{"receive_ready разрешает?"}
Ready -- нет --> Busy["Сохранить пакет, ACK RECEIVE_BUSY"]
Busy --> Resume["resume_receive: отложенное продолжение"]
Resume --> Reorder
Ready -- да --> Callback["Callback сервиса получает entry"]
Callback --> Ack["rx_seq++ → ACK текущей границы"]
Ack --> Release["Отправитель снимает подтверждённый inflight"]
```
## Send Path (отправка данных)
Каждая кодограмма имеет самостоятельный seq; это не байтовый поток.
send_q ограничена 64 пакетами при обычной отправке, `force` обходит этот порог.
Inflight по умолчанию — 256, при minRTT probe — 4. Получатель ограничивает reorder
окном max_inflight и суммарным размером очередей max_inflight+1 записей.
```
etcp_route_send(inst, dst, entry)
etcp_router_conn_send(rconn, data, len)
│
▼
┌─────────────────────┐
│ rconn = find/get │ поиск по (dst, svc_id)
│ авто-создание если │ или etcp_router_conn_get()
│ ещё нет │
└────────┬────────────┘
│
▼
┌──────────────────────────────┐
│ router_enqueue_send(rconn) │ всегда через send_q (FIFO)
│ if send_q >= 64 && !force: │ → вернуть -1 (backpressure)
│ return -1 │
└──────┬───────────────────────┘
│ копия в send_q + router_send_kick()
▼
┌─────────────────────────────────────────┐
│ router_send_kick(rconn) │
│ closed/no_route → return │
│ send_q пуст → return │
│ inflight >= limit → send_blocked=1, ret │
│ waiter уже ждёт → return │
│ нет conn → set_no_route │
│ queue_waiter_wait(send_input_q, │
│ &send_waiter, drain_cb) │
└──────┬───────────────────────────────────┘
│ send_input_q опустел (round-robin: FIFO waiter-ов)
▼
┌──────────────────────────────┐
│ router_send_drain_cb(rconn) │ шлём ОДИН пакет
│ e = send_q.pop() │
│ router_send_one_flags(e) │
│ → tx_seq++ │
│ → etcp_send(conn, entry) │
│ если send_q ещё непуст: │
│ inflight полон → blocked=1│
│ иначе → queue_waiter_wait │ (в хвост → round-robin)
│ если пуст → send_blocked=0 │
└──────────────────────────────┘
Все события выполняются в uasync-потоке. Штатный normalizer откладывает physical waiters,
а callbacks производителей send_q могут выполняться синхронно при освобождении места.
Поэтому код отправки повторно проверяет состояние/эпохи после callback.
Повторный запуск drain:
• приход ACK → router_handle_ack → kick
• маршрут появился → router_no_route_retry_cb → kick
• рост inflight-лимита → router_update_inflight_limit → kick
• страховка → router_send_watchdog_cb (500ms) → DEBUG_ERROR + kick
## Эпохи логического канала
Полный сетевой пакет:
`[57-byte SVC_ROUTE_HDR][payload][signature?][visited_ids:8*N][count:1]`
Числа в заголовке — native endian. flags — отдельный байт; биты seq не заняты флагами.
Заголовок включает group/src/dst/service, seq/timestamp и поля reset_id/peer_reset_id/challenge.
visited-list содержит 1..64 ID, находится вне подписи/шифрования и проверяется на каждом переходе.
Первичное согласование двух сторон имеет вид:
```mermaid
sequenceDiagram
participant A as Узел A, local=a
participant B as Узел B, local=b
A->>B: HELLO(reset=a, peer=0)
B->>A: CHALLENGE(reset=b, peer=a, cB)
A->>B: CONFIRM(reset=a, peer=b, cB)
A->>B: CHALLENGE(reset=a, peer=b, cA)
B->>A: CONFIRM(reset=b, peer=a, cA)
Note over A,B: Каждый узел принимает peer ID только после ответа на свой pending challenge
```
## Receive Path (приём данных)
Получение HELLO/CHALLENGE с неизвестной эпохой само по себе не сбрасывает работающий канал.
Переставленные/старые CONFIRM не совпадают с pending ID/challenge и игнорируются.
Новые challenge ограничены интервалом 300 ms; первичный HELLO повторяется без отдельного timeout.
```
etcp_router_recv_cb(conn, entry)
│
▼
┌──────────────────────┐
│ dst == node_id? │
└──┬───────────────┬───┘
│ YES │ NO (транзит)
▼ ▼
┌────────────────────┐ ┌──────────────────────────┐
│ Мы — целевая нода │ │ Транзит: найти next hop │
└────────┬───────────┘ │ route_bgp_find_conn_for_ │
│ │ node(bgp, hdr.dst) │
▼ │ etcp_send(next, entry) │
┌────────────────────┐ └──────────────────────────┘
│ pl_len == 0? │
└──┬───────────┬─────┘
│ YES │ NO (данные)
▼ ▼
┌─────────┐ ┌─────────────────────────┐
│ CLOSE/ │ │ rconn = find/get │
│ RST? │ │ (авто-создание если нет) │
└──┬──┬───┘ └───────────┬─────────────┘
│ │ │
│ │ NO (ACK) ▼
│ ▼ ┌───────────────────────────────────────┐
│ ┌──────────┐│ Проверка границ seq: │
│ │ tx_acked ││ d = seq - rx_seq │
│ │ = hdr.seq││ |d| < ROUTER_MAX_INFLIGHT? │
│ │ (только ││ │
│ │ вперёд) ││ Дубликат? │
│ │ kick ││ rx_seq - seq > 0 ИЛИ │
│ └──────────┘│ seq уже в recv_q? │
│ └──────┬─────────────┬──────────────────┘
│ │ OK │ out-of-bounds / dup
│ ▼ ▼
│ ┌──────────────┐ ┌─────────┐
│ │ recv_q.put() │ │ DROP │
│ │ хеш по seq │ └─────────┘
│ └──────┬───────┘
│ │
│ ▼
│ ┌──────────────────────┐
│ │ seq == rx_seq? │
│ └──┬───────────────┬───┘
│ │ YES │ NO
│ ▼ │
│ ┌──────────────┐ │
│ │ try_assembly │ │
│ │ while next в │ │
│ │ recv_q: │ │
│ │ deliver() │ │
│ │ rx_seq++ │ │
│ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┴──┐
│ │ router_deliver() │
│ │ вызывает callback │
│ │ сервиса (svc_id) │
│ │ entry: [svc_id|payload] │
│ └──────────────┬──────────┘
│ │
▼ ▼
┌────────────────────────────────────┐
│ router_schedule_ack(rconn) │
│ запускает ack_timer (100ms) если │
│ ещё не запущен │
└────────────────────────────────────┘
```
| Событие | Что сохраняется | Что меняется |
| --- | --- | --- |
| Смена ETCP/next hop | Логические эпохи, seq, очереди | Физический waiter/маршрут |
| Первое подтверждение peer | Накопленная send_q | Заполняется peer_reset_id, начинается отправка |
| Подтверждённый рестарт peer | Собственный reset_id | Очереди, seq, RTT/метрики; уведомление сервиса |
| Явный conn_restart | Ключ group/peer/service и адрес rconn | Новый reset_id, пустое состояние; затем callback и HELLO |
| conn_close | Только данные, переданные во владение callback | Запись снимается с реестра, ресурсы освобождаются |
```
CLOSE / RST ветка:
┌────────────────────────────────────────┐
│ pl_len == 0 && seq & CLOSE/RST │
│ → router_close_and_notify(rconn) │
│ ├─ router_send_close_to_service() │
│ │ (callback с NULL conn) │
│ ├─ отправить CLOSE удалённой стороне│
│ ├─ отмена всех таймеров │
│ ├─ очистка recv_q + send_q │
│ └─ удаление из inst->router_conns │
└────────────────────────────────────────┘
```
Сервис возобновляет прикладную синхронизацию после сброса; старые гарантии доставки через reset не продолжаются.
Готовность router означает наличие подтверждённого peer ID. Это отдельное состояние
от физического UP и READY BGP-группы.
## ACK Mechanism
## Подтверждение и пауза потребителя
```
┌─────────────────────────────────────────┐
│ router_schedule_ack(rconn) │
│ (вызывается при получении данных) │
└────────────────┬────────────────────────┘
│
┌────────────────▼────────────────────────┐
│ если idle_ack_timer активен — отменить │
│ если ack_timer не активен — запустить │
│ ROUTER_ACK_INTERVAL_TB = 1000 (100ms) │
└────────────────┬────────────────────────┘
│
▼ (через 100ms)
┌─────────────────────────────────────────┐
│ router_ack_timer_cb() │
│ │
│ if rx_seq != last_sent_ack_seq: │
│ router_send_ack(rconn) │
│ last_sent_ack_seq = rx_seq │
│ перезапустить ack_timer │
│ else: │
│ запустить idle_ack_timer (500ms) │
└────────────────┬────────────────────────┘
│
▼ (через 500ms)
┌─────────────────────────────────────────┐
│ router_idle_ack_timer_cb() │
│ │
│ if rx_seq != last_sent_ack_seq: │
│ router_send_ack(rconn) (досылка) │
│ last_sent_ack_seq = rx_seq │
└─────────────────────────────────────────┘
DATA проходят crypto policy и verify/decrypt до изменения reorder/rx_seq.
Сервис получает `[svc:1][src:8][dst:8][rx_flags:1][group:8][payload]`, всего 26 байт заголовка.
Его callback владеет entry, ACK продвигается после успешной передачи callback.
router_send_ack(rconn):
hdr.seq = rx_seq
hdr.svc_id = svc_id
pl_len = 0 → это ACK-пакет
etcp_send(conn, entry)
```
ACK подтверждает только диапазон до tx_sent, в модульном порядке uint32.
При продвижении снимаются существующие inflight-записи и один раз учитываются исходные payload bytes.
```
Обработка входящего ACK:
┌────────────────────────────────────────┐
│ pl_len == 0 && нет CLOSE/RST флагов │
│ │
│ if seq >= tx_acked: // только вперёд │
│ tx_acked = seq // обновить │
│ else: │
│ DEBUG_WARN "stale ACK" │
│ │
│ if send_blocked: │
│ send_blocked = 0 │
│ router_send_kick(rconn) │
└────────────────────────────────────────┘
```
RECEIVE_BUSY не подтверждает отложенные DATA, но поддерживает живость ожидающего диска/буфера сервиса.
Даже при прежнем seq допустимый BUSY обновляет отсчёт RTO и останавливает новую отправку/проход ретрансмитов.
Обычный повторный ACK без продвижения этого не делает. Снятие BUSY возобновляет send_q.
## Inflight Control
receive_ready — предикат без I/O и изменения/закрытия роутера. При 0 пакет сохраняется в recv_q;
resume_receive планирует продолжение. Управляющий CLOSE не проходит через этот предикат.
```
ROUTER_MAX_INFLIGHT = 256
| Механизм | Интервал/условие |
| --- | --- |
| ACK | Не чаще 10 ms; текущая граница повторяется при дубликате DATA после потери ACK |
| RECEIVE_BUSY | Около 100 ms, пока сервис остановлен |
| Idle ACK | 500 ms, только если rx_seq ещё не был отправлен в ACK |
| Handshake/RTO | 300 ms |
| Нет маршрута после handshake | Повторный поиск через 20 ms |
| Транзит при backpressure | Waiter + повтор выбора маршрута через 20 ms |
| Watchdog send_q | 500 ms, пока очередь непуста |
| Нет прогресса/BUSY | Закрытие после 17 циклов, для сервиса CALL — после 67 |
Отправка возможна когда: tx_seq - tx_acked < 256
Отправка заблокирована: tx_seq - tx_acked >= 256
Транзитные очереди принадлежат физическому ETCP, сгруппированы по group/src/dst и не имеют своих seq.
При смене маршрута пакет переносится на актуальный next hop без ожидания освобождения старой очереди.
Отсутствие пути приводит к потере транзитной копии; её восстанавливает end-to-end inflight источника.
При блокировке:
данные → send_q (FIFO, без дропов)
send_blocked = 1
## Владение и локальная доставка
Разблокировка:
приход ACK от remote → tx_acked обновляется
→ router_send_kick() → waiter → drain_cb разгребает send_q
или маршрут появился / inflight-лимит вырос → kick
страховка: router_send_watchdog_cb (500ms) → force kick
| Объект | Владение |
| --- | --- |
| Входной entry etcp_route_send | Всегда поглощается, включая ошибку |
| Буфер etcp_router_conn_send | Заимствован на время копирования |
| ETCP_ROUTER_CONN из conn_get | Заимствован до close/destroy |
| Сервисный entry | Передаётся callback; освободить dgram и entry |
| Физический conn в callback | Заимствован, может обозначать транзитного пира |
| Синтетический conn | Только на время callback, instance/peer=self; без сетевого send/ref_take |
| Send-ready handle/arg | У производителя до callback/отмены; перед освобождением отменить |
send_q полностью разобран → send_blocked = 0
```
Self-доставка через etcp_route_send синхронна и не создаёт канал. Она обходит сетевые crypto policy
и receive_ready, выдаёт rx_flags=0; режим проверяется на допустимые биты.
Payload должен быть непустым и помещаться вместе с 26-байтным заголовком в uint16 длину.
Нет обработчика, переполнение размера или отказ памяти возвращают -1.
Отдельный синтетический conn каждого вызова сохраняет контекст при вложенных доставках разных ядер.
## Полная схема потоков
Закрываемый канал удаляется из реестра до callback. Callback может закрывать группу/другие каналы
или открыть замену с тем же ключом; старая запись не возвращается повторным поиском.
Память канала и send_q освобождается отложенно за пределами текущего callback.
До массовой остановки сервисы отменяют producers/waiters; уничтожение ядра из callback останавливаемого сервиса запрещено.
```
┌──────────┐
│ Сервис │ (routing, proxy, NAT, ...)
│ (svc_id)│
└────┬─────┘
│
etcp_route_send() │ callback(svc_id, entry)
▼ ▲
┌─────────┐ send ┌─────────────────────────────────┐ recv ┌─────────┐
│ TUN │─────────▶│ ETCP ROUTER │─────────▶│ TUN │
│ (выход) │ │ │ │ (вход) │
└─────────┘ │ ┌──────────┐ ┌──────────┐ │ └─────────┘
│ │ send_q │ │ recv_q │ │
│ │ (FIFO) │ │ (хеш seq)│ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────┐ │
│ │ Inflight / Assembly │ │
│ │ tx_seq - tx_acked │ │
│ │ rx_seq → deliver │ │
│ └───────────┬─────────────┘ │
│ │ │
└──────────────┼──────────────────┘
│
┌──────▼──────┐
│ BGP │
│ (нахождение│
│ next hop) │
└──────┬──────┘
│
┌──────▼──────┐
│ ETCP conn │──▶ сеть
└─────────────┘
close_group удаляет логические каналы и pending transit указанной группы.
destroy снимает wire callback, закрывает все логические каналы и очищает транзитные очереди,
таймеры и waiters на всех живых ETCP. Уничтожать сам ETCP для очистки роутера не требуется.
──▶ send (исходящие данные)
──▶ recv (входящие данные, callback сервису)
──▶ транзит (через BGP к следующему hop)
```
## Таймеры (один rconn)
```
┌─────────────────────────────────────────────────────┐
│ │
│ ack_timer ──────── 100ms ─────▶ router_ack_timer_cb│
│ (периодический) │
│ │
│ idle_ack_timer ─── 500ms ─────▶ router_idle_ack_cb │
│ (досылка последнего ACK) │
│ │
│ watchdog_timer ── 500ms ─────▶ router_send_watchdog│
│ (страховка инварианта drain, force kick) │
│ │
└─────────────────────────────────────────────────────┘
```
## Флаги seq
```
┌──────────────────────────────────────────────────┐
│ CLOSE: 0x80000000 нормальное закрытие conn │
│ RST: 0x40000000 conn не найден (reset) │
│ │
│ CLOSE/RST-пакеты: seq=0 | флаг, pl_len=0 │
│ Data-пакеты: seq=tx_seq, pl_len>0 │
│ ACK-пакеты: seq=rx_seq, pl_len=0 │
└──────────────────────────────────────────────────┘
```
Метрики delivery считают первые подтверждения исходного payload, без заголовков/crypto/ретрансмитов.
RTT — 0.1 ms, snapshot API выдаёт микросекунды; jitter — unsigned 16.16 EWMA,
монитор публикует удвоенный EWMA. Очереди, метрики и параметры minRTT сбрасываются вместе с логической сессией.
+69
View File
@@ -0,0 +1,69 @@
Аудит документации Local_transport — 26.10.2026
Объём и статус
Полностью прочитаны src/local_transport/local_transport.c и platform.c,
сверены все объявления и комментарии трёх заголовков local_transport.h,
platform.h и protocol.h. Все три заголовка готовы к отметке [V] в чек-листе.
Для проверки интеграции прочитан chat_local.c; прочитаны три профильных теста
и local_transport_ipv6.sh. Полный аудит src/chat этой записью не заявляется.
LAN-раздел doc/node_snapshot.md уточнён; общая отметка документа остаётся [~].
Исправленные описания
1. Доставка и дедупликация. Убрано безусловное обещание отсутствия дублей:
кеш хранит последние 128 успешно принятых (peer, epoch, request) до 30с,
без продления повтором. Отказы received не кешируются. Request уникален
среди pending, а повторное использование ID также ограничено кешем получателя.
FAILED и отсутствие ACK не доказывают, что приложение не получило данные.
Описаны 8 pending, немедленная отправка и до двух повторов по секундному tick.
2. Активность и callbacks. Уточнены синхронные уведомления из API, порядок
activity_changed перед открытием/закрытием сокетов, завершение pending
через FAILED и сохранение эпохи/дедупликации при паузе того же handle.
Отказы открытия сокетов не означают отказ set_active: повтор при сверке;
невозможность создать tick-таймер выключает транспорт. peers() очищает TTL
и может вызвать routes_changed; route() callbacks не вызывает.
3. Broadcast и TTL. Изменение ника рассылается только при active+broadcasts;
тот же ник не вызывает отправку. Отключение broadcasts сохраняет unicast,
приём, сверку интерфейсов и TTL. Только обычный ANNOUNCE продлевает endpoint.
Жизнь кандидата дополнительно ограничена его собственным TTL, интерфейсом
и discovery-endpoint. Offline-gate применяется к подписанному входящему
обмену, но не повторяется при route() и не нужен для ответа на CHALLENGE.
4. Wire. Описаны все смещения 60-байтного заголовка, NUL в 16-байтном magic,
длина тела, BE, типы сообщений, неподписанные MESSAGE/ACK и ограничения
discovery-порта. Уточнены подпись/nonce, scope, допустимые protocol,
уникальные socket_id, ненулевые port/generation и пустой PROOF.
5. Платформа. Зафиксированы различия отбора POSIX/Windows, дедупликация
адресов, усечение снимка, ответственность за ifindex/scope, допустимый
источник и результаты local_receive (включая пустой UDP/would-block/-2).
Добавлены предусловия встроенных BE/UTF-8 helpers и категория discovery.
Проверки
make -C src -j4 libutun.a и сборка трёх профильных тестов — PASS.
Логи /tmp/local-transport-audit-build.log и
/tmp/local-transport-audit-tests-build.log. Существующие предупреждения
db_sync/etcp_connections/dm_media не относятся к изменённым комментариям.
Пять сценариев PASS, последовательно вне sandbox из-за запрета сокетов:
- test_local_transport: UDP discovery, ACK retry/dedup, TTL, настройки,
остановка/повторная активация; /tmp/local-transport-audit-discovery.log.
- test_local_transport_packets: миллион некорректных пакетов, ограничения
flood/памяти/events и восстановление; /tmp/local-transport-audit-packets.log.
- test_local_routes: подписи, nonce/replay, BGP gate, поколения сокетов,
TTL, NCD UDP/STCP и сохранность snapshot; /tmp/local-transport-audit-routes.log.
- sh tests/local_transport_ipv6.sh: изолированная IPv6-only LAN;
/tmp/local-transport-audit-ipv6-discovery.log.
- sh tests/local_transport_ipv6.sh test_local_routes: IPv6 scope и LAN/NCD;
/tmp/local-transport-audit-ipv6-routes.log.
В заголовках изменены только комментарии; логика, ABI и wire не менялись.
Новых подтверждённых дефектов реализации в этом объёме не обнаружено.
Прежняя задача о нестабильном ACK в test_local_transport остаётся открытой:
успешный отдельный прогон не подтверждает устранение флака под общей нагрузкой.
Полный check.sh, санитайзеры, native Windows/FreeBSD и GUI/Android runtime
не запускались. [V] означает завершённую сверку документации по исходникам.
+530
View File
@@ -0,0 +1,530 @@
Аудит media_async / media_delivery — 2026-10-24
Статус: alias encrypted_file, граница voice_file, переанонс SQLite и расхождение MIME write/read исправлены.
Сверка двух каталогов завершена: все11 заголовков, sync.md, _desc.txt и encrypted_attachments.md.
Общий service_lifecycle.md проверен в media-части; разделы других сервисов не считаются закрытыми.
Ниже сохранена хронология воспроизведений и исправлений; прежние отметки незавершённости относятся к этапам.
Последние проверки MIME и итоговая архитектурная сверка — в конце.
Подтверждённый дефект: исходник уничтожается через другое имя того же файла
encrypted_file_write, src/media_async/encrypted_file.c:79-87, сравнивает только строки
source/destination. Разные строки могут обозначать один inode. fopen(destination,"wb")
усекает уже открытый source. Криптография затем успешно обрабатывает пустой файл.
Контейнер корректен для нулевого содержимого, но исходные данные потеряны; возврат равен 0.
Воспроизведено на настоящих функциях из src/libutun.a:
- source и source с компонентом /./: исходные30 байт заменены контейнером152 байта,
plaintext_size=0, rc=0, SHA256 исходника изменился;
- source и hardlink: тот же результат;
- encrypted_file_read тоже пропускает оба вида alias: контейнер182 байта заменяется
plaintext30 байт, rc=0, исходный ciphertext больше не существует в прежнем виде.
У read такая же проверка strcmp в строке118 и разрушающий fopen в строке142.
В текущем групповом pipeline вызывающий выбирает отдельный .part; воспроизведение
доказывает дефект файлового API, а не наблюдавшуюся потерю реального пользовательского вложения.
Ошибку нельзя замаскировать описанием одинаковых файлов как успешного no-op:
шифрование и расшифровка должны создавать другое содержимое.
Диагностика:
- /tmp/media-encrypted-alias-repro.c — четыре проверки, сверяются inode, размеры и SHA256;
- /tmp/media-encrypted-alias-repro.mk — использует цели/библиотеки/флаги tests/Makefile;
- /tmp/media-encrypted-alias-repro.log — полный журнал с media=debug;
- /tmp/media-encrypted-alias-lvVS77/ — сохранённые входы и результаты опыта.
Проектные .c/.h для воспроизведения не менялись.
Согласованная архитектура исправления
Одной проверки file_io_paths перед fopen(wb) недостаточно для полного исправления
контракта владения. Сейчас encrypted_file удаляет destination при ошибке, а
ak_file_done повторно удаляет заранее вычисленный .part даже при раннем отказе.
Нужно убрать открытие произвольного назначения с усечением и очистку по одному имени.
Граница модулей
- lib/file_io: UTF-8/regular-file проверки, идентичность, эксклюзивный temp,
flush/sync/close, atomic rename, владение temp и защита от orphan cleanup.
- encrypted_file: создание/проверка контейнера в собственном temp; без публикации
конечного имени, SQLite и состояния async-задачи.
- chat_attachment: задача владеет temp, выбирает конечное имя и выполняет одну
публикацию; completion меняет состояние БД/UI и освобождает только свои ресурсы.
Подготовка, sync и rename остаются в worker, как сейчас; новые файловые I/O в uasync не добавляются.
Предлагаемый контракт write/read
1. Функция получает source, конечный target_path и указатель на обнулённый file_io_temp.
target_path задаёт место будущего результата; он не открывается с усечением.
До начала работы file_io_paths(source,target_path) обязан вернуть 0.
Совпадение строки, /./, hardlink и конечная ссылка назначения отклоняются без изменений.
2. Источник открывается общим file_io_open_read. Только после успешного открытия
создаётся уникальный temp рядом с target_path через file_io_temp_open.
На POSIX он имеет права 0600; создание эксклюзивное, владелец сразу учтён в общем реестре.
3. Контейнер пишется/проверяется через уже открытый FILE этого temp. Нет второго .part,
второго открытия выхода по пути или дополнительного слоя публикации внутри codec.
4. Полностью проверяются данные, закрытие входа и durable завершение выхода.
При ошибке удаляется только созданный этой операцией temp; существующие source/target сохраняются.
При успехе возвращается закрытый, синхронизированный temp, который ещё не опубликован.
Результат info/type у read присваивается только после полного успешного завершения подготовки.
5. Владелец file_io_temp размещается прямо в ak_file и имеет стабильный адрес до done;
копировать его структуру нельзя: реестр хранит адрес владельца.
Функции не принимают уже активный output и не уничтожают его при отказе аргументов.
Единая политика durability
В file_io_temp_open добавить flags, в file_io_temp хранить выбранный режим на весь
срок операции. FILE_IO_DURABLE включает обе части: finish и publish.
Обычные пользователи передают 0 и сохраняют свою семантику; отдельные флаги в каждом
finish/publish не нужны. Это предотвращает случайное сохранение файла без сохранения имени.
Durable finish: fflush -> fsync/_commit того же открытого fd -> fclose. Ошибка любой
фазы сохраняет первый код ошибки; close выполняется и при отказе sync. Открывать путь
заново ради fsync не нужно. После отказа temp остаётся у владельца до discard.
Publish повторно проверяет идентичность source/target и принимает только закрытый temp.
На POSIX: rename и fsync каталога назначения; соседний temp находится в том же каталоге.
На Windows: общий wide API и MOVEFILE_WRITE_THROUGH. Родительский каталог вычисляется
без обрезания: имя без '/' означает '.', '/file' означает '/'.
Политика sync принадлежит file_io, а не формату зашифрованного контейнера.
Точка фиксации — успешный rename. До неё ошибка сохраняет старый target.
После rename ошибка fsync каталога означает: результат уже установлен, долговечность
имени не подтверждена. Нельзя обещать откат, удалять новый файл вслепую или восстанавливать
старое содержимое. В этот момент temp уже отсоединён от реестра и его имя в owner пусто.
Этот переход должен быть явно описан в file_io.h и проверен тестом.
Длительный fsync выполняется после снятия блокировки реестра временных файлов.
Владение задачи и отмена
ak_file хранит file_io_temp вместо temporary[...]. Worker после успешной подготовки
один раз вызывает file_io_temp_publish с тем же target и источником.
До publish поле temporary непустое; после успешного rename оно пустое даже при
ошибке directory sync. По этому переходу задача фиксирует факт своей публикации.
Completion всегда может вызвать discard: после rename он ничего не удаляет.
Cleanup при отмене/удалении канала учитывает отдельно действительно созданные/
заменённые артефакты. Вычисленный путь не является доказательством владения файлом.
При отказе до создания выхода нельзя удалять существующий target или его alias.
Отдельных async owners, refcount и фоновых потоков не требуется. Ключ по-прежнему сохраняется
до шифрования; ready получателя/публикация сообщения подтверждаются только после
успешного worker и проверки актуальности канала в completion. Авторская строка ключа
сохраняется до worker по прежним правилам. Общей транзакции ФС/SQLite нет: после rename
путь остаётся известен attachment_keys для последующей очистки/повторной операции.
Кеш и восстановление после crash
Активный temp защищён существующим file_io-реестром, в том числе после finish.
В chat_msg убрать специальное сохранение вычисленного cipher_path.part; конечный
cipher_path по-прежнему защищается attachment_keys, временный файл — своим owner.
После crash реестра нет: такие temp допустимо удалить.
Нельзя ограничиться нынешним orphan walk дерева media: plaintext-temp находится
в decrypted, а ak_recover_preparations знает только фиксированный .part.
В init/очистке после завершения workers нужно удалять оставшиеся временные файлы
строгого собственного формата <UUID>.uenc.tmp.<32hex> / <UUID>.file.tmp.<32hex>
в управляемых каталогах media/decrypted. Использовать file_io_remove_orphan,
чтобы сохранить активные temp другого instance того же процесса. Конечные файлы,
ключи и данные иных форматов такой проход не удаляет; ошибка обхода логируется.
Область изменений
Основные файлы: lib/file_io.c/h, media_async/encrypted_file.c/h,
chat/chat_attachment.c, chat/chat_msg.c, test_encrypted_file/test_media_primitives
и сетевые проверки зашифрованных вложений. Остальные вызовы file_io_temp_open
в media_async/media_delivery/video переводятся на явные flags=0.
DM не вызывает encrypted_file_write/read: он использует media_crypto streams
и общие sync/publish helper-функции из encrypted_file.h. При правке общего слоя
необходимо сохранить их durable контракт; перенос общих операций в file_io должен
быть прямым, без совместимых обёрток и без изменения DM state machine.
Формат контейнера, nonce, протокол ключей и схема БД для этого фикса не меняются.
Регрессии и критерии готовности
- Обе операции: одинаковая строка, /./, hardlink, symlink, отсутствующий/необычный source;
source и прежний target не меняются, alias не удаляется, чужой temp не освобождается.
- Обычные файлы размером 0/1/32767/32768/32769 байт, большой файл, UTF-8 пути;
успешная подготовка не меняет target до publish. Read не публикует info/type при ошибке.
- Внедрение ошибок open/fdopen/read/write/seek/flush/fsync/input-close/output-close/
rename/directory-sync/RNG; отсутствие утечек fd/памяти/регистраций temp.
- Подмена target на hardlink между подготовкой и publish: повторная проверка
отклоняет публикацию, исходник сохраняется, удаляется только собственный temp.
- Отказ directory sync после rename: новый файл цел, temp-owner отсоединён,
ошибка не представляется как откат; повторная операция корректна.
- Orphan cleanup во время work, после finish, после publish; crash/restart
в обоих деревьях, чужие имена сохраняются. Отмена, stop и удаление канала
до work, после подготовки и после rename: один done, нет публикации сообщения/ready.
- Профильные 20 тестов, ASAN/UBSan/LSan и полный набор. Изменение file_io требует
также проверки его прежних пользователей: copy, video и media blocks/download.
Этот проект реализован. Успешные 20 тестов ниже относятся к исходной реализации;
проверки исправленного кода приведены в конце.
Что проверено и что осталось
Прочитаны все шесть пар .c/.h media_async, целиком file_transfer.c/h, заголовки
media_delivery/download/index/proto, sync.md и doc/encrypted_attachments.md.
Полная повторная сверка остальных реализаций media_delivery и исправление контрактов
ещё не выполнены. Прежние [V] в общем checklist относятся к предыдущим аудитам;
новые незавершённые пункты отмечены [~].
Linux: ./build.sh --no-gui -j4 — PASS;20/20 профильных тестов — PASS:
media_primitives, attachment, attachment_send, encrypted_file, encrypted_attachment,
media_async, media_index, media_index_regressions, media_delivery_sql,
media_delivery_download, media_download_timeout, media_query, media_blocks,
media_replication, media_delivery_integration, media_delivery_full,
media_delivery_chat, dm_media, dm_worker, services.
Логи: /tmp/media-audit-build.log, /tmp/media-doc-audit-results.log,
/tmp/media-doc-audit-logs/. Штатный test_encrypted_file не покрывает alias.
Старые сбои encrypted_attachment/media_delivery_chat в этом прогоне не воспроизвелись;
один PASS не закрывает задачи о нестабильности.
На исходном этапе полный check.sh и новые sanitizer-прогоны не запускались.
Результаты исправления — 2026-10-24
- file_io_temp получил единый FILE_IO_DURABLE: sync открытого fd при finish,
write-through rename на Windows и directory sync после rename на POSIX.
После rename temporary пуст, включая отказ directory sync; discard не удаляет результат.
Общие sync/publish функции перенесены в file_io; DM вызывает их напрямую.
- Write/read готовят отдельный закрытый temp. Source/target не открываются с усечением;
совпадающая строка, /./, hardlink и конечная ссылка отклоняются до создания выхода.
Read выдаёт info/type только после успешного закрытия/сохранения проверенного результата.
- ak_file хранит один output, учитывает подготовленный plaintext snapshot и выполненный rename.
Worker публикует один раз; completion очищает только собственные артефакты.
Кеш использует реестр владельцев; recovery очищает строгие temp в media и decrypted.
Построение путей и SQL recovery используют одну каноническую media_base.
- test_encrypted_file: прежние шесть размеров, восемь alias-сценариев обоих API,
активный output, alias перед publish и 23 отказа I/O с контролем fd/temp/метаданных.
До rename прежний target сохраняется; после отказа directory sync новый файл установлен.
- test_media_primitives: durable и обычный режим, sync failure до rename, failure после
rename, относительный/корневой каталог, защита активного temp в worker и после close.
- test_attachment_send детерминированно ждёт настоящего crypto rename без обработки
completion, затем останавливает сервис: нет сообщения/ключа/оставшихся файлов/temp.
- test_encrypted_attachment при каждом startup/restart проверяет оба дерева:
stale temp удалены; открытый/закрытый активные owners, конечные файлы и чужие имена сохранены.
- Профильный Linux-набор: 21/21 PASS, включая video/no_ffmpeg, DM, services и media delivery.
Основной make check: 185/185 PASS. TUN в отдельном network namespace с тем же бинарником:
proxy9/9, burst1/1, load8/8 PASS; 134217728 проверенных байт.
- ASAN/UBSan/LSan: 21/21 PASS с detect_leaks=1. Все встроенные зависимости собраны
с sanitizer-флагами в /tmp/utun-media-fix-sanitizers; обычные объекты проекта не смешивались.
Windows/FreeBSD build/runtime не проверялись.
Логи: /tmp/media-fix-profile-results.log, /tmp/media-fix-full-check.log,
/tmp/media-fix-network-check.log, /tmp/media-fix-sanitizer-results.log,
/tmp/media-fix-cancel-after-rename.log, /tmp/media-fix-encrypted-file.log.
Проверка реализации не выявила противоречий владения в перечисленных сценариях.
Частичные отметки остальных файлов аудита сохраняются; этот фикс не означает полного
завершения проверки media_async/media_delivery или закрытия старых нестабильных тестов.
Продолжение аудита — 2026-10-24: граница длительности voice_file
voice_file.h обещает максимум24ч; attachment_validate (attachment.c:35) отвергает
duration_ms >86400000. Однако voice_file.c:16 проверяет samples/channels/rate >86400:
целочисленное деление отбрасывает долю секунды. Записи с полными кадрами на24ч+20..980мс
проходят проверку аргументов. Затем voice_file.c:37 вычисляет точную длительность
в миллисекундах по числу20мс кадров, уже выходящую за предел метаданных.
attachment_send.c вызывает voice_file_encode в worker, а attachment_validate —
при публикации результата. При успешном полном кодировании такой результат будет
отклонён после подготовки файла. Это дефект границы записей длиннее суток;
сбой обычных коротких записей или потеря данных этим опытом не установлены.
Диагностическое воспроизведение на настоящем voice_file_encode из src/libutun.a:
- /tmp/media-voice-duration-repro.c и .mk; сборка через tests/Makefile и его библиотеки/флаги.
- Linux64: виртуальный read-only PCM достаточного размера, без чтения или заполнения.
Linker wrapper opus_codec_encoder_create фиксирует достижение кодировщика
и возвращает NULL до чтения PCM/открытия output. Реальная проверка аргументов не подменена.
- Частоты8000/12000/16000/24000/48000, mono/stereo; для каждой пары проверены
24ч-20мс, ровно24ч, +19мс хвоста, +20мс, +980мс и +1000мс: всего60 случаев.
- Ровно24ч и +19мс хвоста проходят, так как хвост короче полного кадра отбрасывается;
+1000мс отвергается до encoder. На +20/+980мс во всех10 сочетаниях codec достигнут,
тогда как attachment_validate соответствующей длительности возвращает-1:
duration_guard_mismatches=20, confirmed=1.
- Все60 вызовов возвращают-1: для допустивших encoder это намеренная инъекция отказа,
для +1000мс — штатная валидация. Опыт доказывает ошибку допуска к encoder,
но не является24-часовым кодированием или сквозной проверкой отправки такого файла.
- Полный журнал с media=info/debug=info: /tmp/media-voice-duration-repro.log;
сборка: /tmp/media-voice-duration-build.log. Проектные .c/.h не менялись.
Проект простого исправления:
1. Задать один ATTACHMENT_DURATION_MAX_MS=86400000 в attachment.h; использовать
в валидаторе voice/video и проверке video completion вместо отдельных литералов.
2. После проверки rate/channels считать frame_samples=(rate/50)*channels,
frames=count/frame_samples. До создания encoder/открытия файла отклонять
frames<25 либо frames>ATTACHMENT_DURATION_MAX_MS/20 (4320000).
Проверяется длительность записываемых полных кадров, хвост <20мс по-прежнему отбрасывается.
3. Только после ограничения frames считать samples=frames*frame_samples
и duration_ms=frames*20. Размер samples не превосходит count, длительность
помещается в uint32_t; большие count не требуют потенциально переполняющего умножения на20.
4. Сообщение раннего отказа различает неверные параметры и недопустимую длительность;
для длительности выводит count/rate/channels/frames и разрешённые границы.
Дополнительный helper/API или изменение pipeline/формата не нужны.
5. В test_attachment добавить проверку границ500мс/24ч, хвоста <20мс,
превышения на полный кадр, разных rate/channels и раннего отказа без изменения файла/метаданных.
Верхние границы проверять диагностической точкой до encoder без реального кодирования суток;
сохранить существующий настоящий Opus roundtrip короткой записи.
На этапе подтверждения аудит был остановлен по AGENTS.md:10, без изменения рабочего кода.
Пользователь согласовал исправление; результаты приведены ниже.
Результаты исправления voice_file — 2026-10-24
- ATTACHMENT_DURATION_MAX_MS в attachment.h задаёт один предел86400000мс:
его используют валидатор voice/video, video completion и кодировщик голоса.
- voice_file считает frames=count/frame и проверяет25..4320000 до создания Opus
и открытия выходного файла. Метаданные при раннем отказе не изменяются.
Хвост <20мс сохраняет прежнее поведение: отбрасывается, включая хвост после ровно24ч.
- Проверка архитектуры: frame всегда положителен и не больше1920 для разрешённых rate/channels;
деление count/frame не переполняется. После ограничения frames произведение frames*frame
не превосходит исходного count; frames*20 не превосходит86400000 и помещается в uint32_t.
Подготовка, публикация, владение PCM/файлом и формат контейнера не меняются.
- Лог раннего отказа содержит count/rate/channels/frames и разрешённые границы;
debug принятой записи показывает число кадров, длительность и отброшенный хвост.
- test_attachment: 120 граничных вызовов для пяти частот и mono/stereo.
Все70 недопустимых длительностей отвергнуты до encoder; все50 допустимых
дошли до диагностического encoder с инъекцией отказа. Во всех120 случаях
побайтно сохранены исходные metadata и содержимое существующего output.
Linux64 использует виртуальный read-only PCM; суточное кодирование не выполняется.
- Настоящий Opus encode/decode: прежняя1с запись и десять записей на500мс с хвостом
почти20мс, все rate/channels, все три preset. Проверены header, декодирование,
число20мс кадров, EOF и валидность метаданных результата.
- validate/encode/decode voice/video принимают86400000мс и отвергают86400001мс.
- ./build.sh --no-gui -j4 — PASS. В изменённых C-файлах/тесте предупреждений сборки нет;
общие прежние предупреждения остальных модулей остаются в полном логе.
- Профильные тесты10/10 PASS: attachment, attachment_send, encrypted_file,
encrypted_attachment, media_async, dm_media, dm_worker, services, video, video_no_ffmpeg.
Первый запуск attachment внутри sandbox выдал запреты создания stream fd;
весь профильный набор, включая attachment, успешно повторён вне sandbox.
Логи: /tmp/media-voice-fix-build.log, /tmp/media-voice-fix-profile-results.log,
/tmp/media-voice-fix-profile-logs/test_attachment.log и остальные логи этого каталога.
Новый полный make check и ASAN/UBSan/LSan не запускались; прежние результаты encrypted_file
выше не относятся к этому изменению. Windows/FreeBSD не проверялись.
voice_file.h и attachment.h сохраняют [~]: завершение этого фикса не закрывает
оставшуюся сверку контрактов всего аудита. Повторная проверка media_delivery ещё не завершена.
Продолжение аудита — 2026-10-24: переанонс записей SQLite
Полностью прочитаны media_index.c/h, media_download.c/h, media_delivery.c/h,
file_transfer.c/h, media_delivery_proto.h и sync.md. Повторно сверены media_crypto.c/h.
В media_crypto.h уточнены native байты nonce, границы CCM/буферов, пустой поток,
момент выдачи размеров/хеша и отсутствие close/fsync/rename; неверный аргумент не обнуляет выходы.
В file_transfer.h уточнены lookup/авторизация, 16 операций, размер1..INT64_MAX,
усечение своего временного назначения даже при отказе запуска, таймаут и отмена/владение callback.
Изменения только описаний сохранены коммитом5dbca4a2.
Подтверждённый дефект
media_delivery_announce_local_blocks (media_delivery.c:1610) читает media_id/block_id
через sqlite3_column_blob, но проверяет только ненулевой указатель. Затем
media_delivery_send_have_block копирует по16 байт в пакет и подписывает объявление.
media_files имеет BLOB NOT NULL без CHECK длины: такие строки допустимы текущей схемой.
Обычные media_index writers пишут правильные длины; опыт моделирует повреждённые
локальные данные, а не доказанный путь внесения повреждения удалённым участником.
Настоящие SQLite, схема, обработчик, выбор группы/суперузла и Ed25519; перехвачена
только транспортная отправка в существующем tests/media_delivery_fixture.h:
- Контрольные UUID16/16, chunk0, chat_id111: один корректный HAVE_BLOCK.
- media_id длиной1 байт: один HAVE_BLOCK с16-байтным media_id, из них15 байт вне BLOB.
В журнале вместо ожидаемого единственного7a видны посторонние ненулевые байты памяти.
- block_id длиной1 байт: аналогичное чтение15 лишних байт в сетевой block_id.
- media_id или block_id длиной17 байт: отправка с молчаливым усечением до16.
- chunk=-1: wire_chunk=4294967295; chunk=4294967296: wire_chunk=0.
Причина — sqlite3_column_int с последующим приведением к uint32_t без диапазона.
- chat_id111junk: strtoull без endptr принимает префикс111 и направляет объявление в группу111.
- Итого8 случаев,7 повреждённых строк из7 отправлены; invalid_records_sent=7, confirmed=1.
Ошибки соответствующих полей в модуле не логируются. Пакеты сохранены только стендом,
реальные удалённые узлы в опыте не участвовали. Ошибка ASAN или падение не воспроизводились.
Артефакты: /tmp/media-announcement-audit.c, /tmp/media-announcement-audit.mk,
/tmp/media-announcement-audit-build.log, /tmp/media-announcement-audit.log.
Сборка через tests/Makefile с настоящими src/libutun.a/libuasync.a и штатными флагами;
проектные .c для диагностики не менялись. Очистка собственного стенда исправлена и повторно проверена.
Отдельно по коду видно: результаты bind/завершающего step/finalize игнорируются;
SQL-ошибку окончания обхода нельзя выдавать как обычное «done». Инъекция SQL-ошибок здесь не выполнялась.
Проект простого исправления
1. Проверять строку на границе чтения SQLite до доступа как к фиксированным структурам:
media_id/block_id имеют тип BLOB и ровно16 байт; проверить полученные указатели.
Скопировать прошедшие проверку UUID в локальные массивы до вложенных SQL/отправок.
2. chunk читать как int64: тип INTEGER, 0..UINT32_MAX, только затем cast.
chat_id — TEXT, длина1..20 без embedded NUL, каноническое ненулевое uint64 decimal.
Полный разбор должен отвергать суффиксы, знаки, пробелы, leading zero и переполнение;
строка канала должна сохранять свой смысл при переводе в router group_id.
3. Плохую строку пропускать с WARN, указав rowid/типы/длины/некорректное поле,
без чтения за BLOB и без анонса. Корректные соседние строки продолжать обрабатывать.
Никакого общего выделяемого снимка или отката уже отправленных корректных анонсов не требуется.
4. Проверять bind, terminal step и finalize; SQL-ошибки логировать с фазой/кодами
и количеством обработанных строк. Не превращать их в обычный успешный финальный лог.
Сохранить существующий void API, поштучные анонсы и wire-формат.
5. Регрессии в штатной media-цели: valid row, длины UUID0/1/15/16/17 и неверные SQL-типы,
chunk=-1/0/UINT32_MAX/UINT32_MAX+1, неверные/граничные chat_id, embedded NUL;
смесь корректных/плохих записей и ошибки SQL. Перехват HAVE_BLOCK обязан доказать,
что наружу ушли только исходные16 байт проверенных UUID и правильный номер группы/блока.
Аудит остановлен по AGENTS.md:10 после подтверждения дефекта и подготовки проекта исправления.
Новые полные build/check/sanitizer-прогоны не запускались: изменения в репозитории только документальные.
media_crypto.h и file_transfer.h можно отметить [V]; media_delivery.h/proto.h остаются [~]:
нужно исправление этого пути и завершение сверки описаний. Остальные прежние отметки не закрывают
весь аудит. Остались контракты attachment/attachment_send/voice, связанные encrypted/lifecycle документы
и устаревший _desc.txt (RTT-подбор, direct-only репликация и её интервалы расходятся с текущим кодом).
Результаты исправления SQLite-переанонса — 2026-10-24
- Проверка остаётся внутри существующего обхода media_files. UUID допускаются только
как BLOB ровно16 байт; chunk — INTEGER в0..UINT32_MAX, читается через int64.
Исходные SQL-типы получены до преобразований. Проверенные UUID копируются
в локальные16-байтные массивы до вложенных запросов/отправок.
- chat_id имеет тип TEXT, длину1..20; разбор по объявленной длине отвергает embedded NUL,
знаки/пробелы/суффиксы, ноль/leading zero и переполнение. Допускается весь ненулевой uint64,
включая UINT64_MAX. Условие gid<=(UINT64_MAX-digit)/10 до операции исключает переполнение.
- Непроверенные фиксированные memcpy и сужающие cast устранены. Некорректная строка
пропускается с WARN: id/поле, типы/длины, исходный chunk. Хорошие соседние строки продолжают обрабатываться.
- prepare/bind/завершающий step/finalize проверяются. Отказ SQL логирует фазу/коды
и неполный обход; обычный complete-лог не выдаётся. Счётчики rows/eligible/skipped
описывают обработку, а не гарантированную доставку. Уже корректно отправленные пакеты не откатываются.
- Архитектура сохраняет существующие void API, wire-формат, источник ролей и единичные анонсы;
нет общего снимка/новых ресурсов, транзакций или состояния. Statement закрывается и при ошибке bind/step.
- test_media_delivery_sql: 36 случаев полей,32 недопустимых строки не отправляются,
четыре допустимые сохраняют точные UUID/group/chunk/timestamp. Проверены границы
UINT32_MAX/UINT64_MAX, все заявленные длины UUID и неверные SQL-типы, embedded NUL.
В смеси valid/invalid/valid отправлены только два корректных анонса.
- Linux: пять внедрений prepare/bind/first-step/step после первой строки/finalize.
До отказа сохранены0/0/0/1/2 корректных отправки, statement не остаётся активным;
проверяется ERROR с фазой и отсутствие ложного complete. После каждого отказа
обычный повтор успешно отправляет обе строки с исходными UUID и номерами блоков.
После захвата файлового лога тест явно восстанавливает console_enabled.
- ./build.sh --no-gui -j4 PASS; в изменённых C-файлах предупреждений нет.
Профиль12/12 PASS: delivery_sql, query, blocks, replication, index/index_regressions,
delivery_download, download_timeout, delivery_integration/full/chat, services.
После изменения только вывода диагностики SQL-тест повторно прошёл отдельно.
- ASAN/UBSan/LSan4/4 PASS: delivery_sql/query/blocks/replication, detect_leaks=1.
Свежая копия /tmp/utun-media-announcement-sanitizers содержит текущий код;
CPPFLAGS=-fsanitize=address,undefined применён и к встроенным зависимостям.
Обычные объекты проекта не смешивались с sanitizer-объектами.
Первый configure внутри sandbox отклонён LeakSanitizer из-за ptrace;
сборка и тесты успешно выполнены вне sandbox.
Логи: /tmp/media-announcement-fix-build.log, /tmp/media-announcement-fix-profile-results.log,
/tmp/media-announcement-fix-sql-final.log, /tmp/media-announcement-sanitizer-build.log,
/tmp/media-announcement-sanitizer-tests-build.log, /tmp/media-announcement-sanitizer-results.log.
Полный make check и Windows/FreeBSD не запускались. Завершение фикса не означает завершения
сверки оставшихся заголовков и документов двух каталогов.
Продолжение сверки описаний — 2026-10-24: MIME контейнера
Сверены и уточнены attachment.h, attachment_send.h, voice_file.h, media_delivery_proto.h
и комментарий неиспользуемого HAVE_BLOCK timeout в media_delivery.h:
- attachment: UTF-8 и только ASCII C0/DEL, границы полей по kind, состояние выходов
при ошибке encode/decode, content_type не является валидатором kind.
- voice_file: собственное перезаписываемое назначение, поздняя ошибка close может
изменить info; native контейнер и отсутствие fsync/атомарной публикации.
- attachment_send: владение начинается при вызове trampoline, при отказе самого post
остаётся у caller; PCM/compressor, контекст освобождения, DM generation, канальные
request_id/reply, внутренний UUID подготовки и события подготовки вместо обещания доставки.
- proto: вне SYNC числа native-endian; SERVE_REG/LEAVE только вспомогательный учёт,
загрузчик их не посылает. SERVE_ACK не отправляется; приём SERVE_ACK/HAVE_BLOCK_ACK игнорируется,
ожидания ACK/его таймера нет. Не менялись команды, wire или runtime-поведение.
Коммиты описаний:08692fa4 и4e4c60ee. Из общего service_lifecycle повторно сверена
только media-секция111..196 по текущему порядку start/quiesce/destroy и прочитанным модулям;
за пределами media документ не считается полностью проверенным.
Подтверждённый дефект MIME
encrypted_file_write проверяет только ненулевой type и strlen(type)<32, затем шифрует строку.
encrypted_file_read требует непустую NUL-терминированную строку в32-байтном поле,
ASCII33..126 без '"' и '\\'. Поэтому write может успешно подготовить непригодный для read контейнер.
chat_attachment_send (chat_attachment.c:198..225) имеет такую же слабую входную проверку:
до сохранения ключа/старта worker проверяется лишь наличие указателя и длина.
Реальный roundtrip, без подмены криптографии или файловых функций:
- Контроль application/octet-stream: write=0/read=0, hash расшифрованного temp равен исходному.
- Пустой MIME, перевод строки, двойная кавычка, обратная косая черта и не-ASCII UTF-8:
все пять write=0, после publish и вычисления точного внешнего hash соответствующий read=-1.
Ключ, author, UUID и контейнер при чтении те же. Ошибка — invalid metadata/type,
а не аутентификация или порча контейнера в опыте.
- При ошибке read прежние info/type сохранены, temp очищен; SHA256 исходника неизменен во всех6 случаях.
Это подтверждённый дефект принятия аргумента в API. Сквозная отправка/открытие карточки GUI не моделировалась.
- /tmp/media-content-type-audit.c и .mk, сборка через tests/Makefile и настоящие библиотеки;
/tmp/media-content-type-audit-build.log, /tmp/media-content-type-audit.log.
Артефакты: /tmp/media-content-type-audit-FdtMC3/. Лог содержит MIME как hex, без управляющих символов в тексте.
Проект простого исправления
1. Экспортировать encrypted_file_validate_type в encrypted_file.h: 0/-1, bounded strnlen(type,32),
непустая строка длиной1..31, ASCII33..126 кроме двойной кавычки и обратной косой черты.
Это ограничение существующего контейнера, не новый полноценный MIME parser и не правило для всех сообщений чата.
Диагностика показывает длину/позицию/hex ошибочного байта, не выводит сырую непроверенную строку.
2. Вызывать один валидатор в chat_attachment_send до выделения задачи/генерации ключа/записи в БД,
в encrypted_file_write до открытия source/temp и в read для расшифрованного32-байтного поля.
Существующие проверки длины расшифрованного блока сохраняются; правило MIME не дублируется в трёх местах.
3. Не менять формат, nonce, crypto, DB state machine или файловое владение. Reader остаётся строгим;
writer не должен выдавать успех для метаданных, которые этот же reader не принимает.
4. Регрессии: NULL/пустая строка/31/32 байта, каждый запрещённый символ, ASCII-границы
и валидные значения; writer на раннем отказе сохраняет source/target/output.
Для reader плохие metadata шифруются тестом с известным ключом и пересчитанным внешним hash:
отказ не теряет прежние info/type и не оставляет temp. Проверить send-отказ без строки ключа/worker/файлов.
На этапе воспроизведения аудит был остановлен по AGENTS.md:10. Затем пользователь разрешил продолжать исправления.
Реализация и проверка MIME-фикса — 2026-10-24
- encrypted_file_validate_type использует bounded strnlen(type,32) и одно правило для send/write/read.
NULL, пустая/нетерминированная строка и недопустимый байт отклоняются с WARN: длина/позиция/hex.
Успешная проверка имеет DEBUG-лог; сырые непроверенные строки в лог не попадают.
- send проверяет type до выделения задачи, генерации/сохранения ключа и старта worker.
write отказывает до файловых операций и изменения output. read проверяет уже аутентифицированное
поле после проверки длины metadata и сохраняет атомарное присваивание info/type только при успехе.
Валидатор не выполняет I/O и разрешён в uasync; файловые write/read остаются worker-only.
- Правило сохраняет действующий набор reader, включая короткое значение "a"; это не полный MIME parser.
Не добавлены новый wire, изменение nonce/crypto, состояние БД или второй владелец файлов.
Код:90964e7a/f4cc6920, регрессии:aace975a.
- test_encrypted_file: все256 значений первого байта,92 допустимых; NULL отдельно.
Восемь ранних отказов сохраняют output побайтно, source/cipher/target по SHA256.
Три настоящих roundtrip проверяют длины1/24/31, metadata, SHA256 plaintext и сохранность target до publish.
Семь плохих wire-полей зашифрованы настоящим CCM с пересчитанным внешним SHA256:
read отказывает, сохраняя info/type/старый target/source; temp и fd не остаются.
- test_attachment_send: восемь отказов до ключей/worker, число файлов и исходный байт сохранены.
Сохранён прежний сценарий реального encrypted worker, отмены после публикации и recovery.
- ./build.sh --no-gui -j4 PASS; в изменённых C-файлах предупреждений нет.
Профиль10/10 PASS: encrypted_file, attachment_send, encrypted_attachment, attachment, media_async,
services, media_delivery_chat/full, dm_media, dm_worker.
- ASAN/UBSan/LSan4/4 PASS: encrypted_file, attachment_send, encrypted_attachment, services.
detect_leaks=1, halt_on_error=1; встроенные зависимости также инструментированы через CPPFLAGS.
Отдельная копия /tmp/utun-media-mime-sanitizers исключает смешение обычных/sanitizer-объектов.
Первый build остановился из-за нехватки /tmp; очищены воспроизводимые объекты прежних копий,
затем повторная сборка и весь прогон завершились успешно. Исходники и журналы сохранены.
Логи: /tmp/media-mime-fix-build.log, /tmp/media-mime-fix-profile-results.log,
/tmp/media-mime-fix-profile-logs/, /tmp/media-mime-sanitizer-build-retry.log,
/tmp/media-mime-sanitizer-tests-build.log, /tmp/media-mime-sanitizer-results.log,
/tmp/media-mime-sanitizer-test_*.log. Полный check и Windows/FreeBSD для MIME-фикса не запускались.
encrypted_file.h теперь согласован с реализацией. Последующая сверка общих описаний завершена ниже.
Завершение аудита media_async/media_delivery — 2026-10-24
- Все11 публичных заголовков двух каталогов сверены по полным функциям и структурам реализаций:
media_async/crypto, attachment/send, encrypted_file, voice_file, media_index/download/delivery/proto,
file_transfer. Уточнены назначения, входы/выходы, владение и потоки, ошибки и wire-ограничения.
- _desc.txt заменён актуальным описанием подготовки, поиска/загрузки, relay, каталога и репликации.
Убраны прежние предположения о RTT-выборе, только прямых соединениях, часовых повторах и ACK-окне.
Отражены фактические QUERY/CM/transfer UUID, пределы выдач, per-group RX-курсор и атомарный приём страницы.
sync.md уточняет отсутствие ожидания HAVE ACK и проверки SQLite при локальном переанонсе.
- encrypted_attachments.md целиком сопоставлен с chat_attachment.c и источниками desktop/Android UI:
inputbar, MessageList/attachment_key_ui, JNI snapshot, ChatRepository, MessageBubble/Reply/ChannelList.
Уточнены MIME, видимость desktop-кнопки при наличии pending requests и preview цитаты после расшифровки.
Контейнер не выдаётся UI как готовый файл; путь/метаданные заменяются после готовности plaintext.
Это сверка исходников, без нового интерактивного GUI/Android runtime-прогона.
- media-раздел service_lifecycle.md сверён по текущему start/quiesce/join/done/destroy и владельцам
worker/temp/CM/relay. Дописано, что ранний MIME-валидатор допустим в uasync и не выполняет I/O.
Общий документ сохраняет частичную отметку: голос, звонки и другие немедийные владельцы вне этой проверки.
- Архитектура исправлений остаётся простой: формат/валидация контейнера принадлежат encrypted_file,
temp/durability — file_io, публикация и БД/UI — задаче chat_attachment; один владелец на ресурс.
Media_index регистрирует подготовленное, delivery проверяет передачу и ведёт каталог своей группы.
Синхронные приём/проверка/сборка delivery в uasync и отсутствие общей транзакции ФС/SQLite
явно описаны как действующие границы. File_transfer оставляет ключи/авторизацию/проверку caller.
- Все четыре воспроизведённых дефекта исправлены и имеют регрессии. Неразобранных подтверждённых
дефектов в рамках этого аудита не осталось. Последние изменения после MIME-прогона — только описания;
git diff --check проходит. Для MIME действуют профиль10/10 и ASAN/UBSan/LSan4/4 выше;
прежние полные185/186 и sanitizer21/21 не приписываются последующим исправлениям.
Коммиты общей сверки:91fed902 (delivery),8d175049 (encrypted/UI/lifecycle).
Чек-лист: все media-заголовки и encrypted_attachments.md — [V], общий service_lifecycle.md — [~].
Windows/FreeBSD, новый полный check и интерактивные GUI/Android проверки не выполнялись.
+41 -12
View File
@@ -25,7 +25,8 @@
member_files_path=/путь/к/хранилищу (опционально, до 255 байт UTF-8)
Путь, квота и TTL читаются при старте чат-сервиса; изменение требует его restart.
enabled проверяется на каждом запросе. Лимит одинаков для каждого UID отдельно,
enabled проверяется на каждом серверном запросе; клиент доступа к другим storage остаётся доступен.
Лимит одинаков для каждого UID отдельно,
размер UID суммируется по всем группам на этом сервере. Значение в МиБ (1024*1024 байт).
Счётчики used/reserved/files/directories хранятся в памяти дискового worker и восстанавливаются с диска только при старте.
Каталоги не занимают байтовую квоту. files/directories считают опубликованные файлы и каталоги внутри деревьев UID
@@ -86,7 +87,7 @@ Wire, версия 4
Данные максимум 8192 байт; вместе с полным router-путём и crypto укладываются
в лимит normalizer 16384 байта. Размер файла/смещение ограничены INT64_MAX.
Пути — строгий UTF-8, относительные, разделитель '/'. Запрещены пустые компоненты,
компоненты с ведущей '.', завершающей точкой/пробелом, управляющие символы,
компоненты с ведущей '.', завершающей точкой/пробелом, ASCII C0/DEL,
Windows device names и символы \ : * ? " < > |. Корень внутри UID — пустой путь.
POSIX разрешает путь через *at/O_NOFOLLOW; regular-файлы с hardlink отвергаются.
Windows удерживает родительские каталоги без SHARE_DELETE и не принимает reparse points.
@@ -136,6 +137,7 @@ Windows удерживает родительские каталоги без SH
9 RENAME: owner/path/target/version. Только внутри UID/группы, без замены
существующего назначения. Активная загрузка в затрагиваемом пути — BUSY.
10 REMOVE: owner/path/version. Файл или пустой каталог, без рекурсивного удаления.
Непустой каталог возвращает DIRECTORY_NOT_EMPTY.
11 STREAM_WRITE: открытие потоковой загрузки; поля как BEGIN_WRITE,
cursor — ненулевой ID потока. Ответ подтверждает готовность temp и конечный размер.
@@ -164,13 +166,22 @@ Windows удерживает родительские каталоги без SH
При закрытии/перезапуске router-канала его handles завершаются с IO_ERROR;
байты разных эпох не смешиваются. Возобновление через новую эпоху пока не реализовано.
Статусы (0..13): OK, ACCESS_DENIED, NOT_FOUND, INVALID_PATH, INVALID_RANGE,
EXISTS, BUSY, STALE, NO_SPACE, IO_ERROR, BAD_PACKET, HASH_MISMATCH, CANCELLED, TIMEOUT.
Мутации COMMIT/MKDIR/RENAME/REMOVE сохраняют успешный ответ на 120 секунд
Статусы (0..14): OK, ACCESS_DENIED, NOT_FOUND, INVALID_PATH, INVALID_RANGE,
EXISTS, BUSY, STALE, NO_SPACE, IO_ERROR, BAD_PACKET, HASH_MISMATCH, CANCELLED, TIMEOUT,
DIRECTORY_NOT_EMPTY.
Мутации COMMIT/MKDIR/RENAME/REMOVE сохраняют успешный ответ до истечения 120 секунд от начала обработки
(64 квитанции); тот же request_id с другим содержимым — BAD_PACKET.
COMMIT дополнительно узнаётся по upload UUID. При заполнении квитанций новая
мутация возвращает BUSY до истечения старых. Клиент повторяет запрос с прежним
request_id раз в 5 секунд, общий срок 120 секунд. Повторы после этого срока
мутация возвращает BUSY до истечения старых. В таком ответе cursor содержит
остаток до ближайшего освобождения квитанции в timebase 0.1мс (1..1200000).
Нулевой cursor означает BUSY другой причины с обычной задержкой 5 секунд.
Клиент COMMIT/MKDIR/RENAME/REMOVE сохраняет pending и прежний request_id/параметры,
ставит отменяемый таймер на этот срок и повторяет команду; промежуточный BUSY не завершает callback.
При потере ответа обычный повтор идёт через 5 секунд. Общий срок от создания
запроса остаётся 120 секунд и не продлевается даже при BUSY; pump не отправляет
команду после этого срока. Поэтому повторы остаются внутри серверного окна
квитанции, не требуя её досрочного вытеснения. Остальные команды возвращают BUSY caller.
Повторы после этого срока
не дают гарантии того же результата; перед новой мутацией требуется STAT.
Потоки, таймауты и остановка
@@ -222,6 +233,8 @@ quiesce запрещает новые команды, отменяет тайм
вне callbacks дожидается только своих работ/completions и закрывает дисковый
поток до уничтожения CHAT-групп. Контексты не освобождаются под работающим I/O.
Отмена не прерывает текущий системный I/O или хеширование, а ждёт его completion.
Немедленная отмена после start снимает starting: поздний CM UP не запускает STAT/подготовку заново.
После перехода к финализации публикации повторный cancel не меняет уже выбранный результат.
Desktop
-------
@@ -260,7 +273,14 @@ Stop сервиса/группы завершает все принадлежа
Все API/callbacks выполняются в uasync-потоке экземпляра. GUI trampolines используют
тот же API и превращают callback в существующие события прогресса; Qt не делает I/O.
Один локальный файл назначения не может одновременно принадлежать двум download.
Два download с одной строкой local одновременно не допускаются; физические aliases
и подмену локальных путей исключает вызывающий. Upload читает стабильный regular source;
изменение снимка возвращает STALE. На POSIX O_NONBLOCK позволяет отвергнуть FIFO
до блокирующего open/read единственного дискового worker.
Download создаёт соседний temp эксклюзивно и получает право cleanup только после успеха open.
Отказ из-за существующего temp, max_bytes, STALE или ранняя отмена не удаляют чужой путь.
После проверки SHA256 выполняются sync/close и заменяющий rename; до публикации прежний local сохраняется.
POSIX-публикация локального download не делает fsync родительского каталога, в отличие от серверного COMMIT.
Аватарки пользователей
---------------------
@@ -348,18 +368,27 @@ tests/test_member_files: wire/UTF-8, произвольные блоки, пов
резервирование/повторы/TTL, инкрементальные files/directories, замены пустых файлов,
создание/удаление/rename каталогов, отсутствие runtime-rescan, стартовый scan и его ошибки, ссылки, expiry/restart, 10-секундный
fd idle и принадлежность work/done потокам.
На POSIX отдельный процесс с границей 2с проверяет неблокирующее открытие FIFO
и последующий отказ проверки типа; fork выполняется до создания pthread.
tests/test_member_files_network: настоящие A/B/M, подписанная storage-роль,
параллельные upload/download одного пира, local loopback, пустые файлы, побайтная
проверка, отмена одного handle, restart router и destroy с активным server I/O.
Шестисекундная задержка worker проверяет receive backpressure и busy ACK.
Также проверяются чужая запись, виртуальный корень и подписанное удаление читателя.
QUOTA проверяется на настоящем подписанном/зашифрованном канале; удалённый читатель не получает счётчики.
Проверены сохранность чужого temp/прежнего назначения при EXISTS, max_bytes и немедленном cancel,
поздний CM UP после отмены, 76 созданий и 76 удалений с исчерпанием квитанций.
Один успешный ответ отброшен перед клиентским handler: повтор с прежним ID возвращает квитанцию,
счётчики остаются точными (76 каталогов после создания, 0 после удаления).
test_services — stop/start общего чат-сервиса.
Текущая диагностика выделена в категорию debug. Настройка для полного трасса:
Диагностика файлового сервиса — категория media, маршрутизатора — etcp_route.
Настройка для полного трасса этих модулей:
[debug]
debug=trace
debug=debug показывает запрос/ответ, ACL, очереди, маршрут, ACK, crypto-решения,
I/O, fd idle и состояние браузера. trace дополнительно показывает каждый
media=trace
etcp_route=trace
media=debug показывает запрос/ответ, ACL, очереди файлового сервиса, I/O и fd idle.
etcp_route=debug добавляет выбор маршрута, состояния канала и ретрансмиты.
Уровень trace дополнительно показывает каждый
read/write и передачу задач/завершений между дисковым потоком и uasync.
stream progress раз в секунду показывает ID/направление, done/total, число блоков
в кольце, активность I/O, причину ожидания и скорость B/s. Router логирует переходы
+128
View File
@@ -0,0 +1,128 @@
Аудит C-модуля member_files — 2026-10-24
Статус: полностью прочитаны шесть реализаций и шесть заголовков src/member_files/.
Заголовки сверены; четыре подтверждённых дефекта исправлены и проверены.
Серверная/клиентская части doc/member_files.txt согласованы с кодом. Общий документ
остаётся [~]: его desktop/AvatarManager/Android-подсистемы целиком в этот аудит не входили.
Ниже результаты относятся к C-сервису, а не к полному аудиту всех файлов GUI/аватарок.
1. Удаление чужого локального temp
До исправления TRANSFER_FINISH безусловно удалял вычисленный путь temp при ошибке,
включая отказ эксклюзивного open из-за существующего файла и отказ до открытия.
Настоящий test_member_files_network: preexisting target/temp по37 проверенных байт,
скачивание с подписанного storage, MF_EXISTS; target=1, foreign_temp=0, FAIL phase23.
Корректное воспроизведение: /tmp/member-files-temp-repro5.log, строки391..395.
Исправление: один temp_owned у handle, устанавливается только после успешного
эксклюзивного создания. Finish закрывает fd, публикует/удаляет только свой temp.
После успешного rename владение именем temp снято; source/target не подменяются caller.
Новый FILE-слой или второй файловый владелец не добавлены: disk API работает с native fd.
Обычный и sanitizer-прогоны: EXISTS, max_bytes, немедленная отмена сохраняют
прежние37 байт target и чужого temp, finished ровно один раз.
2. Ранняя отмена возобновляла передачу и приводила к UAF
После cancel сразу вслед за start поздний CM UP вызывал wake. Starting оставался1,
поэтому pump после начала cleanup снова посылал STAT. Finish освобождал handle,
а ответ STAT вызывал transfer_response с уже освобождённым arg.
Лог: /tmp/member-files-temp-fixed.log; GDB: /tmp/member-files-cancel-gdb.log,
SIGSEGV в transfer_response → mf_pending_finish → mf_dispatch.
Переход stopping теперь снимает starting; wake без готового финального ответа
ничего не планирует. Обязательная cleanup-задача остаётся единственным продолжением,
затем отправляется финальный CANCEL/END и освобождается handle до callback.
Не добавлены новые ссылки/таймеры или зависимость от скорости ответа CM.
Регрессия early cancel прошла вместе с обычной отменой, параллельными потоками,
router restart и destroy при активном server I/O.
3. Заполнение64 квитанций обрывало массовые мутации
Прежнее воспроизведение в doc/tasks.md: из76 удалений выполнены64, затем MF_BUSY.
Серверный предел/окно нужны для идемпотентности и остаются: до64 успешных
COMMIT/MKDIR/RENAME/REMOVE, MF_REPLAY_TB=1200000 (120с) от начала обработки.
Квитанции не вытесняются досрочно. Полный кеш возвращает BUSY с cursor=остаток
до ближайшего освобождения в timebase0.1мс; это значение кодируется обычным LE-полем.
Нулевой/недопустимый hint использует обычную задержку5с; недопустимый логируется.
Клиент этих четырёх команд сохраняет тот же pending/ID/параметры и ожидает
отменяемый retry timer. Остальные BUSY возвращаются callback как раньше.
Общий срок запроса остаётся120с от создания; ни BUSY, ни потеря ответа его не продлевают.
Перед повторной отправкой pump также проверяет срок. Сервер начинает обработку
не раньше создания клиентского запроса, поэтому допустимые повторы не выходят
за окно первой успешной квитанции. Ошибка/timeout не обещают отката серверной мутации.
При постоянной конкуренции запрос может завершиться TIMEOUT; unbounded waiting нет.
Вариант с продлением срока клиента отклонён при архитектурной сверке до итогового
прогона: поздний BUSY и потеря результата могли вывести повтор за жизнь квитанции.
Один секундный tick также недостаточен для точного освобождения; введён отдельный
retry timer, который снимается до free/cancel/quiesce, с tick fallback при OOM.
Формат125-байтного заголовка не менялся; смысл mutation BUSY.cursor описан в protocol.h.
Новая сетевая регрессия проходит настоящий клиентский API/router/storage:
-76 MKDIR, включая исчерпание кеша; QUOTA подтверждает ровно76 каталогов.
-Первый успешный ответ намеренно отброшен до клиентского handler; повтор с прежним
ID возвращает успех, не создавая второй объект и не выдавая EXISTS.
-76 STAT/REMOVE, включая новое исчерпание квитанций; QUOTA подтверждает0 объектов/байт/резерва.
Qt recursiveDeletion(server-busy) вызывает storage напрямую через stub bridge:
его сырой MF_BUSY остаётся правильным серверным ответом. Поведение полного
клиентского ожидания и массового удаления проверено C-сетевой регрессией, не этим stub.
4. FIFO блокировал общий disk worker до проверки типа
POSIX mf_disk_local_open(source,0) использовал блокирующий open. FIFO без writer
зависал ещё до mf_disk_info, который должен отвергнуть не-regular источник.
Регрессия запускает отдельный процесс до создания pthread, alarm2с ограничивает
только диагностический child. До фикса он завершился SIGALRM (status14), failures=1:
/tmp/member-files-fifo-repro.log. Добавлен O_NONBLOCK, как у серверного open:
FIFO открывается без ожидания, проверка типа даёт INVALID_PATH, fd закрывается.
Обычный и sanitizer-прогоны: child status0, failures0; invalid mode/links имеют WARN.
Произвольный уже выполняющийся системный I/O/хеширование всё ещё не прерывается cancel.
Сверка архитектуры и описаний
-Uasync владеет запросами, streams, local FIFO, ACL, CM handles, waiter/pump/timers.
Единственный worker выполняет файловое I/O последовательно; work FIFO, порядок
done при синхронном drain не обещается. Данные живут до completion.
-Обычных работ до64; заранее зарезервированные cleanup jobs обходят лимит.
Quiesce запрещает producers; drain завершает I/O и cleanup, затем storage destroy
выполняется в worker, после него join/free, пока ядро/группы ещё живы.
-Storage принимает только уже проверенные членство/роли. Сам проверяет UID мутации
и токены temp. Квота общая для UID во всех группах; обычные операции не делают rescan.
-Server COMMIT и local download имеют разные гарантии: локальный POSIX rename
не синхронизирует каталог. Ошибка server sync после rename не откатывает публикацию.
-Пути disk API доверенные и уже проверены protocol/storage. Callback LIST получает
заимствованные name/info; порядок файловой системы, не сортировка. Source/target
aliases локальных download исключает caller; одинаковая строка local запрещена.
-Codec проверяет формат и размеры, не ACL и не UTF-8 policy пути. Ошибка decode
может изменить output; encode не проверяет семантику команды. Эти границы описаны.
-Все поля/функции шести .h получили соответствующие контракты и комментарии.
Проверки и границы
./build.sh --no-gui -j4 PASS: /tmp/member-files-audit-final-build.log.
Профиль7/7 PASS: member_files, member_files_network, services, encrypted_attachment,
dm_media, attachment_send, member_adapter. Результат: /tmp/member-files-audit-profile-results.log,
подробности: /tmp/member-files-audit-profile-logs/.
ASAN/UBSan/LSan4/4 PASS, detect_leaks=1, halt_on_error=1: member_files,
member_files_network, services, encrypted_attachment. Отдельная копия:
/home/user/utun4/.cache/member-files-asan-b87xwg24, все встроенные зависимости
инструментированы через CPPFLAGS; системные OpenSSL/FFmpeg не пересобирались.
Логи: /tmp/member-files-sanitizer-build.log, /tmp/member-files-sanitizer-tests-build.log,
/tmp/member-files-sanitizer-results.log, /tmp/member-files-sanitizer-test_*.log.
После этого прогона добавлены только уточнения описаний и WARN без изменения логики.
В sanitizer-сборке остаётся прежнее предупреждение snprintf в disk_parent_open:
strlen(component)<=MF_NAME_MAX проверяется до записи в leaf[MF_NAME_MAX+1];
воспроизведённого усечения здесь нет. Остальные предупреждения сборки относятся
к существующему коду других модулей/зависимостей, не скрывались фильтрами компилятора.
Первые диагностические прогоны до repro5 недействительны для temp: тест был
собран с новым utun_instance.h и старой libutun.a, в startup читались0xaaaaaaaa.
Общая сборка устранила несовпадение; ненужное изменение маршрутного условия отменено.
Один промежуточный старт остановился из-за полного /tmp; очищены только
воспроизводимые объекты предыдущей sanitizer-копии, исходники/журналы сохранены.
Новый полный make check, Windows/FreeBSD runtime и GUI/Android проверки не выполнялись.
Четыре подтверждённых дефекта закрыты; новых подтверждённых незакрытых в этом объёме нет.
+37
View File
@@ -107,3 +107,40 @@ IP совпали — трансляция IP не обнаружена; отл
публичному подключению и сохраняется при последующих совпавших наблюдениях IP.
Через SOCKS внешний IP прокси
не записывается как собственный адрес.
Управляющий обмен и ресурсы
--------------------------
Команды идут через сервис NAT_DETECTION ETCP-router с подписью и шифрованием.
Используются узлы группы с рабочим маршрутом через READY-соседа; это не требует
прямого UDP-соединения A с C. CHAT-допуск требует полной неудалённой записи участника.
Источник и группа берутся из проверенного заголовка router, а не из next hop.
Версия управляющего формата — 4. Основное сообщение занимает 117 байт:
10 однобайтовых полей команды/сокетов/этапа/результата, шесть uint64
(owner, generation, round, token, observer, helper), uint16 timeout_ms
и три адреса по 19 байт (target, primary, observer_address).
RECHECK занимает 23 байта: cmd, command, version, socket_id и адрес 19 байт.
Многобайтовые числа кодируются вручную в big-endian; адреса — route_ping_addr_encode.
Это размеры сообщения после служебного заголовка router. Приём требует точного
размера; старые generation/round/token и чужие делегирования результата отклоняются.
Детектор работает с шагом 100 мс. Помощник отправляет до трёх проб на этап;
таймаут одной пробы — не меньше секунды и не меньше четырёх RTT наблюдателя.
Общий бюджет обхода — 30 секунд; незавершённый обход может продолжиться после паузы.
Полный результат перепроверяется через 30 минут, неполный — через 30 секунд.
Повтор по неудаче подключения ограничен одним запросом за пять минут на endpoint.
Сохранённый успешный ETCP-профиль не истекает по таймеру; изменение endpoint его сбрасывает.
История исходящих хранит до 256 IP для каждого локального endpoint (IP:порт).
Она сохраняется в SQLite. Переполнение или ошибка загрузки/записи помечают историю
неполной: UNKNOWN сохраняется до достоверного измерения, OPEN по незнакомому входу
с неполной историей не подтверждается. Кеш наблюдений ограничен 128 записями,
одновременно выполняется не более 64 задач помощи.
Детектор владеет таймерами, подписками и изолированными probe_only UDP-сокетами.
Эти сокеты не публикуются как рабочие адреса. Их отдельное удаление снимает ссылку
в состоянии родителя, отменяет зависимые пробы и допускает обычный последующий teardown.
Ошибка регистрации любой подписки или начального таймера отменяет весь запуск.
Ядро сохраняет контекст в instance->nat_det; останавливает его до ETCP/router
и обнуляет поле после destroy. Уничтожать ядро из callback детектора нельзя.
+75 -2
View File
@@ -6,6 +6,14 @@
заменяются вместе. Timestamp входит в подпись. Групповые пути и подсети
остаются в `TOPO_GROUP_NODE` и не являются частью общей записи узла.
В NODEINFO секции подсетей присутствуют только при `TOPO_FLAG_SEND_SUBNETS`.
Без флага оба счётчика подсетей равны нулю; противоречащий этому заголовок отклоняется.
Parser проверяет точную длину пакета и проходит все присутствующие секции независимо
от запрошенных `out_*`: например, `out_subnets=NULL` отключает создание объектов подсетей,
но сохраняет проверку префиксов и правильное положение следующего hop-list.
Выходы выдаются только после полного успеха; при ошибке они обнулены, частичные объекты
освобождены. Подпись и допустимость пути проверяются после структурного разбора.
Получатель проверяет подпись до изменения реестра или маршрутов. Более свежая
запись заменяет старую целиком, включая пустые списки адресов. Старые записи
игнорируются; разные подписанные записи с одинаковым timestamp — конфликт.
@@ -13,6 +21,12 @@
8-битный `ver` сохраняется как диагностический счётчик; свежесть определяется
только timestamp.
Локальный узел группы готовится до удаления прежнего `local_node`: сравниваются сами адреса
и префиксы подсетей, включая замену при прежнем количестве. При отказе памяти, подписи или
конфликте IPv4-маршрутов прежние узел и маршруты сохраняются. Подсети CHAT не публикуются.
`topo_group_update_my_nodeinfo` возвращает число IPv4-подсетей, для CHAT — 0, при ошибке — -1;
рассылку подготовленного NODEINFO выполняет вызывающий.
Живая телефонная занятость передаётся без изменения структуры NODEINFO: в существующем
`flags` бит `0x04` означает известное состояние, `0x08` — занятость звонком.
Это групповые флаги `TOPO_GROUP_NODE.phone_status`, как флаг рации `0x02`;
@@ -50,6 +64,8 @@ snapshot загрузка использует его. Локальная пуб
Уникальность определяется endpoint и видом адреса,
а не отображаемым классом NAT. Старый производный формат пересоздаётся;
bootstrap-загрузка без подписанного snapshot не подтверждает NAT-профиль.
Такая загрузка требует хотя бы одного адреса; отказ SQL/памяти освобождает все частичные списки
и возвращает NULL. Наличие валидного snapshot с пустым списком адресов, напротив, допустимо.
При удалении старых `nat_evidence`/`nat_checked` адреса и REALITY сохраняются для bootstrap,
старые профили сбрасываются, сериализованные snapshots инвалидируются.
Максимальные timestamp snapshots сохраняются, чтобы новая локальная публикация
@@ -62,14 +78,71 @@ Timestamp 0 обозначает bootstrap-сведения без подпис
После принятия BGP-записи `node_conn_direct_update_node` применяет адреса к
существующему NCD-соединению. Отсутствующее соединение этот вызов не создаёт.
Путь подготавливается до изменения общего реестра и групповых подсетей: отказ
памяти сохраняет прежнюю запись, ссылки и пути. После commit IPv4-таблица
перестраивается для этого узла, включая удаление старых подсетей при пустом
новом наборе. Конфликт подсетей или OOM установки маршрутов логируется,
новый набор не устанавливается, прежние маршруты удаляются; ошибка запрещает
READY сессии. Принятая подписанная запись и пути при этом могут сохраниться.
Линки дополняются как при UP, так и до установления соединения; повторная
доставка не создаёт дубликаты UDP/TCP. Работающие линки не закрываются только
из-за смены списка адресов. NCD удерживает ссылку на общую запись, пока
обслуживает соединение, и использует её при восстановлении.
Для обхода адресов NCD делает отдельную копию: callbacks транспорта могут заменить
списки в общем реестре до завершения обхода. Удержание ссылки на запись сохраняет
сам объект, но не старые списки. Обновление, открытие и добавление нового сокета
используют общий подбор разрешённых пар: family, UDP/TCP, локальная политика,
NAT-профиль и обязательные метаданные REALITY. IPv6 link-local требует netif_index.
Новый сокет обслуживает NCD-транспорты вне FIN_WAIT; случайный выбор одного адреса не используется.
`node_conn_direct_open_node` сверяет переданную запись с реестром/БД. Более
свежая подписанная запись принимается целиком, старая уступает уже известной.
Bootstrap-адреса не перекрывают подписанную запись даже при DOWN.
ID узла проверяется по public_key. Ошибка запуска возвращает NULL handle и не снимает
прежний FIN_WAIT. Отдельно принятый snapshot и уже добавленные линки при частичной
ошибке могут сохраниться; новый handle публикуется после подготовки ресурсов.
Явный specific_sock задаёт локальную политику, сохраняя проверки family/протокола/цели/REALITY;
автоматическое появление сокета использует обычную политику подбора.
LAN discovery предоставляет отдельные адреса подключения и не меняет подписанный снимок.
Открытое объявление рабочих сокетов и ответ на проверку имеют Ed25519-подпись;
публичные ключи в этих пакетах отсутствуют. Получатель использует известный ключ
из registry/SQLite, а неизвестное устройство остаётся только в списке discovery.
Обычные объявления имени, сообщения и ACK не подписаны и не подтверждают личность.
Общий UDP-порт 45731 принимает только объявления; unicast-обмен использует отдельные
ephemeral-порты. Формат и API описаны в `src/local_transport/protocol.h` и `local_transport.h`.
Подписываются весь заголовок local_transport (magic/version/type, длина, IDs/эпохи,
request=0) и тело: nonce(16), family(4/6), IP(16), количество сокетов,
затем socket_id/protocol/port(BE)/generation(BE) каждого сокета. IPv4 дополнен
нулевыми байтами. Подписанный IP обязан совпасть с источником UDP; рабочий порт
отдельный от discovery-порта. Публикуются принимающие IP/interface обычные сокеты,
без PRIVATE, probe и SOCKS UDP; до 32 endpoint на один IP, без фрагментации.
Объявление само не разрешает NCD использовать адрес. Только для узла без прямого UP
и живого BGP-пути во всех группах получатель отправляет случайный nonce;
подписанный ответ связывает его с обоими node_id и эпохами discovery. Ответ принимается
однократно в течение 3 секунд. Это проверка свежести без часов/NTP: старые подписи,
эпохи или случайные поколения сокетов сами по себе свежесть не доказывают.
После проверки адрес действует 30 секунд по монотонным часам и только на интерфейсе
приёма (включая IPv6 scope). Повтор объявления/ответа не продлевает срок без нового nonce.
Дополнительно должен оставаться живым discovery-endpoint: только обычный ANNOUNCE
продлевает его собственный TTL 30 секунд; MESSAGE/ACK и подписанный обмен этого не делают.
Смена discovery-эпохи очищает endpoints и кандидаты. Проверка offline выполняется
при обработке подписанного объявления/ответа; чтение кандидатов её не повторяет
и само по себе не удаляет ранее проверенные адреса при появлении рабочего пути.
Сверка интерфейсов и периодические объявления выполняются каждые 10 секунд,
очистка TTL — по секундному tick или при запросе списка устройств/отправке сообщения.
Отключение broadcasts сохраняет unicast-объявления и ответы, приём и сроки жизни;
полная остановка discovery закрывает сокеты, завершает pending-доставки и очищает кандидаты.
NCD использует эти кандидаты при проверке совместимости, открытии/обновлении
и появлении локального сокета. Если сохранены только ключи, идентичность загружается
из nodes без требования старого адреса. LAN не создаёт NCD handle автоматически.
Неподтверждённые ETCP-пробы очищаются после истечения кандидата/остановки discovery;
после успешного handshake линк живёт по обычным правилам транспорта. REALITY требует
метаданных из известного снимка; прямые LAN TCP-пробы при глобальном SOCKS выключены.
Диагностика: discovery для подписи/nonce/адресов, etcp для NCD-проб и их удаления.
Конфигурационные подключения UTUN используют один адаптер
`init_client_connections` для старта и reload. Все адреса, TCP и REALITY
@@ -78,8 +151,8 @@ Bootstrap-адреса не перекрывают подписанную зап
удерживает один handle на клиента, независимо от количества его адресов.
Включение конфигурационного пира в UTUN-группу выполняет callback NCD UP.
Транспортный UP и наличие callback старого `etcp_connect` больше не включают
соединение в UTUN-группу автоматически. Входящий JOIN остаётся явным запросом
Само событие транспортного UP не включает соединение в UTUN-группу автоматически.
Входящий JOIN остаётся явным запросом
участия. При пересоздании UTUN-группы учитываются её конфигурационные handles.
Удаление клиента отправляет `LEAVE_GROUP` и освобождает удержание UTUN-группы
с обеих сторон. Другие владельцы NCD сохраняют транспорт. Поздние NODEINFO и
+5 -2
View File
@@ -14,8 +14,11 @@ GUI добавляет заголовок «Приглашение в групп
Доставку, повтор и шифрование обеспечивает существующий механизм DM.
После успешной отправки открывается личная беседа с получателем.
Срок действия ключа — **10 минут с момента создания**, а не получения PM.
Он указан в диалогах отправки и подключения. Поздняя offline-доставка
Срок действия ключа — **10 минут с момента последней успешной регистрации на каждом узле**.
Повторная регистрация того же ключа для того же канала тем же инвайтером продлевает срок.
Connection-узел сохраняет ключ при обработке регистрации, инвайтер — после её подтверждения;
при совпадении этих ролей запись одна. Для JOIN ключ должен действовать у обоих.
Получение PM срок не продлевает. Длительность указана в диалогах отправки и подключения. Поздняя offline-доставка
может принести уже просроченную ссылку; в этом случае нужно новое приглашение.
Ссылка не содержит время истечения, поэтому клиент показывает длительность,
без обратного отсчёта и вычисления оставшегося времени.
+198
View File
@@ -0,0 +1,198 @@
Proxy: исправления и проверка архитектуры
Дата: 2026-10-24. Статус: реализовано и проверено на Linux.
Ниже сохранён согласованный проект; фактические изменения и результаты — в конце.
Подтверждённые факты до исправлений
1. udp_proxy.c не проверяет bind/sendto, результат socket_set_nonblocking,
часть выделений/регистраций/таймеров и отправку ответа через router.
Неудачный запуск flow может оставлять ресурс без рабочего таймера очистки.
2. server_init и client_create оба запускают общие UDP/ICMP-контексты.
client_destroy сохраняет их по server.enabled, server_destroy уничтожает всегда.
Простой rollback через destroy одного модуля не выражает владение общими ресурсами.
3. ICMP init публикует контекст и сообщает успех без raw socket. При этом тот же
контекст нужен для клиентских REPLY: отсутствие возможности exit не означает,
что клиентская сторона ICMP не может работать через другой exit.
4. В диагностическом прогоне tcp_io error_cb при HUP отменяет read_waiter,
включает чтение и напрямую вызывает read_cb вопреки read_paused.
Для fd=15 порог чтения 4; журнал фиксирует read_paused=1 и рост rq до 53.
Это нарушение ограничения очереди, а не только избыточная диагностика.
5. EPIPE/ENOTCONN в HTTPS CONNECT воспроизведены. Системная трасса четырёх TLS-потоков:
recv(destination)=0 -> поздняя TLS-запись 24 байта от клиента -> send(destination)=24
-> SO_ERROR=EPIPE либо shutdown(SHUT_WR)=ENOTCONN. Повторный shutdown не обнаружен.
Определение содержимого зашифрованной TLS-записи по трассе невозможно.
Сам по себе этот сценарий не доказывает дефект half-close в proxy.
6. После удаления exit-потока приходят поздние WINDOW: диспетчер пишет unhandled,
хотя это известная команда уже завершённого потока. Создавать поток по WINDOW нельзя.
7. enabled=yes в серверных тестовых конфигах не поддержан парсером. Сейчас сервер
включается при обработке любого параметра секции, включая неизвестный.
Предлагаемая архитектура
Владелец общих UDP/ICMP-контекстов — UTUN_INSTANCE. Их жизненный цикл координируется
в utun_instance.c после router_init и до router_destroy. TCP client/server управляют
только своими listeners, TCP-потоками и bindings. Общих счётчиков ссылок не требуется:
при существующем lifecycle они живут до остановки instance.
Контексты нужны при включённом exit или клиентском TUN; SOCKS/HTTP без TUN их не требуют.
server.enabled обозначает действующий TCP exit. Роль при запуске ICMP exit должна
передаваться явно, а не определяться случайным порядком вызовов init.
При отказе создания клиента rollback освобождает только его ресурсы и собственный binding.
Повторное создание клиента/сервера поверх активного состояния отклоняется.
Результат client_create публикуется в inst->tcp_proxy_client только после успеха.
Порядок остановки: клиент и exit-потоки -> общие UDP/ICMP -> router -> drain deferred free.
При destroy всего instance остановку proxy перенести перед остановкой UTUN-группы,
разборкой topo_groups и ETCP-сокетов/соединений. Сейчас proxy уничтожается в phase M,
после topo_groups и транспорта: для нового rollback нельзя сохранять этот поздний порядок.
Общий instance_init_common уже вызывает instance_destroy при отказе создания;
нужно использовать этот путь, не добавляя параллельную систему частичной очистки.
При create_from_config сохранить существующее правило: отказ возвращает config вызывающему коду.
Ресурсы закрываются в одном uasync-потоке; stop/destroy не вызывается из активного
callback уничтожаемого владельца. Таймеры/waiters отменяются до освобождения памяти.
Инициализация и ICMP
Разделить создание общего ICMP-контекста/регистрацию REPLY и запуск raw exit socket.
Это две разные операции: при отказе raw нельзя уничтожать работающую клиентскую сторону.
Например, icmp_proxy_init создаёт общий контекст, отдельный enable_exit открывает
raw socket и регистрирует чтение. Новый raw socket публикуется только после полного успеха.
Обе операции возвращают ошибки непосредственно своему вызывающему коду.
Рекомендуемая политика: EPERM/EACCES/отсутствие поддержки raw явно отключают только ICMP exit,
с WARN о причине и итогом запуска tcp=on/udp=on/icmp_exit=off. TCP/UDP продолжают работать.
Ошибки выделения/регистрации обязательных ресурсов прерывают запуск proxy с полной очисткой.
Если продукт требует обязательного ICMP, отсутствие raw должно быть фатальным на уровне
конфигурации; это отдельная политика, а не скрытый fallback внутри init.
В production виртуальные echo-ответы недопустимы. Тестовый loopback выбирается явно
и должен работать одинаково с правами raw и без них.
Заданные SOCKS/HTTP listeners обязательны: enabled с пустым/неверным адресом либо
неудачный bind/listen проваливают create. Нельзя вернуть готовый клиент без выбранного входа.
Проверки router bind оставляем, но при живом inst нынешний bind не выделяет память
и возвращает 0; не нужно изображать реальные OOM-пути там, где их нет.
UDP и ограниченные ресурсы
Новый flow сначала полностью создаётся локально: socket -> nonblocking -> bind ->
read registration -> обеспечение expire timer. Только затем добавляется в список.
Любой отказ снимает уже созданные регистрации/таймеры и закрывает именно новый сокет.
Если общий таймер не удаётся продлить, оставшиеся flows закрываются с диагностикой:
состояние без гарантированной очистки не сохраняется.
sendto проверяется по точному размеру. Для нулевой датаграммы n=0 означает успех.
EINTR допускает повтор операции; EAGAIN/ENOBUFS означает явное отбрасывание датаграммы
с диагностикой. Не добавляем неограниченную очередь и TCP-подобные повторы UDP.
Остальные ошибки также логируются с peer, tuple, длиной и errno; packet-specific ошибка
не должна автоматически уничтожать рабочий flow. Невалидный/сломанный сокет удаляется.
Проверяются ошибки router send, выделения ответов и доставки в TUN.
ICMP request тоже не публикуется без рабочего механизма истечения срока ожидания.
TCP HUP, FIN и границы ответственности
FIN относится только к одному направлению и следует после его DATA. Ошибка реальной
записи остаётся ошибкой даже после FIN противоположной стороны; ENOTCONN не превращается
в успешный on_fin_sent. Существующая регрессия test_fin_shutdown_error сохраняет этот контракт.
HUP — повод обработать завершение сокета, но не разрешение обойти high_water или явную pause_read.
При достижении high_water чтение останавливается до low_water. Если level-triggered HUP
не даёт event loop уснуть, регистрацию сокета нужно временно снять и восстановить по
queue waiter; простой return с оставленным HUP в epoll/poll создаёт busy loop.
Нужен один механизм восстановления регистрации в tcp_io, учитывающий connected,
fin_remote, write_buf, write_queue и уничтожение владельца. Состояние fd/пулы сохраняются
до окончательного destroy; запланированная запись не должна зависеть от паузы чтения.
Повторное добавление использует новую generation uasync, старые события игнорируются.
Явную паузу владельца следует отличать от паузы по high_water; HUP не снимает ни одну.
Это наиболее чувствительный этап: перед реализацией проверить переходы регистрации
при HUP с ожидающей записью, low_water, обеих FIN и destroy из callback.
Не менять глобальную семантику всех регистраций uasync ради одного tcp_io-сценария.
Поздние WINDOW/FIN/CLOSE/ERROR для отсутствующего потока игнорируются как события
завершённого sid с debug-диагностикой. DATA не воскрешает поток, неверное окно живого
потока по-прежнему является ошибкой. HTTP не обязан ждать FIN для ответа с известной длиной.
Проверить синхронный loopback: состояние перед отправкой control резервируется заранее;
после callback с возможным destroy нельзя обращаться к владельцу без проверки его жизни.
HTTPS-тест разделить на нормальное двустороннее TLS shutdown и явный abrupt close.
В первом случае ожидать отсутствие EPIPE/ENOTCONN; во втором — корректную ошибку,
сохранность уже доставленного ответа и освобождение ресурсов. Proxy не разбирает TLS.
Сначала проверить эту гипотезу управляемым тестовым сервером, затем менять fixture.
Проверка после реализации
- Fault injection: socket/nonblocking/bind/read registration/expire timer/sendto/OOM.
Проверять отсутствие fd/памяти/таймеров/handlers после каждого отказа и успешный повторный запуск.
- client only, exit only, обе роли на одном instance, два независимых instance;
отказ raw сохраняет ICMP REPLY клиента, остановка одного TCP-модуля не ломает общие контексты.
- UDP payload=0, краткая/ошибочная отправка, transient error и следующий успешный пакет.
- HUP при полном proxy window и остановленном consumer: очередь остаётся ограниченной,
event loop не крутится, после снятия блокировки все байты и FIN доходят в правильном порядке.
- pending write при HUP, manual pause, low_water/re-register, destroy в callback и stale events.
- FIN в обе стороны при backpressure, RST до/после ответа, нормальный/оборванный TLS,
поздний WINDOW, синхронный loopback и HTTP keep-alive со сменой sid.
- Текущие proxy9, test_tcp_io, тесты uasync/queue, затем ASAN/UBSan и полный check.sh.
Windows/FreeBSD требуют отдельной проверки poll/select; Linux PASS её не заменяет.
Доказательства исходного анализа
Диагностический test_socks_http_proxy под gdb: PASS, 16 byte-exact загрузок.
Включены существующие proxy/socket debug через gdb, проектные файлы не менялись.
Полный журнал: /tmp/proxy-design-debug.log; события lifecycle: /tmp/proxy-design-lifecycle.log.
Системные вызовы TLS: /tmp/proxy-design-close.744427,
сводка четырёх соединений: /tmp/proxy-design-close-summary.txt.
Эти журналы относятся к реализации до исправлений.
Реализация и проверка архитектуры
1. UTUN_INSTANCE единолично создаёт и уничтожает общие UDP/ICMP-контексты. TCP-модули
владеют только своими bindings/listeners/потоками. Их rollback не затрагивает общие
контексты. Занятой binding не перезаписывается, повторный init TCP отклоняется.
Proxy закрывается и deferred free обрабатываются до остановки групп/транспорта.
2. Все заданные listeners и TUN mappings обязательны. Частичный отказ возвращает NULL
и освобождает созданные ресурсы; create_from_config сохраняет config у вызывающего.
3. ICMP init создаёт контекст/binding, enable_exit атомарно создаёт raw socket/регистрацию.
Отсутствие прав/поддержки отключает только ICMP exit с явным WARN. Клиентские REPLY
сохраняются; остальные ошибки запуска фатальны. Test loopback включается явно.
4. UDP flow публикуется после всех обязательных шагов. Отказы создания/продления таймера
не оставляют бессрочных flows. Проверяются socket/nonblocking/bind/registration,
sendto, ответные выделения/router/TUN. EINTR повторяется, нулевая датаграмма допустима;
ошибки пакета логируются без очереди повторов, сломанный сокет закрывается.
5. TCP HUP сохраняет high/low-water и ручную остановку чтения. Без доступного I/O
регистрация временно снимается; low_water восстанавливает её с новой generation.
fd/очереди сохраняются; запись обслуживается независимо. Ошибка восстановления
регистрации корректно вызывает on_error, включая destroy владельца из callback.
6. Поздний WINDOW отсутствующего sid отбрасывается с DEBUG. Формат протокола и машина
CONNECTED/WINDOW/FIN сохранены. Реальные ошибки записи/SHUT_WR не превращаются в успех.
HTTPS fixture теперь завершает TLS двусторонне; abrupt close — отдельный режим.
7. Неподдержанный enabled в серверных конфигурациях профильных и TUN-тестов заменён
поддержанным connect_timeout. Контракты заголовков и документы модулей обновлены.
Проверено:
- test_proxy_lifecycle: 25/25 изолированных сценариев, контроль fd/памяти/регистраций/таймеров.
Покрыты socket/nonblock/bind/registration/timer/OOM, отправка/EINTR/нулевой UDP,
частичный отказ instance, повторный запуск, ICMP REPLY после EPERM, HUP re-register,
ожидающая запись и destroy из error callback. /tmp/proxy-fix-lifecycle2.log.
- test_tcp_io: 19/19, HUP high_water=4 на epoll/poll, парковка без busy loop,
точная доставка данных и FIN после low_water, ручная пауза и независимая запись.
- SOCKS/HTTP/HTTPS: оба режима по 16 byte-exact загрузок. Штатный TLS shutdown без
EPIPE/ENOTCONN; abrupt сохраняет ошибки TCP без повреждения доставленных ответов.
/tmp/proxy-fix-check1/test_socks_http_proxy.log, /tmp/proxy-fix-tls-abrupt.log.
- Основной make check: 185/185 PASS. /tmp/proxy-fix-full-check2.log.
Первый check.sh остановлен общим лимитом 141с; local_transport_packets отдельно PASS.
Повтор завершил основной набор, но TUN-этапу помешал чужой процесс на UDP 15002.
- Те же TUN fixtures и идентичный src/utun выполнены в отдельном network namespace:
proxy9/9, burst1/1, load8/8 PASS; load проверил134217728 байт.
/tmp/proxy-fix-network-check.log; копия fixtures/логов: /tmp/utun-proxy-fix-network/.
- Настоящий raw ICMP echo до127.0.0.1 через два ETCP-instance и явный loopback PASS.
UTUN_TEST_ICMP_RAW=1 требует raw socket и не переключается на виртуальный ответ.
/tmp/proxy-fix-icmp-raw.log, /tmp/proxy-fix-icmp-loopback.log.
- ASAN/UBSan/LSan: 16/16 PASS с detect_leaks=1, включая оба TLS-режима, lifecycle,
tcp_io/flush, пакеты, regressions, HTTP, TCP/UDP/ICMP, services, uasync/queue и STCP.
Настоящий raw ICMP дополнительно PASS под теми же санитайзерами с root.
Все встроенные зависимости собраны с sanitizer-флагами в отдельной копии текущих
исходников /tmp/utun-proxy-fix-sanitizers; обычные объекты проекта не менялись.
/tmp/proxy-fix-sanitizer-results.log, /tmp/proxy-fix-sanitizer-icmp-raw.log.
В проверенных Linux-сценариях противоречий владения/rollback/half-close не выявлено.
Сборка и runtime Windows/FreeBSD не проверялись; Linux epoll/poll не заменяют эту проверку.
+185 -46
View File
@@ -1,61 +1,200 @@
# Proxy: потоки TCP и датаграммы
# Proxy: TCP, UDP и ICMP
Клиентские входы — TUN (lwIP), SOCKS5 CONNECT и HTTP proxy/CONNECT. Exit открывает
обычный TCP-сокет к назначению. TCP-поток на exit определяется парой `(peer_node_id,
stream_id)`. Клиент принимает ответы и уведомления restart только от настроенного
exit в UTUN-группе. UDP/ICMP-контексты принадлежат конкретному `UTUN_INSTANCE`.
Клиент принимает IPv4 из отдельного TUN через lwIP либо TCP от локальных SOCKS5/HTTP
listeners. Exit открывает обычный IPv4 TCP/UDP или raw ICMP socket. Вся передача proxy
идёт через `etcp_route_send` в `TOPO_GROUP_UTUN`, mode=0. Протокол не имеет слоя
совместимости с прежними форматами.
## TCP
## Сообщения для router и приёмные метаданные
Общие определения находятся в `src/proxy/proxy_protocol.h`. Передаваемый заголовок:
`svc_id:1, command:1, stream_id:4`; поля маршрута добавляет ETCP-router.
Это форматы **сервисного API**, а не полные ETCP wire packets. На отправке первый байт —
svc_id, остальное — payload router. Router callback получает такой заголовок:
- CONNECT (1): IPv4:4 и порт:2 в сетевом порядке.
- CONNECTED (2): exit подтвердил успешный TCP connect. Только после этого клиент
сообщает SOCKS success / HTTP 200 и начинает передавать накопленные данные.
- DATA (3): от 1 до 4096 байт.
- CLOSE (4), ERROR (5): прекращение потока.
- FIN (6): конец одного направления; все предшествующие DATA должны быть переданы.
- WINDOW (7): uint32_t, число дополнительно разрешённых байт.
| Смещение | Размер | Поле |
|---|---|---|
| 0 | 1 | svc_id |
| 1 | 8 | src_node_id |
| 9 | 8 | dst_node_id |
| 17 | 1 | rx_flags |
| 18 | 8 | group_id |
| 26 | N | service payload |
У каждого направления начальное окно 65536 байт. DATA уменьшает окно отправителя
и получателя. Получатель возвращает кредит после записи в локальный TCP-сокет
либо принятия данных ограниченным send-buffer lwIP. Очередь ETCP сама по себе
не является подтверждением потребления данных конечным TCP.
Смещения определены `ROUTER_SVC_*_OFF` в `src/routing_layer/etcp_router.h`.
Заголовок callback имеет размер 26 байт. Сервис освобождает полученные entry/dgram
при любом исходе. На отправке router принимает владение entry/dgram даже при ошибке;
0 означает принятие сообщения router, а не доставку конечному TCP/UDP.
FIN ждёт освобождения исходящей очереди, включая данные, остановленные окном
или backpressure роутера. Закрытие одного направления не прекращает другое.
Окончательное освобождение происходит после обоих FIN и передачи оставшихся данных.
Непереданные управляющие сообщения повторяются таймером. Ошибки и переходы
состояния диагностируются категорией `proxy`; состояние сокетов — `socket`.
TCP-клиент проверяет src_node_id==via_node_id и group_id==TOPO_GROUP_UTUN, включая
пустые уведомления. UDP/ICMP допускают REQUEST при включённом exit, REPLY — только при
наличии клиента и совпадении src_node_id с его via_node_id; отдельной проверки group
в их callback нет. IP, TCP/UDP-порты и ICMP id/seq в proxy API передаются в network
byte order. TCP stream_id и WINDOW credit копируются как host-order uint32_t:
формат не нормализован между архитектурами с разным byte order.
Парсеры сохраняют остаток после greeting/request/HTTP headers. HTTP CONNECT ждёт
полного блока заголовков. Во время DNS/connect чтение ограничено очередью tcp_io;
накопленные HTTP headers/body передаются частями размером DATA.
## TCP и управление окном
Определения — `src/proxy/proxy_protocol.h`.
На отправке: `svc:1, cmd:1, stream_id:4, data` (заголовок 6 байт).
На приёме: router header(26), cmd(1), stream_id(4), data (заголовок 31 байт).
Клиент отправляет в `ETCP_RT_ID_TCP_PROXY_SERVER=0x04`, exit отвечает в
`ETCP_RT_ID_TCP_PROXY_CLIENT=0x08`. На exit поток определяется парой peer_node_id/stream_id;
повторный CONNECT этой пары отклоняется с ERROR.
| Команда | Код | Содержимое после stream_id |
|---|---|---|
| CONNECT | 1 | IPv4:4, порт:2 |
| CONNECTED | 2 | Пусто; exit завершил реальный TCP connect |
| DATA | 3 | 1..4096 байт |
| CLOSE | 4 | Пусто; прекращение потока с дренажом принятой записи на exit |
| ERROR | 5 | Пусто; отмена, на exit немедленно уничтожает TCP и очереди |
| FIN | 6 | Пусто; конец одного направления после всех его DATA |
| WINDOW | 7 | uint32_t дополнительного кредита, ровно 4 байта |
Каждое направление начинает с окна 65536 байт. proxy_flow init оставляет ready=0.
DATA ждёт CONNECTED; SOCKS success и HTTP CONNECT 200 также отправляются только после него.
Локальный TUN TCP handshake выполняет lwIP и сам по себе connect назначения не подтверждает.
При отправке DATA списывается tx_credit, при приёме — rx_credit.
WINDOW допускает только ненулевой кредит без увеличения tx_credit сверх 65536.
Кредит возвращается после потребления локальным TCP: на OS-сокетах после опустошения
write_queue **и write_buf**, в lwIP после принятия байтов ограниченным send-buffer
через tcp_write. Это не подтверждение чтения удалённым приложением.
Для HTTP учитывается число исходных upstream байтов независимо от преобразования
headers/chunks/trailers; кредит возвращается после доставки соответствующего результата в OS TCP.
Backpressure router и окно потока независимы. При ошибке DATA остаётся в очереди/буфере
владельца, регистрируется send-ready waiter и safety-net на 500 мс.
У TCP-клиента с lwIP timer повторяет DATA с force=1, у exit и SOCKS/HTTP — обычный force=0.
Force обходит лимит send_q router, но никогда не окно proxy.
При отсутствии CONNECTED/кредита (-2) отправка ждёт ready/WINDOW, а не только router waiter.
OS TCP сохраняет ограничение read_high_water и явную остановку чтения при HUP.
Если HUP не оставляет работы для чтения/записи, tcp_io временно снимает регистрацию fd;
low_water восстанавливает её с новой generation. Сокет и очереди сохраняются, запись
обслуживается независимо от чтения. Ошибка повторной регистрации закрывает соединение.
## Half-close и удаление потока
Владелец выставляет fin_pending только после отправки всех исходящих DATA.
proxy_flow не видит очередей владельца и сам это условие не проверяет.
FIN не должен ждать записи противоположного направления: обе половины независимы.
Приём FIN ставит FIN-маркер после ранее принятых DATA в OS write_queue; в lwIP FIN
откладывается до дренажа to_lwip и успешного tcp_shutdown.
Окончательное закрытие сырого TCP наступает после обоих FIN и необходимых локальных
очередей/shutdown. Дополнительный обмен CLOSE при штатном half-close не требуется.
На exit CLOSE паузит чтение, отбрасывает непрочитанный read_queue и закрывает сокет
после принятой write_queue; ERROR уничтожает поток сразу. На клиенте CLOSE/ERROR
прекращают поток; HTTP вне туннеля дополнительно проверяет завершённость ответа.
Общий retry повторяет pending CONNECTED → WINDOW → FIN через 5000 tb (500 мс),
используя force=1. CONNECT/CLOSE/ERROR в эту очередь не входят.
Клиентский close_pending может повторяться в poll/free пока поток жив; сервер после
ошибки TCP удаляет поток и делает попытку ERROR. Гарантированного повторения CLOSE/ERROR
после освобождения владельца нет.
Поздний WINDOW отсутствующего потока отбрасывается с debug-диагностикой, без создания потока.
Router close/restart уведомляет сервис entry длиной 26 байт без payload.
Proxy называет это CLOSE_ALL: exit удаляет потоки указанного peer, клиент — все свои
TUN/SOCKS/HTTP потоки к настроенному exit. Это не самостоятельная подкоманда или
9-байтовый пакет на проводе.
## SOCKS5 и HTTP
SOCKS реализует CONNECT к IPv4 или DNS-имени, метод аутентификации 0 (no auth).
IPv6 отклоняется с REP=8; BIND и UDP ASSOCIATE не реализованы.
DNS выполняется асинхронно на ingress, выбирается первая A-запись.
Совмещённые и разделённые greeting/request/data сохраняют непрочитанный хвост.
Ошибка DNS/connect до успеха даёт SOCKS REP=4 либо HTTP 502.
HTTP/1.0–1.1 forward proxy требует absolute http:// URL; upstream получает origin-form,
Host из URL и Via. Обычный запрос отправляется как HTTP/1.1 с Connection: close,
кроме Upgrade. Frontend HTTP/1.1 может сохранять keep-alive: каждый запрос получает
новый stream_id/upstream, pipelining обслуживается последовательно, включая разные цели.
Новый stream начинается после доставки полного ответа и отправки CLOSE/ERROR старого.
Codec проверяет headers до пересылки, поддерживает Content-Length, chunked
с extensions/trailers, промежуточные ответы, HEAD/204/304 и тело до EOF.
Hop-by-hop и proxy authentication поля удаляются, framing/Connection строятся заново.
HTTP/1.0 получает декодированное chunked тело и закрытие. CONNECT ждёт полного блока
headers и реального CONNECTED; согласованный 101 переводит соединение в сырой туннель.
Ранний final response прекращает upload и требует закрытия клиента.
Ошибка до final headers может дать собственный HTTP error; после них закрывает оборванный ответ.
Лимиты: headers/trailers 32 KiB, start line и chunk/trailer line 8 KiB, 512 header fields.
CL+TE, разные Content-Length, obs-fold, управляющие байты и неверные порты отклоняются.
Поддержан только transfer coding chunked; HTTP IPv6-назначения отклоняются с 502.
Headers имеют deadline 30 с, DNS/connect — 15 с, передача/idle/дренаж закрытия — 60 с.
Deadline headers не продлевается каждым байтом. Установленные CONNECT/Upgrade-туннели
используют TCP lifecycle без HTTP deadline. Детали codec — в `src/proxy/http_proxy.h`.
## UDP и ICMP
REQUEST и REPLY различаются явно. Ответ допускается только от настроенного exit.
UDP-ответ восстанавливается с нулевой UDP checksum (допустимо для IPv4), IPv4 checksum
пересчитывается. ICMP checksum учитывает нечётный последний байт без чтения за буфером.
Контексты принадлежат конкретному UTUN_INSTANCE. REQUEST=1, REPLY=2.
На отправке UDP: `svc:1, cmd:1, src_ip:4, src_port:2, dst_ip:4, dst_port:2, payload`.
ICMP: `svc:1, cmd:1, dst_ip:4, orig_src_ip:4, echo_id:2, echo_seq:2, payload`.
Заголовки отправки — 14 байт, приёма — 39. Отдельного sender_node_id в payload нет.
Exit заменяет ICMP id/sequence уникальной среди ожидающих запросов парой и сопоставляет
ответ также с IP назначения. Исходные id/sequence восстанавливаются для клиента.
Это исключает смешивание ping-запросов разных клиентов с одинаковыми исходными ID.
UDP exit создаёт сокет на tuple client_node_id/src_ip/src_port/dst_ip/dst_port,
проверяет источник ответа, меняет src/dst местами в REPLY. Удаляет неактивные flows
при очередной периодической проверке с порогом >60 с. Клиент восстанавливает IPv4 checksum,
UDP checksum=0 (допустимо для IPv4).
Flow публикуется только после socket/nonblocking/bind/read registration и создания
таймера очистки. Ошибка продления общего таймера закрывает оставшиеся flows.
sendto проверяется по точной длине, включая успешный нулевой payload; EINTR повторяется.
EAGAIN/ENOBUFS и ошибки отдельного пакета логируются без очереди повторов; сломанный
сокет удаляется. Ошибки выделения ответа, router send и доставки в TUN диагностируются.
## Проверки и ограничения
ICMP exit использует один raw socket; заменяет id/seq token и сопоставляет
wire_id/wire_seq/IP цели с pending. Исходные id/seq восстанавливаются для клиента.
Порог ожидания >5 с проверяется периодически. Raw reply проходит проверку ICMP checksum.
Нечётный хвост checksum обрабатывается без чтения за буфером.
`icmp_proxy_init` создаёт общий контекст и binding; `icmp_proxy_enable_exit` отдельно
открывает raw socket. EPERM/EACCES/отсутствие поддержки raw отключают только ICMP exit
с WARN; клиентские REPLY сохраняются. Прочие ошибки запуска прерывают создание instance.
Тестовый `test_loopback` включается явно, закрывает raw socket и даёт виртуальные ответы
независимо от прав процесса. Production его не включает.
Pending raw request создаётся только с рабочим таймером истечения срока ожидания.
`test_proxy_packets` проверяет IPv4/UDP/ICMP и payload длиной 0..1501.
`test_tcp_io_flush` проверяет уведомление после последнего синхронного send.
`test_proxy_regressions` использует реальные локальные сокеты и управляемую доставку
ETCP: совместные/раздельные handshake, ранние данные, отложенный CONNECTED, HTTP 502,
большой POST, одинаковые stream ID разных пиров, FIN при закрытом окне, RST и IPv4 options.
Send API принимает payload 0..65507, TUN delivery — 0..65506 из-за однобайтового
префикса в uint16_t длине entry. UDP send требует локальный UDP-контекст, ICMP send
его не требует. Доставка ответа требует клиентский TUN; своего окна/повтора датаграмм нет.
На Linux полный `./check.sh`: 109 passed, 0 failed, 1 skipped; дополнительно прошли
настоящий TUN, full-duplex burst и нагрузка 8 соединений / 128 МиБ.
Windows/FreeBSD в этом прогоне не проверялись.
## Жизненный цикл, диагностика и проверки
SOCKS IPv6 явно отклоняется кодом address type not supported. IPv4-фрагменты
на входе proxy TUN отклоняются с диагностикой; сборка фрагментов не реализована.
Формат TCP дополнен CONNECTED/WINDOW, UDP-команды разделены: клиент и exit необходимо
обновлять вместе; совместимость со старым proxy-протоколом не предусмотрена.
Все операции и callbacks — в uasync владельца. Proxy init запрашивает binding
после router init; client/server имеют разные service IDs. Общие UDP/ICMP создаёт
и уничтожает UTUN_INSTANCE; TCP-модули освобождают только собственные ресурсы.
Контексты нужны для включённого exit либо клиентского TUN; SOCKS/HTTP без TUN их не требуют.
Заданные listeners, TUN mappings и bindings обязательны: ошибка откатывает создание.
Повторный init активного модуля и попытка занять чужой binding отклоняются.
При уничтожении instance TCP-клиент/сервер и общие контексты закрываются до остановки
групп и транспорта; ua обрабатывает отложенные free. Точные контракты — в .h.
Категория proxy: info — подключения/закрытия/транзакции, debug — окна/паузы/повторы,
trace — трафик и секундная диагностика exit TCP-очередей/пулов/OS buffers.
Для поиска проблемы также полезны socket и etcp_route.
Регрессионный тест включает proxy=debug через UTUN_TEST_DEBUG=1.
| Тест | Что проверяет |
|---|---|
| test_http_proxy | HTTP codec, разбиения headers/chunks, framing, лимиты, source credit, Upgrade |
| test_proxy_packets | IPv4/UDP/ICMP, checksum и payload длиной 0..1501 |
| test_proxy_regressions | Handshake/CONNECTED, HTTP pipeline/EOF/deadlines, окна/FIN, peer+sid, reentrancy, TUN options/fragment validation, per-instance UDP/ICMP |
| test_proxy_lifecycle | 25 сценариев отказов/rollback, fd/память/таймеры, владение bindings, ICMP без raw, HUP re-register и destroy из callback |
| test_tcp_proxy_client | Create/destroy с тестовым TUN, без утечек |
| test_tcp_proxy_server | TCP echo через tcp_conn; сам по себе не проверяет протокол exit |
| test_socks_http_proxy | Реальные SOCKS IPv4/domain, HTTP и HTTPS CONNECT, byte-exact загрузки; штатный TLS shutdown либо UTUN_TEST_TLS_ABRUPT=1 |
| test_udp_proxy / test_icmp_proxy | Два instance через ETCP; ICMP: явный test_loopback, UTUN_TEST_ICMP_RAW=1 требует настоящий raw socket |
| test_tcp_io | 19 сценариев, включая HUP high/low-water на epoll/poll, ручную паузу и независимую запись |
| test_tcp_io_flush | Callback после последнего синхронного send |
Проверка 2026-10-24: профильные тесты проходят на Linux. ICMP проверен и через явный
test_loopback, и настоящим raw echo до 127.0.0.1 с правами root. Штатный TLS shutdown проходит без EPIPE/ENOTCONN;
аварийный сохраняет реальные ошибки записи, все 16 загрузок остаются byte-exact.
Тестовые конфиги используют поддержанный connect_timeout. Итоги полного прогона
и sanitizer-проверок — в [proxy_fix_plan.txt](proxy_fix_plan.txt).
Windows/FreeBSD runtime в этом окружении не проверены.
Создание proxy из конфигурации в utun_instance.c исключено под _WIN32.
IPv4-фрагменты на входе proxy TUN отклоняются; сборки фрагментов нет.
Описания модулей: `src/proxy/*_doc.md`; все семь заголовков `src/proxy/*.h` сверены с реализацией.
+201 -15
View File
@@ -22,17 +22,30 @@ utun_instance_destroy(inst);
```
`utun_instance_init()` — сокращение для запуска ядра, UTUN и чата, если чат включён
в конфигурации. Desktop GUI запускает ядро, UTUN и чат явно; TUN создаётся при
`[global] tun_enabled=yes`. Без TUN и NAT запуск чата не требует прав администратора.
в конфигурации. Desktop и Android GUI по умолчанию запускают ядро и чат.
`[gui] utun_enabled=yes` явно добавляет UTUN-сервис; `[global] tun_enabled=yes`
управляет только созданием TUN внутри включённого UTUN. Отсутствующий GUI-флаг означает no.
Без TUN и NAT запуск чата не требует прав администратора.
Desktop подтверждает start только после готовности всех сервисов и uasync poll;
ошибка TUN откатывает запуск и возвращается в GUI. Android запускает ядро и чат без UTUN.
ошибка запрошенного TUN откатывает запуск и возвращается в GUI. Android сохраняет тот же выбор при рестарте.
Повторный start работающего сервиса и повторный stop допустимы. Ошибка старта
сервиса откатывает созданные им ресурсы. `utun_instance_stop()` останавливает цикл
UTUN/чата откатывает созданные им ресурсы; ошибка core start оставляет подготовленные
ресурсы для retry/destroy. `utun_instance_init()` сохраняет ранее запущенные сервисы
при отказе последующего. `utun_instance_stop()` останавливает цикл
приложения; освобождение ядра выполняет `utun_instance_destroy()`.
Create уже подготавливает listeners, топологию/SQLite, router/proxy и мониторинг;
core/UTUN/chat запускаются отдельно. `create_from_str` использует временный файл
в `/tmp`, updater и parse. `create_from_config` передаёт владение config только
при успехе; при отказе config остаётся caller, но node_id мог быть исправлен.
Logger общий для процесса, его изменения не откатываются при отказе создания.
Для TUN в desktop `vibechat.cfg` задаются те же параметры, что и у CLI:
```ini
[gui]
utun_enabled=yes
[global]
tun_enabled=yes
tun_ifname=vibechat
@@ -65,15 +78,60 @@ ConnectivityManager до запроса запуска и передаёт ко
| Владелец | Ресурсы |
|---|---|
| Ядро | Идентичность узла, реестр узлов и групп, SQLite, транспортные сокеты, NCD, маршрутизатор, общая репликация, время, мониторинг |
| UTUN | Группа UTUN, запросы соединений из конфигурации, DATA binding, TUN, системные маршруты, NAT transport |
| Ядро | Идентичность узла, реестр узлов и групп, SQLite, сокеты, NCD и постоянные транспорты `[client]`, маршрутизатор, репликация, время, мониторинг |
| UTUN | Группа UTUN, независимые запросы участия к настроенным пирам, DATA binding, TUN, системные маршруты, NAT transport |
| Чат | CHAT-группы, запросы подключения и join, экземпляры синхронизации каналов, chat API, медиа, личные сообщения, звонки и headless API |
| Группа | Сессии пиров, её маршруты и логические каналы, планировщик подключения и recovery |
| Сессия пира группы | NCD handle с момента CONNECTING, согласованные эпохи, состояние обмена таблицей, очередь отправки |
`[client]` открывает транспорт без вступления в UTUN. CHAT согласуется только при наличии
членства в соответствующем канале. Входящий JOIN отсутствующей группы отклоняется.
UTUN stop закрывает свои групповые запросы, но сохраняет постоянные транспорты ядра.
Запись настроенного пира владеет копией параметров и адресов; DOWN/TIMEOUT сохраняет NCD,
окончательный CLOSED запускает повтор через секунду. Удаление записи отменяет её повтор
и освобождает только её handles. Ошибка подготовки нового списка сохраняет прежний список.
Изменение автосокетов переприменяет сохранённые endpoints также для bootstrap без подписанного NODEINFO.
Полный reload сохраняет набор запущенных сервисов и running; частичный обновляет `[client]` также без UTUN.
Restart нужен при смене негорячих global-настроек (в том числе db_path, control,
TUN/NAT/proxy), подсетей либо server-конфига в automatic режиме. В ручном режиме
сравниваются все параметры endpoint и IPv6 scope; неизменённый port=0 сохраняет
порт ОС. TCP endpoint закрывается вместе с listener и своей TCP-моделью.
Ошибка подготовки сохраняет прежний instance; ошибка создания после полного destroy
возвращает NULL. Частичный reload не атомарен: отказ нового endpoint логируется,
новый config сохраняется для повторной попытки. Caller всегда использует результат reload.
Proxy настраивается при создании ядра. Instance владеет общими UDP/ICMP-контекстами;
TCP client/server владеют только своими bindings, listeners и TCP-потоками.
Общие контексты нужны для exit либо клиентского TUN; SOCKS/HTTP без TUN их не требуют.
Ошибка обязательного listener, TUN mapping или регистрации откатывает создание instance.
Недоступность raw ICMP по правам или поддержке отключает только ICMP exit с WARN;
клиентские ICMP REPLY и TCP/UDP продолжают работать.
`utun_instance_destroy()` закрывает proxy и обрабатывает отложенные free до остановки
UTUN/CHAT-групп и транспорта. Waiters/таймеры снимаются до освобождения их владельцев.
Остановка отдельного TCP proxy-модуля не уничтожает общие UDP/ICMP-контексты.
Команды JOIN из Qt/JNI используют удерживаемый `chat_join_port`: frontend копирует ссылку,
а состояние чата читает только callback в uasync. Close закрывает приём под mutex, отменяет
ожидающие команды и отделяет порт от instance. Ссылка frontend остаётся безопасной после stop;
новый запуск публикует новый порт. Команды старого запуска не обращаются к новому ядру.
При stop чата порт и активная попытка закрываются до ожидания media workers. В принятой команде
есть отдельный request_id интерфейса; сетевой JOIN сопоставляется по peer/group/attempt_id/phase.
Результат публикуется после очистки состояния и собственного NCD handle, поэтому callback может
начать следующий JOIN. Отмена ожидания не откатывает уже сохранённое удалённое членство.
SQLite открывается ядром один раз: `db_path/chats.db`, либо `:memory:`, если путь
не задан. Остановка чата сохраняет каналы и сообщения в этой БД. В варианте
`:memory:` данные сохраняются только до уничтожения ядра.
Caller завершает заимствованные SQLite statements/backups до destroy. Ошибка close
логируется; close_v2 при BUSY откладывает освобождение БД до завершения этих объектов.
UASYNC живёт до конца destroy. Отменённые heap-таймеры могут оставаться в нём
до следующего poll; это не разрешает callback обращаться к освобождённому instance.
Headless chat/call/radio bind задаёт IPv4 literal:port, decimal 0..65535;
port=0 выбирает ОС. Ошибка bind/listener не останавливает чат и не открывает
порт по умолчанию. Stop снимает клиентов с UASYNC, отменяет cleanup/mix-таймеры,
освобождает headless-контексты и кодеки. PCM после 11-байтового заголовка
копируется в выровненный буфер перед Opus.
Работающий общий транспорт сохраняется, пока он нужен хотя бы одному из пиров. Закрытие запроса одной группы
не разрывает соединение, нужное другой группе. При остановке группы отменяются
@@ -97,6 +155,21 @@ KEEP_ALIVE полностью отменяет FIN_WAIT. Ожидание CLOSE
для спящего пира увеличивается до его RX deadline. Сам факт DOWN во время FIN_WAIT
завершает ранее запрошенное мягкое закрытие.
NCD возвращает новый handle после подготовки подписки на все события ETCP и нужных
таймеров. До возврата open новый владелец событий не получает; уже работающий
транспорт доставляет начальный UP через call_soon. Отказ подписки, таймера или
планирования UP возвращает ошибку и сохраняет прежних владельцев и FIN_WAIT.
TIMEOUT не закрывает удерживаемый транспорт: возможен поздний UP, новое open может
начать новый бюджет подключения. CLOSED отменяет таймеры, отсоединяет conn и требует
закрыть оставшийся handle; его можно закрыть после уничтожения ядра.
Операции открытия, обновления линков и закрытия удерживают NCD/ETCP через вложенные
callbacks, включая синхронный callback исходящей очереди при отправке CLOSE.
Более новый вложенный переход прекращает оставшуюся рассылку прежнего события.
Управляющий пакет NCD занимает ровно 10 байт: cmd=0x12, subcmd и node_id little-endian.
Приём проверяет длину, команду и принадлежность отправителя тому же транспорту.
Отказы ресурсов, вложенные события, обновление snapshot и удаление соседних линков
проверяет `test_ncd_faults`; готовый handle, TIMEOUT и сетевой CLOSE/KEEP_ALIVE — `test_node_conn_direct`.
Контроль RX независим от отправки keepalive. Объявленный сон не отключает проверку
жизни: срок от последнего RX равен максимуму согласованного keepalive timeout и
объявленного остатка ACTIVE+SLEEP с запасом normal timeout. Фазовые таймеры и наши
@@ -113,10 +186,92 @@ KEEP_ALIVE полностью отменяет FIN_WAIT. Ожидание CLOSE
Worker резервирует completion до запуска и передаёт его через `uasync_post_reserved`;
периодического polling нет. Destroy выполняется вне callbacks uasync, после join
отменяет pending post и завершает done ровно один раз. Uasync живёт до конца этой очистки.
Результаты отменённых задач не записываются в БД. Это синхронное ожидание: длительное
Подготовленные блоки отменённых задач не регистрируются; учёт назначений подготовки
сохраняется для последующей очистки. Это синхронное ожидание: длительное
транскодирование или распознавание речи может задержать stop; принудительного
прерывания этих библиотек нет.
Владелец принимает не более 16 задач, включая уже завершённые workers до доставки
completion. Отказ запуска/неверные аргументы дают синхронный done с -1, остановка
владельца — с `MEDIA_ASYNC_CANCELLED`; при NULL done только логируется ошибка.
Примитивы `media_async.h` синхронные: отмена задачи не откатывает выполненные ими
действия. `ma_copy_file()` публикует копию внутри worker после проверенных записи и
закрытия temp; вызывающий отвечает за очистку своего результата при отмене.
Подробные файловые контракты, 64-битные размеры и ошибки RNG описаны в `media_async.h`.
Групповой crypto worker хранит один `file_io_temp` прямо в задаче вложения.
Общий валидатор MIME не выполняет I/O: send вызывает его в uasync до создания
задачи/ключа, файловый codec повторяет проверку в worker до открытия файлов.
`encrypted_file_write/read` только готовят закрытый durable temp; worker публикует
его одним rename, completion подтверждает результат в БД/UI. Отмена до completion
не публикует сообщение; cleanup учитывает реально созданные/заменённые файлы.
Ошибка синхронизации каталога после rename означает установленное имя без подтверждённой
durability; отложенный discard уже не удаляет результат. Общей транзакции ФС/БД нет.
До запуска workers и после их завершения recovery очищает собственные stale temp
в `media/decrypted`; общий file_io-реестр сохраняет активные temp всех instance.
`media_delivery_init()` вызывается после подготовки chat core и media_index, пока
общая БД и группы живы; подписка на роли обязательна. Репликация хранит отдельный
контекст и CM handle для каждой пары «CHAT-группа–пир». DOWN отменяет текущий RPC,
сохраняя handle для восстановления CM; TIMEOUT закрывает его перед повторным открытием.
Отдельный ETCP DOWN не означает потерю всех путей CM. До free контекста отменяются
его waiter, pump, watchdog и собственный handle; destroy отменяет также сверку ролей.
Обычный stop сохраняет каталог, его эпоху и RX-курсоры. Потеря локальной роли удаляет
чужие объявления вместе с RX-курсорами только этой группы одной транзакцией;
удаление канала удаляет также собственные объявления. Страница репликации и её
RX-курсор фиксируются одним самостоятельным commit; вложенная внешняя транзакция
не считается сохранённым прогрессом. Wire-контракт находится в `media_delivery_proto.h`.
Перед файловой подготовкой назначение записывается в `media_preparations`. Успешный
`media_index_commit()` регистрирует весь файл и снимает эту запись в одном savepoint;
после ошибки/отмены путь остаётся известен, даже если worker уже опубликовал копию.
Это гарантия записи БД; файловые изменения и внешний SQL COMMIT ей не охватываются.
При обычной ошибке откатывается только регистрация; SQLite может сама отменить всю
транзакцию при тяжёлой ошибке. Общая база берётся из `ui_state.media_base` и повторно
используется после stop/start. Загрузчик не меняет существующую базу.
Входящая загрузка и исходящая подготовка резервируют нормализованный путь в общем
media_delivery до начала записи. Отмена снимает загрузку с индекса, закрывает CM handles
и таймеры, освобождает резервацию; выполняющийся обработчик удерживает сам контекст.
Поэтому отмена и повторный start из progress callback допустимы, а done старой загрузки
вызывается после возврата из progress. Каждая попытка блока имеет отдельные UUID и temp:
поздние пакеты старого держателя/попытки не изменяют новый файл.
Producer и relay-выдачи независимо удерживают файл блока. После отмены producer
непроверенный источник завершается с ошибкой; проверенный может обслужить оставшихся
читателей. Последний владелец удаляет temp. При удалении канала отменяются и producer,
и все его выдачи; перед освобождением выдачи отменяются её waiter, pump и таймер.
DONE выдачи следует только после проверки блока и принятия всех его данных router.
После проверки всех блоков загрузчик собирает отдельный temp, проверяет операции
чтения/записи/close, полный размер и SHA-256, затем атомарно заменяет имя назначения.
Ошибка до публикации сохраняет прежний файл. Затем выполняется регистрация в SQLite:
при её ошибке опубликованный файл остаётся учтён в media_preparations для очистки.
Проверка и сборка блоков пока синхронны в uasync; контракт workers относится к исходящей подготовке.
Локальное удаление канала ждёт его исходящие video/register задачи. Последний
completion продолжает удаление; новые исходящие медиа канала в это время отклоняются,
подготовленные результаты ожидающих отправок не публикуются как новые сообщения.
После завершения задач отменяются загрузки/раздача и удаляются учтённые файлы, включая
назначения неуспешных подготовок. Общие файлы других каналов сохраняются. Очистка файлов
выполняется вне внешней транзакции: SQL-откат не восстанавливает удалённый файл.
При ошибке очистки записи оставшихся файлов сохраняются для повторного вызова;
удаление метаданных канала и неполная файловая очистка диагностируются отдельно.
Низкоуровневый `media_index_delete_channel()` сам workers не останавливает.
Точные ограничения путей и контракт повторной регистрации находятся в `media_index.h`.
Подготовка файлов в `src/video/video.h` выполняет remux подходящих H.264/AAC либо
перекодирование в MP4 H.264/YUV420P и AAC. Worker пишет уникальный соседний временный
файл, завершает декодеры/ресемплер/FIFO/кодеры, проверяет trailer, закрытие и метаданные.
Конечное имя заменяется только успешным completion в uasync. При отмене, включая
отмену уже подготовленного результата до доставки completion, временный файл удаляется,
старое назначение сохраняется. Совпадающие исходник/назначение и файловые алиасы
отклоняются; исходный файл должен оставаться неизменным до done. В процессе занята
одна задача до completion; атомарный захват исключает гонку между экземплярами ядра.
Перед done занятость освобождается, поэтому callback может запустить следующую задачу.
JPEG использует те же правила файлов, но извлекается синхронно в отдельном frontend worker.
Probe также синхронен и не обещает ограниченного времени чтения.
Live media хранится в RS-кольце с общим индексом PTS; каждый локальный handle владеет
отдельным cursor. Пауза player не останавливает сетевой ingest и не закрепляет старые
части против вытеснения. После FIN publisher может снять своё владение, сохранив
@@ -136,9 +291,24 @@ destroy отсоединённого cursor не обращается к store.
и worker до освобождения своего DSP; borrowed handle/RS pointers между потоками не передаются.
Чтение video cursor запрашивает следующий кадр по готовности decoder, независимо от
PTS показа; audio cursor сохраняет временной lookahead. Bridges готовят один ответ
на незавершённый запрос и не читают следующий кадр после получения ответа автоматически.
Android worker держит один рабочий input и один готовый output MediaCodec; свободный
PTS показа; audio cursor сохраняет временной lookahead. Viewers готовят ограниченный батч
на запрос: отдельно задаются число видеокадров, audio lookahead и число всех записей.
Desktop и Android принимают до 16 записей и обрабатывают треки по их готовности.
VIDEO_END/AUDIO_END следуют за последними пакетами своего трека, не ожидая другого трека;
общий END завершает вход, а окно ждёт опустошения обоих выходов. GAP/CLOSED отменяют старый батч.
Получение батча погашает запрос, включая повторные обращения до pull; скрытого чтения следующего батча нет.
Callback готовности только будит desktop worker и не вызывает API ядра/bridge. Stop снимает callback
под mutex viewer до освобождения его контекста; detach будит ожидающие workers до уничтожения manager.
На Android signal принадлежит audio worker и живёт до stream_stop, join video worker и close mixer input;
ядро только уведомляет его. Surface освобождается после join video worker.
В wait не удерживаются mutex bridge/ядра. Расход PCM будит тот же signal, уведомления сливаются без потери пробуждения.
GUI и audio callback будят worker при движении часов/освобождении места; сна между готовыми пакетами нет.
Каждый reader имеет отдельные audio/control и video workers. Audio worker читает батчи и готовит
до 250ms PCM через Opus/SoundTouch, не удерживая блокировку consumer. Запас пополняется при наличии
данных без обязательного prefill. SPSC передаёт PCM вместе с отображением исходных PTS; mixer/audio
callback только копирует и смешивает. Короткая блокировка защищает output map/clock и lifetime,
в ней нет декодирования, time-stretch или MediaCodec/FFmpeg. EOF продолжает prepare до опустошения DSP.
Android video worker получает ограниченный mailbox (два кадра) и держит один рабочий input и один готовый output MediaCodec; свободный
input buffer получает лишь для уже готовых данных и сразу возвращает codec.
Первый decoded frame разрешает движение clock/audio. Будущий output ждёт PTS и
приостанавливает новые video requests, сохраняя независимую обработку audio/control.
@@ -146,8 +316,11 @@ input buffer получает лишь для уже готовых данных
первого аудиопакета, сохраняя это основание неизменным. Opus/SoundTouch получает PCM до
запуска audio clock; неподвижные часы не разрешают читать дальше этого ограниченного окна.
PTS поданных кадров служат диагностике/проверке запаздывания, а не лимиту заполнения codec.
Смена revision заменяет только video decoder и его измеренный защитный guard;
Смена revision заменяет только video decoder и его диагностические измерения;
GAP/EXPIRED начинают новую точку KEY. Нехватка сети сохраняет decoder references.
Reset увеличивает video generation; результаты прежнего поколения не публикуются. Video worker
после EOF остаётся доступен для reset до явного stop. Audio clock после старта не ждёт видео:
просроченные изображения декодируются для сохранения references, но не показываются.
Desktop FFmpeg держит один рабочий AVPacket и два готовых изображения; EAGAIN оставляет
packet до следующего прохода. FIN подаёт EOS и дренирует выход до decoder EOF;
конец GUI требует также показа оставшихся изображений и завершения audio tail.
@@ -187,10 +360,16 @@ Microphone lease публикации исключает call/recorder; раци
Если первой работала рация, публикация заменяет профиль захвата своим; после остановки публикации
оставшаяся рация продолжает тот же захват. Recovery останавливает callbacks, workers TX и один раз
открывает общий вход; RX рации сохраняется. Каждый потребитель обрабатывает собственную копию PCM.
На Android `MicrophoneCapture` владеет AudioRecord и reader-потоком, подписчики — ограниченными очередями.
Stop публикации удаляет её подписку до join Opus worker и FIN; AudioRecord остаётся живым для рации.
PTT и PCM рации проходят через одну очередь. Последний подписчик делает stop/read join/release;
приоритет глобального AudioManager сохраняется до освобождения трансляционного захвата.
На Android `MediaAudioController` владеет общим выбором устройства, AudioManager и одним аппаратным reader.
Маршрутизация использует `CallAudioRouter`; AudioTrack рации и микшера просмотра регистрируются у одного владельца.
`MicrophoneCapture` раздаёт независимые ограниченные очереди постоянного PCM 48k stereo; моно апмиксуется.
После подключения публикации встроенный микрофон использует CAMCORDER, сохраняемый до последней подписки.
Смена профиля заменяет только аппаратный AudioRecord: камера, видеокодер и source_epoch продолжают работать.
Общая сетка по capture timestamps имеет запас 200ms; пропуски заполняются тишиной, старый PCM не переносится вперёд.
Stop публикации удаляет её подписку до join Opus worker и FIN, сохраняя захват для оставшейся рации.
PTT и PCM рации проходят через одну очередь. Последняя подписка завершает общий reader;
контроллер закрывает аппаратный вход, сохраняя выходы. Перед Telecom все владельцы снимаются,
а `MediaAudioController.awaitIdle()` дожидается освобождения входа и глобального маршрута.
Ограничения фонового захвата рации, её разрешения и foreground-сервис продолжают действовать.
Fatal ошибка audio worker передаётся в GUI с session ID и завершает всю публикацию;
откат видеоконфигурации применяется только к video/capture. Поздний отказ старого publisher
@@ -291,3 +470,10 @@ standby даже при начальном `client_activity=STANDBY`. Keepalive
handshake. Пробуждение по таймеру и внешней сети сначала сообщает ACTIVE-окно
пирам, затем запускает ожидающие задачи. При выходе сначала восстанавливаются
линки; deinit отменяет ожидающие задачи и очищает подписки старого ядра.
Activity/peer-sleep подписки уникальны по fn+arg и вызываются синхронно в обратном
порядке добавления. Remove себя/соседа безопасен: память записи освобождается
после внешней рассылки. Новые подписки могут получить вложенное событие;
более новая эффективная activity прерывает остаток прежней рассылки.
Destroy/reload из этих callbacks запрещён. `test_instance_contract` проверяет
вложенные подписки, отказы ресурсов, manual/full reload, TCP listener и headless stop/start.
+260 -60
View File
@@ -1,5 +1,17 @@
Модуль стриминга. Техническое задание, этап 1.
Статус аудита 26.10.2026: сверка завершена только для stream_proto.h,
stream_block.h, stream_source.h и stream_media.h; остальные разделы проверены частично.
Расхождение допуска deleted устранено: stream_member_load читает принятый флаг
в том же SELECT, что и профиль, и возвращает STREAM_ERR_NOT_MEMBER до crypto.
Штатный test_stream_member проверяет подписи/версии, source/supplier/self,
закрытие handles/выдач, изоляцию каналов и повторное разрешение участия.
Полный аудит оставшихся разделов ещё не завершён. Подтверждения, архитектура
исправления и результаты проверок — doc/stream_audit_findings.txt.
Дефект publisher_block устранён: помещающийся Opus уходит в отдельную аудиопачку,
ожидающее видео получает следующий слот actor. Штатный test_stream_publisher
проверяет 23 сценария границ, продвижения треков и OPENING/FIN; раздел 7 отчёта.
1. Цель и принятые решения
Модуль размещается в src/stream. Узлы получают поток данных от источника через других подписчиков группы,
@@ -8,7 +20,7 @@
Принятые решения:
- Транспорт — существующий ETCP с быстрыми переповторами, мультилинком, шифрованием и управлением нагрузкой.
- Параметры FEC задаёт источник; 6+2 — значение по умолчанию, а не ограничение протокола.
- Параметры FEC задаёт источник; 8+0 — значение по умолчанию, а не ограничение протокола.
- Источник подписывает всю пачку один раз до RS; подпись восстанавливается вместе с содержимым и проверяется после decode.
- В источник добавляется циклический timestamp uint16_t в миллисекундах; он сохраняется при всей ретрансляции.
Задержка оценивается относительно предыдущих данных по дельтам timestamp и времени прихода, включая полную остановку.
@@ -68,7 +80,7 @@ INFO/SUBSCRIBE/REVERSE_REQ к неподписанному узлу получа
Источник формирует пачки и кодирует их lib/reed_solomon:
- n — полное число субпотоков, r — число проверочных, k = n-r — число исходных.
- По умолчанию n=8, r=2, k=6. Любые k кодограмм одной пачки восстанавливают её содержимое.
- По умолчанию n=8, r=0, k=8. Любые k кодограмм одной пачки восстанавливают её содержимое.
- subblock от 0 до n-1 определяет субпоток; первые k блоков систематические, остальные проверочные.
- batch_seq — общий 64-битный индекс пачки; блок субпотока X определяется парой (batch_seq, X).
Позиция N в запросе — batch_seq, а не байтовое смещение или отдельный счётчик субпотока.
@@ -78,7 +90,7 @@ INFO/SUBSCRIBE/REVERSE_REQ к неподписанному узлу получа
Дескриптор содержит ключ трансляции, n, r и максимальный размер RS-кодограммы block_size.
Параметры задаёт только источник и не меняет в пределах source_epoch; изменение создаёт новый запуск.
В настройках desktop/Android раздел «Трансляция» сохраняет общее число субблоков и число избыточных RS.
В [chatserver] это stream_subblocks (1..255, по умолчанию 8) и stream_rs_redundant (0..n-1, по умолчанию 2).
В [chatserver] это stream_subblocks (1..255, по умолчанию 8) и stream_rs_redundant (0..n-1, по умолчанию 0).
Медиапубликатор читает параметры при запуске и один раз выделяет рабочий буфер по ёмкости пачки,
но не больше предела медиаблока 65536 байт. Изменение настроек действует только со следующего запуска.
Дескриптор подтверждается подписанной опцией источника; поля FEC также входят в подписанную пачку.
@@ -87,9 +99,9 @@ INFO/SUBSCRIBE/REVERSE_REQ к неподписанному узлу получа
Максимальный размер части субпотока по умолчанию — 1300 байт: внешние метаданные 47 и RS до 1253 байт.
Внутри RS находятся подписанные метаданные 55 байт, содержимое и одна 64-байтная подпись.
block_size включает 9-байтный заголовок RS seq/subblock. Максимальная длина содержимого при 6+2 — 7345 байт.
block_size включает 9-байтный заголовок RS seq/subblock. Максимальная длина содержимого при 8+0 — 9833 байт.
При длине содержимого L фактический RS-size = 9 + ceil((55 + L + 64)/k), полный размер части = 47 + RS-size.
Например, L=1000 даёт восемь частей по 243 байта; L=7345 даёт восемь частей по 1300 байт.
Например, при 8+0 L=1000 даёт восемь частей по 196 байт; L=9833 даёт восемь частей по 1300 байт.
Один контекст RS принимает пачки разной длины без пересоздания матрицы; все части одной пачки равны по размеру.
Handle получателя, поколение stream-сессии, изменяемый путь и служебные заголовки ETCP идут сверх этого бюджета.
Не обещаем соответствия одной кодограммы одному UDP-пакету: необходимую фрагментацию выполняет ETCP normalizer.
@@ -396,21 +408,25 @@ A — поступившая нагрузка, D — передано в ETCP, X
в интервале без смены выдач g = max(0, (A-D)/elapsed_seconds).
Rate выдачи — её средний вклад в поступающую нагрузку, а не урезанная скорость выхода из перегруженного канала.
Для разгрузки нужно снять суммарный rate больше g, чтобы очередь начала убывать.
Число выдач выбирается по их rates: закрываем достаточно выдач для требуемого сокращения;
при сильном росте это сразу несколько, не фиксированная одна и не случайное постоянное число.
Число выдач оценивается по их rates, но закрывать можно только избыточные части сверх k.
Недостаточная полоса без избыточных частей требует пропуска устаревших данных, а не разрушения FEC-набора.
OVERLOAD содержит request_id, расчётное число выдач, требуемое сокращение rate и список handles с фиксированными rates.
Получатель выбирает handles с учётом FEC и альтернативных поставщиков и отправляет OVERLOAD_SELECT.
Получатель выбирает handles только при сохранении k различных работающих частей и отправляет OVERLOAD_SELECT.
Альтернатива должна уже получать DATA с действительными метриками без остановки; ожидающий INFO не является заменой.
Одновременный выбор для других перегруженных пиров тоже исключается из доступных альтернатив.
Отправитель исполняет выбор, только пока актуален исходный request_id, и сообщает CLOSED.
Получатель не отсоединяет выбранные handles до CLOSED: поздний SELECT после принудительного закрытия не режет дополнительные выдачи.
При разных rates одного количества закрытий недостаточно: суммарный снятый rate должен покрыть требование.
Получатель не отсоединяет выбранные handles до CLOSED; поздний SELECT после завершения запроса не меняет выдачи.
При разных rates снятая нагрузка считается по сумме rates; безопасность FEC важнее полного исполнения требования.
Rate закрываемых выдач фиксируется по оценке на момент запроса, чтобы условие исполнения не менялось на каждом тике.
Срок реакции — 2 секунды с первого запроса; повторы не сбрасывают срок и не начинают новое требование.
Новые выдачи, компенсирующие запрошенное сокращение, до разгрузки не принимаются.
Если требование не выполнено, отправитель сам закрывает достаточный остаток выдач и сообщает handles/причину.
Далее снова измеряется рост; автоматический выбор учитывает освобождаемую нагрузку, а при равенстве допустима случайность.
Если очередь опустела, запрос завершается сразу. По истечении 2 секунд отправитель завершает запрос без слепых закрытий.
Далее снова измеряется рост; автоматический выбор учитывает освобождаемую нагрузку.
При глобальном отставании сокращение выбирается по всем пирам модуля.
Память и возраст очереди остаются ограниченными во время ожидания; просроченные задания удаляются с GAP/EXPIRED.
Возраст ожидающих DATA используется для диагностики перегрузки, но не разрешает пропуск данных.
Backpressure сохраняет курсоры и непрерывную цепочку кадров. GAP обозначает фактическое вытеснение/утрату диапазона.
Принятые ETCP пакеты и подписки сохраняются. Завершённый источник дочитывается до подписанного END.
13. Статусы через опции мембера и Merkle
@@ -502,7 +518,7 @@ Stop/destroy выполняются по правилам ядра, вне callb
- Перекрытие поставщиков без дублирования потребителю, rollback, одновременное восстановление нескольких субпотоков.
- Цикл по устаревшим метрикам, одновременная перестройка и выдача сохранённых путей: DATA не циркулирует.
- Глитч короче подтверждения, устойчивое отставание 500 мс, нулевой выход и локальная перегрузка uasync.
- Выбор числа закрытий по g/rates, разные rates, повтор OVERLOAD, частичное выполнение и закрытие остатка через 2 секунды.
- Выбор безопасных закрытий по g/rates, разные rates, повтор OVERLOAD, частичное выполнение и сохранение k после 2 секунд.
- 32 совместимых кандидата на старте, далее 1/с, маленький/большой список и запрет повторного полного цикла раньше 30 с.
- Merkle-опции до/после BGP-события, офлайн-опция не кандидат, поздний участник, перезапуск и сохранение других полей userinfo.
- Мобильный источник/получатель и исключение мобильных из фоновых проб ретрансляторов.
@@ -592,13 +608,14 @@ Stop/destroy выполняются по правилам ядра, вне callb
Rates фиксируются при создании OVERLOAD. Расчётное число — подсказка, исполнение проверяется по суммарному rate.
Получатель предпочитает закрытия, сохраняющие k с учётом других поставщиков, затем parity при равных условиях.
OVERLOAD_SELECT предлагает handles; source закрывает только актуальное требование, поздний/повторный выбор безопасен.
Срок 2с начинается с первого запроса и не меняется при повторе. Невыполненный остаток source закрывает сам.
Срок 2с начинается с первого запроса и не меняется при повторе. С 25.10 остаток без безопасного SELECT не закрывается.
Новые SUBSCRIBE не компенсируют разгрузку: запрет действует до исполнения и 500мс снятого давления;
stream_peer_get_load.accepting сообщает этот статус отдельно от request_active.
Глобальный бюджет включается только для backlog нескольких пиров вместе с CPU/опозданием callbacks;
CPU отдельно не отключает выдачи, уже запрошенное сокращение повторно не учитывается.
Control FIFO ограничена 128 кодограммами по 1094 байта, возрастом 4с и отменой неподтверждённых запросов.
Просроченные ожидающие DATA при активном OVERLOAD учитываются X и пропускаются явным GAP, кольцо не стирается.
Удаление DATA только по возрасту отменено: оно разрезало KEY и вызывало постоянные resync при недостаточной полосе.
GAP сообщает фактическую потерю диапазона; после END ожидающие DATA дочитываются.
Диагностика сообщает A/D/X/Q, age, input/output/g, длительность интервала, cpu/late callbacks,
frozen/released rates, request/deadline и нижние очереди/inflight; D остаётся принятием ETCP, а не доставкой.
test_stream_core: баланс, X отдельно от D, граница 500мс, глитч, смена состава, 10-секундное окно и широкие wire-метрики.
@@ -666,26 +683,30 @@ Stop/destroy выполняются по правилам ядра, вне callb
Все stream-числа на wire — big-endian, без packed-структур и зависимости от host byte order.
Версия stream wire — 4; обратная совместимость не поддерживается.
Заголовок raw (52 байта, version=5): cmd:1, version:1, command:1, subblock:1,
Заголовок raw (52 байта, version=7): cmd:1, version:1, command:1, subblock:1,
group_id:8, source_node_id:8, source_epoch:8, sender_epoch:8, receiver_epoch:8, handle:8.
INFO_REQ/RESP согласуют sender/receiver epochs; receiver_epoch=0 используется INFO_REQ для обнаружения поколения и REVERSE_REQ.
Подписанный routed payload содержит тот же заголовок; его group_id сверяется с реальной группой router,
sender берётся из end-to-end router metadata. DATA через routed service отвергается.
INFO_REQ: probe_nonce:8, не резервирует номер SUBSCRIBE.
INFO_REQ: probe_nonce:8, want_media:1 (0/1), не резервирует номер SUBSCRIBE.
want_media=1 просит поставщика подготовить общий медиаиндекс; поиск KEY выполняется отложенно с ограниченным бюджетом.
INFO_RESP/ACCEPT: probe_nonce:8 (0 в ACCEPT), actual_seq:8, first_available:8, last_available:8, n:1, r:1, block_size:2,
metric_flags:1, lag_ms:8, source_rate:8, receive_rate:8, blocks:8, late_blocks:8, stops:8,
offer_flags:1, free_slots:2, cpu_basis_points:2, queue_bytes:8, progress_age_ms:8, path_rtt_ms:4,
path_count:1, node_ids:8*path_count — фиксированная часть 111 байт. В INFO actual_seq — последний доступный, в ACCEPT — старт.
key_seq:8, key_last_seq:8, path_count:1, node_ids:8*path_count — фиксированная часть 127 байт. В INFO actual_seq — последний доступный, в ACCEPT — старт.
metric_flags: bit0 — измерение известно, bit1 — source_rate известен, bit2 — receive_rate известен, bit3 — stopped.
offer_flags: bit0 — принимает новые выдачи, bit1 — RTT цепочки известен, bit2 — возраст прогресса известен,
bit3 — CPU известен, bit4 — кольцо непусто для этого субпотока. CPU — 0..10000 (сотые доли процента).
bit3 — CPU известен, bit4 — кольцо непусто для этого субпотока, bit5 — полный KEY сохранён для выдачи этой части.
При bit5=0 обе позиции KEY равны UINT64_MAX; при bit5=1 first_available <= key_seq <= key_last_seq <= last_available.
Каждая часть диапазона KEY проверяется на наличие; свежий незавершённый KEY не заменяет предыдущий полный.
CPU — 0..10000 (сотые доли процента).
Пустой источник: first/last=UINT64_MAX, bit4=0, INFO actual_seq=UINT64_MAX, ACCEPT actual_seq=0, путь {source}.
Курсор last+1 допустим только в ACCEPT непустого кольца; DATA/END проверяются по точной позиции и подписи источника.
SUBSCRIBE: requested_seq:8; REJECT: reason:1, для SUBSCRIBE дополнительно first_available:8,last_available:8,
для INFO дополнительно probe_nonce:8. Старый nonce не подтверждает новую пробу.
RENEW: один header handle без payload (каждая кодограмма продлевает один элемент списка).
UNSUBSCRIBE/CLOSED: reason:1. DATA: path_count:1,node_ids:8*path_count,неизменяемая часть RS-пачки переменной длины.
UNSUBSCRIBE/CLOSED: reason:1. DATA: path_count:1,node_ids:8*path_count,frame_timing:48,неизменяемая часть RS-пачки переменной длины.
GAP: old_cursor:8,next_cursor:8, уведомление включает диапазон [old_cursor,next_cursor).
END: "stream_end":10,group_id:8,source_node_id:8,source_epoch:8,last_seq:8,Ed25519:64 — всего 106 байт.
UINT64_MAX в END означает пустой источник; принимается только выдачей с next=0 и без ранее принятых DATA.
@@ -743,7 +764,7 @@ Restart в той же source_epoch сохраняет consumer cursor: bootstra
смешивание отвергается до изменения данных. stream_publish_finish не освобождает буфер источника.
Медиаблок uAV2 находится целиком внутри входа RS, поэтому header восстанавливается вместе с данными.
При штатных 6+2 полный медиаблок <=7345 байт. Раскладка big-endian, без packed-структур:
При штатных 8+0 полный медиаблок <=9833 байт. Раскладка big-endian, без packed-структур:
fixed header (32 байта):
magic="uAV2":4, video_codec:1, audio_codec:1, video_count:1, audio_count:1,
@@ -811,13 +832,15 @@ Android:
Разделённые CODEC_CONFIG buffers собираются отдельно с пределом 64KiB до разбора конфигурации.
- Camera MediaCodec Surface timestamps и AudioTimestamp.TIMEBASE_MONOTONIC приводятся к общему
origin System.nanoTime; время callback/получения блока не заменяет capture PTS.
- Микрофон трансляции использует AudioRecord.CAMCORDER, 48kHz stereo. Перед Opus AUDIO/128kbps
- Встроенный микрофон трансляции использует AudioRecord.CAMCORDER, 48kHz stereo (mono fallback при отказе формата).
Гарнитуры используют разговорный тракт; mono приводится к stereo перед Opus AUDIO/128kbps. Перед кодированием
применяется тот же audio_compressor, что в TX рации: общие настройки включения, max_gain и rise_rate,
lookback=200ms, lookahead=0, release=rise*5, target=0.25. История и gain принадлежат только audio worker
трансляции; оба канала используют общий gain, сохраняющий стереобаланс. Изменения настроек рации применяются на следующем PCM-кадре, без перезапуска микрофона,
Opus или source_epoch; compressor не добавляет задержку и не меняет capture PTS.
- Camera/codec callbacks — HandlerThread, Opus encoder — audio capture thread, decoder — отдельный viewer worker,
общий AudioTrack — worker StreamAudioMixer. Перекладка плиток сохраняет TextureView/decoder.
- Camera/codec callbacks — HandlerThread, Opus encoder — audio capture thread. Каждый viewer имеет
audio/control worker и независимый video worker; общий AudioTrack — worker StreamAudioMixer.
Перекладка плиток сохраняет TextureView/decoder.
- Native bridge использует общий stream_publisher для TX и stream_viewers для RX.
stream_publisher держит pthread mutex, ограниченные очереди и один pending post либо media timer;
статистика обслуживается отдельным таймером 100ms. Тот же модуль используется desktop.
@@ -826,22 +849,39 @@ Android:
UI не сохраняет borrowed inst/ua. Idle без pending data не будит мобильное ядро.
- TX<=2MiB/128 records, не более 250ms ожидания. Actor публикует <=2 пачек за вызов,
неполную пачку объединяет с аудио/видео до 20ms; продолжение работы через timer 5ms.
Opus целиком занимает header32 + audio descriptor16 + payload; точное заполнение допустимо.
Без ожидающего видео резерв под video descriptor отсутствует. Обычная смешанная пачка
сохраняет место под описание и непустой фрагмент видео. Если первый Opus помещается только отдельно,
второй слот того же actor отдаётся видео без ожидания сборки неполной пачки.
feed=1 означает копирование в очередь. Actor после определения FEC-ёмкости при open удаляет Opus,
не помещающийся даже отдельно, с WARN (ID, PTS, bytes, max, capacity), не блокируя следующие записи.
При video pressure отбрасываются целые ожидающие кадры, P-кадры запрещены до нового KEY.
Кодировщик получает отказ admission и запрашивает KEY с ограничением частоты 500ms.
- RX: stream_viewers держит отдельный handle/cursor, demand, status и один owned ответ на session ID.
- RX: stream_viewers держит отдельный handle/cursor, demand, status и ограниченный owned батч на session ID.
Android принимает до 16 записей: максимум один новый видеокадр и звук в пределах собственного lookahead.
Общей RX FIFO нет: остановка/отставание одного reader не задерживает остальных.
Получение ответа и сетевые AVAILABLE не запускают автоматическое чтение следующего кадра.
Запас остаётся в RS-кольце. want_video запрашивает следующий полный видеокадр независимо от часов показа;
audio_ahead ограничивает только звук относительно PTS потребителя. Управление/звук обслуживаются независимо от video pressure.
StreamVideoPlayback владеет MediaCodec и принимает mailbox до двух кадров от audio/control worker.
StreamDecodePump держит один рабочий кодированный кадр и один ожидающий output MediaCodec.
Decoder заполняет свой внутренний конвейер до первого выхода без ограничений по числу поданных кадров или video lookahead.
Перед каждым входом проверяется выход; свободный input buffer берётся только для уже готового рабочего кадра.
Будущий output ждёт своего PTS и приостанавливает новые video requests; уже запрошенный ответ помещается в рабочий input.
Низкая задержка запрашивается через KEY_LOW_LATENCY при заявленной поддержке, но корректность не зависит от этого режима.
За проход обслуживаются до четырёх media replies, до восьми output buffers за один вызов pump.
За проход обрабатываются до 32 готовых записей, до восьми output buffers за один вызов pump.
Готовый батч и расход PCM будят worker через отдельную condition variable, без JVM callbacks из ядра.
Video worker проверяет MediaCodec каждые 5ms при незавершённой работе, иначе ждёт до 50ms;
audio worker будят media, расход PCM и изменение video state. Без звука его wall clock обновляется каждые 5ms.
Между готовыми записями sleep нет. Audio worker владеет signal до stream_stop, join video worker
и отсоединения своего mixer input; detach ядра будит signal, не освобождая его.
При обычной нехватке данных decoder references сохраняются; GAP/вытеснение требуют нового KEY.
Просроченный кадр при нормальной последовательности декодируется без показа.
- Фиксированного prebuffer нет. Начало clock/audio output разрешено после первого декодированного кадра.
- Первый декодированный кадр разрешает audio output; общий PCM-модуль desktop/Android перед первым звуком
и после underrun накапливает 250ms готового PCM. Это порог готового звука, без аппаратной задержки и lookahead DSP.
Во время накопления backend выдаёт silence с неподвижным исходным PTS; decoder references сохраняются.
Уже переданный устройству PCM продолжает воспроизводиться. До возобновления регулятор держит темп 1x;
затем избыток сокращается по обычной истории минимального запаса. EOF снимает порог и выпускает весь короткий хвост.
AudioTrack задаёт время показа видео по реально услышанному PCM;
без аудио используются монотонные часы с остановкой на границе готовых кадров.
Android StreamAudioOutput принадлежит StreamAudioMixer и хранит один недописанный общий PCM (до 480 stereo frames).
@@ -851,13 +891,14 @@ Android:
Заполнение поддерживается до фактического bufferSizeInFrames устройства; API>=31 задаёт start threshold=1.
При EOF недописанный PCM передаётся полностью; mixer выдаёт тишину после последнего кадра источника
до его физического воспроизведения. END одного input не закрывает AudioTrack остальных.
Hold/starvation одного источника добавляет silence span только в его native timeline, не двигая его PTS.
Starvation одного источника добавляет silence span только в его native timeline, не двигая его PTS.
Видео не удерживает звук после старта; накопившийся из-за тишины избыток погашается ускорением.
GAP создаёт новый input/origin, сохраняя общее устройство и часы остальных трансляций.
Watchdog 2s проверяет отсутствие движения hardware head только при достаточном заполнении либо заблокированной записи;
намеренная пауза и ожидание данных до порога запуска не считаются зависанием. Ошибка содержит состояние и счётчики устройства.
Защитный guard минутного контроллера учитывает audio device lead и наблюдённое опережение decoder
(максимум last_input_PTS-output_PTS текущей revision плюс период кадра). Drain не обнуляет этот guard;
новый decoder начинает измерение заново. Отдельного контроллера задержки декодера нет.
Защитный guard аудиоконтроллера — максимум 40ms и audio device lead + 20ms, плюс
внутренний lookahead SoundTouch (SETTING_INITIAL_LATENCY при текущем темпе).
Наблюдённое опережение decoder остаётся диагностикой и не увеличивает задержку звука.
Отсутствие output progress 10s при наличии работы даёт ERROR; сетевые паузы и ожидание PTS допустимы.
Состояния ожидания KEY и decoder разделены; логи показывают inputs/decoded/shown, рабочие slots, inflight и decoderLeadUs.
Короткие audio gaps заполняются Opus PLC, большой разрыв сбрасывает timeline.
@@ -925,17 +966,20 @@ Android:
Это ограниченный drain, не подтверждение воспроизведения всеми viewers. Abort/core restart очищает очередь сразу.
Завершение логирует срок drain, оставшиеся RS-байты выдач и локальных readers; RS=0 не означает подтверждение доставки ETCP.
Поздние команды старого session ID не меняют новый запуск.
- Рация работает одновременно с публикацией. При совпадении выбранного входа она получает копию
PCM CAMCORDER 48kHz stereo с исходными timestamp; её source/stereo предпочтения не меняют трансляцию.
Другой явно выбранный вход открывается отдельно; отказ не заменяет его системным default.
Трансляция имеет приоритет глобального AudioManager: рация не включает разговорный режим и не меняет SCO
общего тракта. Перед запуском трансляции ранее работавшая рация освобождает capture и затем подключается
к фактическому входу трансляции либо повторно открывает свой отдельный вход.
Очереди потребителей ограничены; PTT упорядочен с PCM, разрыв сбрасывает историю детектора рации.
Последняя подписка закрывает AudioRecord; stop рации/трансляции не закрывает захват другого потребителя.
Проверяется фактический Android route и системное silencing: конфликтующий отдельный capture рации
завершается с диагностикой, приём рации сохраняется. Окно VAD/экран/разрешения не обходятся общим захватом.
- Рация, публикация и просмотр трансляций используют общий профиль MediaAudioController: AUTO/PHONE/WIRED/BLUETOOTH.
Выбор из любого экрана меняет общий вход и маршруты всех активных AudioTrack; настройка сохраняется одна.
Аппаратный микрофон один, очереди потребителей и обработка PCM независимы. PTT упорядочен с PCM.
Подключение публикации переводит встроенный вход в CAMCORDER stereo; профиль сохраняется до последней подписки.
Смена устройства сохраняет подписки, камеру, видеокодер, source_epoch и временную шкалу эфира.
MediaCaptureTimeline выдаёт 20ms PCM в общей сетке с запасом 200ms; аппаратные timestamps задают положение звука.
Отсутствующий PCM заменяется тишиной. Запоздавшие данные отбрасываются, а не получают новые PTS.
Подтверждаются фактический маршрут, свежий PCM и отсутствие системного silencing. При ошибке — диагностика
и резервный телефонный профиль; при полном отказе входа публикация продолжает видео с тишиной.
Последний потребитель закрывает вход; выходы живут до удаления своих AudioTrack. Просмотр не открывает микрофон.
Окно VAD/экран/разрешения не обходятся общим захватом. Ошибки видны в общем меню выбора устройства.
Звонок и voice recording по-прежнему исключают публикацию в обе стороны.
Аппаратная проверка (debug APK, RECORD_AUDIO разрешён):
adb shell am instrument -w com.utun.chat/com.utun.chat.data.MediaAudioProbe
Звук трансляции и рация (Android и desktop):
- Во время публикации с микрофоном звук можно выключить и включить. Заглушается только копия PCM
@@ -1026,23 +1070,41 @@ Desktop:
в uasync. Протокол uAV2 и wire v5 при добавлении desktop publisher не меняются.
- Одиночный StreamPlayerWindow либо StreamGridWindow с отдельным player на источник.
Player содержит имя, состояние и громкость; fullscreen/close в сетке принадлежат общему окну.
- transport/stream_bridge владеет stream_viewers: независимый cursor и один ближайший ответ на reader.
- transport/stream_bridge владеет stream_viewers: независимый cursor и ближайший батч на reader.
Attach/detach выполняются в uasync; команды и копирование медиаданных защищены mutex.
Post отменяется до destroy ядра; GUI/decoder не хранят заимствованные instance/handle pointers.
Запрос задаёт число видеокадров, audio lookahead и максимум 16 записей. Проход reader ограничен 2ms/256KiB;
последняя целая запись может пересечь порог байтов, но один кадр не превышает 2MiB.
При WAIT/MORE готовая часть отдаётся сразу. Получение батча погашает запрос, включая его повторы до pull.
Callback готовности будит worker через condition variable; GUI/audio также будят его при освобождении места.
Stop снимает callback до уничтожения worker; detach будит ожидание перед освобождением manager.
- stream_media_cursor собирает ближайший access unit выбранного кодека и выдаёт Opus 48kHz mono/stereo/20ms из RS-кольца.
Android AudioTrack и desktop output работают в stereo; mono-пакеты upmix выполняет libopus.
FFmpeg/Opus декодируются отдельным worker; Qt-очередь не накапливает кадры.
Worker держит один рабочий AVPacket и не более двух готовых изображений. Следующий video request не ограничен часами показа.
Opus/SoundTouch и FFmpeg декодируются разными workers каждого player; Qt-очередь не накапливает кадры.
Video worker держит один рабочий AVPacket и не более двух готовых изображений; audio worker передаёт ему
ближайшие кодированные кадры через ограниченную очередь. Следующий video request не ограничен часами показа.
Бюджет нового видео учитывает AVPacket, готовые изображения и оставшиеся кодированные кадры батча.
Записи аудио/видео расходуются независимо по готовности потребителя; заполненное видео не блокирует запрос звука.
VIDEO_END/AUDIO_END следуют за последними пакетами своего трека и не ждут расходования другого трека.
END подтверждает выдачу обоих треков. Нулевые бюджеты media также разрешают получение управляющих событий.
GAP/CLOSED отменяют прежний батч и сбрасывают frontend сразу; старые media перед ними не доигрываются.
Между готовыми пакетами нет sleep или нового round trip к ядру.
FFmpeg output обслуживается перед input; EAGAIN сохраняет рабочий packet и означает backpressure, не GAP.
Дальние кадры и звук не копируются из кольца. SoundTouch и устройство держат только рабочую порцию PCM.
Кадры сохраняются до достижения их PTS аудиочасами; задержка показа удерживает clock только этого player.
Видео декодируется без задержки frame-threading. При GAP сбрасываются decoder refs и timeline до KEY.
- Первый decoded frame разрешает движение clock/audio, защитный guard учитывает наблюдённый decoder lead.
FIN отправляет decoder EOS; выход дренируется теми же ограниченными проходами до EOF, GUI продолжает показ хвоста.
Кадры сохраняются до достижения их PTS аудиочасами; опоздавшие более чем на 100ms отбрасываются до RGB conversion.
FFmpeg использует четыре slice threads без frame-threading. При GAP сбрасываются decoder refs и timeline до KEY;
поколение video worker исключает публикацию результатов прежнего decoder.
- Первый decoded frame разрешает движение clock/audio; затем позднее видео не удерживает звук.
VIDEO_END отправляет decoder EOS, AUDIO_END дренирует SoundTouch независимо от видеодекодера.
Общий END не управляет сбросом отдельных декодеров: иначе заполненный PCM, ожидание первой картинки и decoder EOS образуют цикл.
Выход дренируется теми же ограниченными проходами до EOF, GUI продолжает показ хвоста.
После физического воспроизведения последнего PCM оставшееся видео использует монотонные часы;
EOF видеодекодера не влияет на независимое воспроизведение оставшегося звука.
Ожидающий кадр не теряется при заполненной очереди изображений. Отсутствие прогресса при наличии работы логируется как ошибка.
- При начальном подключении desktop/Android прогревают Opus из кольца до первого KEY (до 500ms истории).
Старый звук обрезается по PTS видео, включая начало stereo PCM-пакета; декодер не сбрасывается из-за preroll.
Фиксированного prebuffer нет: показ следует физическим audio callback timestamps;
Звук начинается после накопления 250ms готового PCM; после underrun применяется тот же порог без сброса decoder/PTS.
EOF выпускает хвост любой длины. Показ следует физическим audio callback timestamps;
без звука — монотонным часам с ограничением готовым PTS. END до KEY завершает пустой reader.
Короткие пропуски аудио заполняются Opus PLC; большой разрыв/pressure сбрасывает timeline.
Закрытие окна снимает только reader, собственная публикация сохраняется.
@@ -1104,7 +1166,15 @@ StreamDecodePumpTest проверяет конвейеры 4/8 кадров пр
test_stream_playback проверяет восемь будущих кадров из кольца без video lookahead; test_android_stream — один ответ
при повторных запросах, отсутствие скрытого prefetch и возобновление с прежней позиции.
test_stream_view дополнительно задерживает четыре настоящих FFmpeg decoded frames: запуск требует кадра после 80ms,
а короткий поток из трёх кадров получает все изображения после FIN, без потери decoder tail.
а короткий поток из трёх кадров получает все изображения после FIN, в том числе при заполненном аудиобуфере.
Проверяются также 8 кадров, отсутствие звука и аудиотрек короче/длиннее видео; завершение ждёт оба выходных хвоста.
StreamReadQueueTest проверяет независимые EOF, отмену старых записей по GAP/CLOSED и границы записей батча.
test_android_stream проверяет батч из 3 video + 3 audio, уведомления без потерь и пробуждение ожидающего worker при stop.
Батчевый сценарий проверяет накопленный поток, раздельные бюджеты треков, независимые readers, уведомления и остановку:
28 video + 50 audio записей проходят через 16 батчей без пропусков/повторов, END следует за последними данными.
Диагностика debug: viewer buffer выводит число/максимальный размер батчей и prepare/handoff (среднее/максимум);
desktop viewer work — обработанные записи, batch_max, pending_records, data_signals и wait_us (сумма/максимум).
wait_us включает полезное ожидание новых данных/места; задержка уже готового батча измеряется handoff_us.
test_stream_publish проверяет реальные encoder/decoder H.264/H.265/VP9/AV1, обновление libx264 bitrate
с прежним decoder, общую публикацию/RS, смену revision, pressure/KEY recovery, FIN и concurrent detach
с балансом tracked allocations. Дополнительный Xvfb/private D-Bus сценарий проверяет реальный экран/окно,
@@ -1147,23 +1217,31 @@ test_stream_publish проверяет реальные encoder/decoder H.264/H.
- AVAILABLE сообщает готовность индекса, DATA/GAP/END callbacks описывают live ingest.
media_read выдаёт END после обоих треков конкретного reader. MORE продолжает ограниченную сборку
отдельным callback без ожидания новых пакетов; WAIT ожидает новые данные или изменение позиции.
- Общий C-контроллер хранит 60 секундных минимумов запаса: ready_PTS - playback_PTS.
ready_PTS — меньшая из границ полного видео и звука, для preview без звука — граница видео.
Каждое обновление учитывается в минимуме текущей секунды, включая короткий простой.
- Декодированный PCM пополняется до 250ms при наличии данных; перед стартом и после underrun consumer ждёт этот порог.
При EOF остаток выпускается без порога. Во время накопления PTS удерживается silence span; Opus/SoundTouch не сбрасываются.
Это запас отдельного audio worker, размер аппаратного буфера не увеличивается. Audio lookahead — device lead + 500ms,
достаточный для 250ms PCM даже при x1.6. SPSC имеет 128 слотов по 960 stereo frames с собственными PTS spans.
Mixer/callback не вызывает Opus/SoundTouch; Android переиспользует PCM-массив каждого источника.
- Общий C-контроллер хранит четыре минимума за интервалы по 50ms: ready_PTS - playback_PTS.
ready_PTS — граница готового звука, пока аудиотрек активен; без звука/после AUDIO_END — максимум границ треков.
Каждое обновление учитывается в минимуме текущего интервала, включая короткий простой.
История нормализована через correction += wall_elapsed - actually_played_media_elapsed.
Старые минимумы автоматически увеличиваются на длительность паузы и уменьшаются на реальный расход при догоне.
Измерения опираются на PTS фактически услышанного PCM, включая аппаратную очередь и периоды тишины.
- После полной здоровой минуты темп плавно повышается до x1.1; SoundTouch сохраняет тон и стерео.
Ускорение расходует только минимальный запас за минуту сверх безопасной границы (не менее 33ms,
с учётом аппаратной задержки), уже поставленного ускоренного звука и плавного торможения.
- После 200ms без фактической нехватки темп плавно повышается до x1.6; SoundTouch сохраняет тон и стерео.
Темп пропорционален избытку (масштаб 2s), изменение ограничено x0.6 за 500ms.
Ускорение расходует только минимальный запас сверх защитной границы, уже подготовленного/поставленного
ускоренного звука и плавного торможения. Каждый источник имеет собственный controller и отображение PTS.
При нехватке данных темп возвращается к x1.0, часы ждут, сетевой приём продолжает накапливать запас.
После возобновления отсчёт здоровой минуты начинается заново. Разрыв/GAP/новая эпоха сбрасывают историю.
После возобновления начинается новое окно 200ms. Разрыв/GAP/новая эпоха сбрасывают историю.
- Диагностика stream: PTS, ready, текущий/минимальный запас, correction, healthy interval, tempo,
pending gain, wait/resume, generation, причины resync и размеры рабочих очередей. DEBUG — раз в секунду.
test_stream_playback проверяет историю, плавный догон с отложенной аппаратной очередью,
независимые readers, позднюю повреждённую RS-часть, вытеснение, GAP, FIN и большой многопачечный кадр.
test_stream_audio проверяет настоящий стерео Opus/SoundTouch x1.1, сохранение тона/баланса,
отображение аппаратного frame counter в исходный PTS и остановку PTS на тишине.
test_stream_audio проверяет настоящий стерео Opus/SoundTouch x1.1/x1.6, сохранение тона/баланса,
конкурентные producer/consumer, запас 250ms, аппаратный frame counter и остановку PTS на тишине.
test_stream_playback моделирует паузы 200ms/4s с отложенным выходом 350ms и проверяет возврат к guard без перерасхода.
test_stream_view задерживает видеодекодер на 500ms и проверяет непрерывность аудио.
21. Суперузлы: помощь в раздаче по запросу
@@ -1248,3 +1326,125 @@ callback обнаружения поставщика без секундной
раздачу нескольким зрителям, общий owner с локальным viewer, остановку при уходе последнего,
поздний повтор запроса до/после удаления tombstone, CANCEL до REQUEST, истечение READY без SUBSCRIBE,
откат неуспешного добавления viewer, исключение relay-only кандидатов и передачу пустого подписанного END.
Диагностика доставки и индикатор буфера
[debug] video=debug включает две сводки с периодом не меньше 500 мс и фактическим dt_ms.
stream NET — одна строка на соседа для всех его трансляций и субпотоков вместе:
- TX: sent/drop/gap за интервал, remain до головы локального кольца, возраст очереди и скорость её роста.
- ETCP: очередь до нормализации/отправки, уникальные unacked DATA, физический flight и сумма окон линков,
уникальные acked_B, принятые транспортом повторы и число физических RTO за интервал; очередь пересборки RX.
Все поля ETCP относятся к общему соединению с пиром, включая другие сервисы. unacked и flight не складываются.
errors_total — суммы счётчиков send/recv/crypto существующих линков; удаление/сброс линка меняет базу этих сумм.
- PATH: рабочие UDP/TCP, stalled, заполненные окна для первого ожидающего DATA, pacing, RTT/jitter/RTO и причина ожидания.
- RX: received/gap/dup/expired за интервал, время без DATA, максимальное относительное отставание и число stopped.
worst=group/source/epoch/sub обозначает самый отставший измеренный субпоток. connecting — все ещё открывающиеся выдачи пира.
Счётчики блоков/байтов записаны как blocks/bytes. sent означает принятие ETCP, не ACK.
na — нет измерения. После смены эпохи changed=1, транспортные дельты na; следующая сводка имеет новую базу.
ack_idle_ms начинается только после реального логического ACK, не после отправки/keepalive.
Метрика lag относительна исходной базе timestamp, не является абсолютной задержкой от источника.
stream PLAY — одна строка на плеер/источник: TIME (позиция, непрерывная готовая граница, запас, минимум, guard,
темп и причина решения), VIDEO (показ/отброс за интервал, PTS показанного/переданного поверхности кадра,
av_ms=video PTS минус media clock, очередь и ожидание KEY), AUDIO (PCM, устройство, underruns/тишина за интервал).
Длительности — мс, tempo=1000 означает 1x. generation/epoch отделяют новые временные шкалы.
GAP, перегрузка, смена поставщика и restart выводятся отдельными событиями. Подробности кодеков/кадров/GUI — video=trace.
Kotlin videoDebug/videoTrace используют native-фильтр и общий log hook, включая файловый/UDP вывод.
Индикатор публикации обновляется раз в 500 мс независимо от уровня логирования:
Android — 96x48dp слева от Start/Stop в шапке, desktop — рядом с этой кнопкой над preview.
Горизонталь [first, head] показывает сохранённое окно, где head — следующий seq после последнего блока.
Красная/жёлтая/зелёная трети обозначают близость к вытеснению/середину окна/близость к голове.
Линия объединяет субпотоки одного получателя и отмечает худшую и лучшую позиции передачи в ETCP.
Показываются до 16 наиболее отставших получателей, порядок при равенстве — ID. Позиции вне окна прижимаются к краям.
Индикатор отражает чтение кольца для отправки; неподтверждённые транспортные данные учитываются отдельно в inflight.
Восстановление доставки и старт с полного KEY — 25.10.2026
Отправитель не пропускает очередь по времени: 1-секундный catchup удалён. Возраст и рост очереди остаются метриками.
Последняя полная проверенная RS-пачка обновляет progress_ms общей сессии. Приход отдельных частей/дубликатов,
INFO/RENEW и уже объявленные GAP не являются таким прогрессом. Отсутствие полной пачки 5с запускает перезапрос, в том числе до первого DATA;
следующая попытка не раньше чем через 5с. Сохраняются сессия, принятые части, consumer/media cursors и decoder references.
Переоткрываются лишь зависшие выдачи, свежие INFO обновляют диапазоны и точки KEY поставщиков.
Счётчик selection.restarts означает попытки перезапроса, а не сбросы медиабуфера.
INFO и ACCEPT сообщают last_available и [key_seq,key_last_seq] последнего полного сохранённого KEY.
Медиаиндекс обновляет KEY только после всех фрагментов; позднее создание индекса ищет начало назад с бюджетом
и индексирует вперёд. Снимок использует уже известную точку, без повторного сканирования всего 32-МБ кольца.
INFO_REQ.want_media позволяет реле подготовить индекс без локального плеера; частичный relay не объявляет непроверенный KEY.
Начальная media-подписка выбирает KEY из INFO и открывает части параллельно с одной явной позиции.
Обычный непрозрачный stream сохраняет прежний старт с последней доступной пачки.
При восстановлении сначала запрашивается текущая недостающая пачка. Если свежие диапазоны/отказы поставщиков
и локальные части не позволяют собрать k, выбирается полный доступный KEY не ниже уже пройденной границы.
Отправитель не заменяет согласованную позицию самостоятельно; вытеснение между INFO и SUBSCRIBE возвращает EXPIRED.
Данные старых handles не примешиваются к новой попытке. Пропущенный диапазон выдаётся одним явным GAP;
новая цепочка начинает декодирование с KEY, уже выданные DATA/PTS не повторяются.
Desktop/Android не закрывают соединение по отдельному 10-секундному ожиданию KEY: отсутствие DATA обслуживает ядро.
Ошибка декодера, смена эпохи/членства, остановка сервиса и подписанный END сохраняют собственную обработку.
Новый KEY может занимать несколько пачек; продвижение этих пачек предотвращает ложный перезапрос во время сборки кадра.
Автор публикует все n частей вместе: один INFO разрешает параллельный выбор, каждое открытие проходит собственный INFO/ACCEPT.
Реле проверяются отдельно для каждой части. Подтверждённая поломка уже работавшего транспорта допускает повтор через 250мс.
Диагностика video:
- stream source offers ready — first/last, key/key_last, valid;
- stream recovery request — epoch, недостающий next, idle_ms, число переоткрытых выдач, preserve_media=1;
- stream recovery resumed — продвижение после попытки без сброса медиа;
- stream recovery KEY — пройденная граница, выбранный KEY и причина недоступности прежнего диапазона;
- stream GAP — точные first/next и направление; stream NET/PLAY — сводки каждые >=500мс.
- stream BUFFER — худшая цепочка, сумма её буферов, число устаревших/недоступных/неизвестных узлов, максимум возраста раз в секунду.
Буферы цепочки воспроизведения (raw protocol v7):
TELEMETRY=18 идёт через общий raw ETCP отдельной служебной кодограммой. В очередь видео DATA он не ставится:
перед очередной DATA pump сначала отправляет просроченную/обновлённую телеметрию. Снимок строится в момент отправки,
не копится в control FIFO; при backpressure сохраняется только dirty-флаг. Уже принятые ETCP данные он не обгоняет.
Источник и relay обновляют собственные измерения каждые 200ms; получение нового upstream сразу будит downstream.
Если upstream не обновлялся 400ms, его значения сохраняются с STALE. Подтверждённый разрыв помечает последний переход DOWN,
предыдущую часть цепочки — STALE. Отсутствовавшее измерение отличается от известного нулевого буфера.
Payload TELEMETRY: count:1, count записей по 33 байта:
node_id:8, buffered_us:8, sample:8, frames:4, age_ms:4, flags:1. Все числа big-endian.
Флаги KNOWN=1, STALE=2, DOWN=4. Путь начинается источником, заканчивается отправителем и не содержит повторов/получателя.
sample — монотонный номер измерения узла; возраст увеличивается при каждом хранении/пересылке, без синхронизации часов.
Потеря выдачи не удаляет последний снимок из общего кольца; новая эпоха получает отдельное кольцо.
Подсказка frame_timing в DATA содержит first_frame:8,last_frame:8,first_pts:8,last_pts:8,period_us:4,
started:4,audio_us:4,flags:1,reserved:3. Флаги: valid=1,video=2,continuation=4; reserved=0.
Она формируется из uAV2 у источника и неизменно пересылается relay. Это диагностическая подсказка непосредственного пира,
а не часть подписанного RS-тела: на проверку подписи, выбор частей и декодирование видео она не влияет.
Считаются только части >= cursor конкретной выдачи, ещё не переданные в ETCP. История кольца и другие получатели не прибавляются.
Начала кадров плюс одно продолжение в начале очереди дают число уникальных кадров, независимо от FEC и дробления большого KEY.
Длительность определяется их PTS и последним измеренным периодом кадра; при постоянном FPS соответствует frames/FPS.
До измерения периода первого кадра время неизвестно. Аудио считается по сэмплам 48kHz; параллельные треки не суммируются.
Источник также учитывает ещё не упакованную publisher-очередь, объединяя границы частично упакованного кадра без дублирования.
Для каждого viewer выбирается целый фактический входящий путь с максимальной суммой buffered_us.
Desktop/Android показывают две строки: «0.6S → 0.5S → 2S 1.1x», под каждым временем имя узла.
Имя длиннее восьми Unicode code points сокращается до первых восьми и одного символа «…».
Последняя колонка — собственный непрерывный запас до фактической playback-позиции, включая decoder/PCM/device;
перенос между локальными очередями не уменьшает запас повторно. Темп берётся из общего playback clock и виден только при ускорении.
STALE отмечается приглушением и «?», DOWN — приглушением и «×»; последнее известное время сохраняется.
Регрессии test_stream_service --recovery-only используют настоящий CHAT/ETCP, подписи и RS 8+0.
Медиатест создаёт 50 кадров/с по 6000 байт, KEY 55000 байт на нескольких пачках раз в 50 кадров.
Проверяются порядок, возрастающий PTS, длины и каждый байт после штатного media cursor; H.264-декодер здесь не запускается.
Сценарии: блокировка отправки 850мс и 2600мс без GAP; REINIT с задержкой stream-управления 100мс;
10с выдачи 60КБ/с без потери KEY; остановка 5600мс с одной попыткой после 5с; поздний viewer с общим KEY-стартом;
настоящее вытеснение 32-МБ source-кольца с переходом на KEY; зависание одной части при продолжающейся доставке семи других;
вытеснение KEY между INFO и SUBSCRIBE ещё до первого DATA с обновлением стартовой точки через 5с.
После коротких задержек сохраняются все кадры и состояние media; при фактической утрате диапазона требуется явный GAP.
test_stream_media отдельно проверяет пустой индекс, неполный KEY, сохранение предыдущего KEY, завершение, вытеснение и GAP.
Предыдущая проверка 25.10 до этого изменения: ./check.sh — 187/187 и proxy/burst/load;
Qt stream_view/audio/publish — 3/3, Android assembleDebug — PASS.
Предыдущий изолированный стенд с 16 узлами: baseline 10с, WAN 2Мбит/с + RTT 300мс + loss 5% на 10с,
восстановление, закрытие двух подписок и END — PASS. Результат: /tmp/stream-recovery-netns/result.json.
Новая реализация использует stream wire v6: обновляются отправитель, получатели и задействованные реле.
Обратная совместимость v5 не поддерживается. Desktop и Android собраны из utun2.
Проверка реализации c3cf86b9: ./check.sh — 187/187, proxy/burst/load — PASS; Android assembleDebug — PASS.
Девять сетевых регрессий входят в test_stream_service; лог полного прогона: /tmp/utun2-key-check-final-pass.log.
Известный нестабильный Qt audio-gap тест описан в doc/tasks.md; результаты отдельного Qt-прогона: /tmp/utun2-key-qt-isolated.log.
+332
View File
@@ -0,0 +1,332 @@
Аудит документации src/stream — 26.10.2026, частично завершён
Текущий статус: deleted исправлен и покрыт штатным test_stream_member; сборка,
профиль7/7 и check.sh195/195 PASS относятся к внедрению этого исправления (раздел 5).
При продолжении аудита подтверждена потеря помещающегося Opus в publisher_block (раздел 6).
Исправление внедрено: сборка и штатный test_stream_publisher23/23 PASS; после
объединения origin/master профиль8/8 и check.sh200/200 PASS (раздел 7).
Остальные частичные/непроверенные контракты каталога не закрыты. Разделы 1–4
описывают первоначальную диагностику и проверку архитектуры до внедрения deleted.
1. Подтверждённый дефект: deleted не учитывается stream_member_load
src/stream/stream_member.c:166–167 выбирает строку только по node_id. Условие
source=0 проверяется ниже, но столбец deleted не выбирается и не проверяется.
Проверка дерева приглашений и подписи профиля не заменяет проверку блока владельца:
после принятого deleted=yes эти подписи участника остаются корректными.
Поэтому loader возвращает STREAM_OK и публикует прежние stream-опции/ключ.
Это расходится с допуском CHAT (source=0, deleted=0), закреплённым в topo_group.h,
и с результатом topo_node_sqlite_member_allowed. stream_member_supernode уже
отбирает deleted=0. Существующий тест deleted проверяет только supernode lookup.
Обработчик member_changed в stream.c:539–546 использует stream_member_load:
данный результат не запускает ветку инвалидирования STREAM_CLOSE_MEMBER.
Для исходной реализации полный сетевой сценарий продолжения выдачи после удаления не проверялся;
обход сетевой авторизации этим воспроизведением не заявляется.
Изолированная проверка
Настоящее ядро с SQLite :memory:, отдельные ключи канала и тестового участника.
Join/update/tree подписи создаются штатными функциями и принимаются member_sync_put.
В профиле корректные stream_source/subscriptions. Участник отличается от локального
узла; никаких удалённых узлов или пользовательской БД проверка не затрагивает.
Затем через member_sync_put применяется подписанный ключом владельца блок
{"ver":"1","deleted":"yes"}; прямой UPDATE deleted для воспроизведения не используется.
Фактический лог:
before load=0 allowed=1; after load=0 allowed=0 published=1; expected load=-9
Тест завершился exit=1 на несовпадении с ожидаемым STREAM_ERR_NOT_MEMBER.
Стенд: /tmp/stream-audit-member-deleted.c.
Сборка через существующие Makefile/LIBUTUN/COMMON_LIBS и дополнительную цель:
make -C tests -f Makefile -f /tmp/stream-audit-member.mk /tmp/stream-audit-member-deleted
Логи: /tmp/stream-audit-member-build.log, /tmp/stream-audit-member-deleted.log.
Runtime выполнен вне sandbox из-за ограничений сокетов uasync.
Предлагаемый вариант исправления
Добавить deleted в список полей этого же SELECT и до проверки профиля вернуть
STREAM_ERR_NOT_MEMBER для удалённого участника, с отдельным логом причины и ID.
Вторая SQL-проверка и новый API не требуются. Вариант WHERE deleted=0 тоже
отсечёт запись, но существующий лог "member absent" не отличит удаление от отсутствия.
Сохранить различие отсутствующего/удалённого участника и неполного профиля (WAIT),
атомарность state/public_key при отказе.
Регрессия должна проверить подписанное удаление источника/локального участника/
поставщика, закрытие соответствующих handles и отсутствие выдачи новых DATA.
После исправления продолжить сверку оставшихся заголовков и сетевых контрактов.
Реализация не исправлялась: аудит остановлен по правилу AGENTS.md:10.
2. Выполненная сверка
Все 18 заголовков src/stream прочитаны предварительно. Полностью прочитаны
10 реализаций: stream_block/source/buffer/media/media_buffer/wire/member/metrics/
load/playback_clock. Другие реализации stream, включая playback_audio.cpp,
network/optimizer/publisher/viewer и stream.c целиком, ещё не проверены.
В stream.c просмотрена только затронутая ветка member_changed; полный аудит
жизненного цикла сервиса этим не заявляется.
Завершены и готовы к [V]: stream_proto.h, stream_block.h, stream_source.h,
stream_media.h. Исправлены описание полного размера части и области подписи,
предусловия BE helpers, Ed25519 seed/ключ, владение и ошибки codec/source,
нулевой write/accepted, отсутствие END в source_finish, ограничения uAV2,
пересчёт first_frame_offset и атомарность views/выходов при ошибке.
В этих четырёх заголовках изменены комментарии/оформление, C-токены сохранены.
Частично проверены [~]: stream_buffer.h, stream_load.h, stream_media_buffer.h,
stream_member.h, stream_metrics.h, stream_playback_clock.h, stream_wire.h.
Их реализации прочитаны, но оставшиеся уточнения контрактов не внесены после
остановки. В stream_member.h добавлено предупреждение о подтверждённом дефекте.
Остальные семь заголовков остаются [ ]: stream.h, stream_network.h,
stream_optimizer.h, stream_playback_audio.h, stream_priv.h, stream_publisher.h,
stream_viewer.h. Прочтение одного заголовка без реализации не закрывает аудит.
doc/stream.txt остаётся [~]: просмотрены начальные разделы о FEC, подписи,
кольце и выдачах; последующие сетевые/media/desktop/Android разделы не сверены.
Добавлен статус аудита.
Полные .h/doc контракты всего модуля ещё не закрыты.
3. Проверки и ограничения
Сборка диагностического стенда PASS; отрицательная проверка deleted воспроизвела дефект.
Профильные наборы stream, полный check.sh, санитайзеры, Windows/FreeBSD и
GUI/Android runtime не запускались. Неизменность C-токенов заголовков и
git diff --check — PASS; успешные сетевые/медиа тесты не заявляются.
Предыдущие изменения Local_transport и чужие записи чек-листа сохранены.
4. Архитектурная проверка предлагаемого исправления — 26.10.2026
Вывод: проверка deleted в stream_member_load архитектурно корректна. На этом
этапе проверка выполнялась на временной копии модуля без изменений .c проекта;
внедрение описано в разделе 5. Статусы полного аудита каталога этим не закрываются.
Граница ответственности
Блок A участника описывает личность/stream-опции. Блок B владельца определяет
в том числе исключение из канала. Корректный A не отменяет запрет из B.
member_apply_record проверяет подпись B и его версию; owner_put обновляет
adm_tags/adm_tags_sig/storage/deleted одним UPDATE. После успешного commit
member_committed посылает props-событие; Merkle-page уведомляет после commit
всей страницы. Поэтому достаточно прочитать принятую проекцию deleted в том же
SELECT, не вводя вторую криптопроверку блока B или отдельный SQL-запрос допуска.
STREAM_ERR_NOT_MEMBER уже обработан в stream_open_check, каталоге источников,
member_changed/service_reconcile, authorized и network_membership_changed.
Событие источника/self немедленно ставит invalidated, затем reconcile закрывает
сессию. session_find/handle_active/session_deliver учитывают invalidated до
отложенного освобождения. Закрытие сохраняет владение handles у caller до close.
Для исключённого поставщика закрываются его flow, а локальная сессия источника
может продолжать получать от другого поставщика. Сфера запрета — конкретный
канал; общий ETCP/NCD и владельцы других каналов не должны закрываться.
Результаты реального стенда
В /tmp/stream-arch-member.c добавлены только поле deleted в существующий SELECT
и ветка NOT_MEMBER с причиной в логе до разбора профиля. Копия связана перед
штатной libutun.a, остальные модули взяты из существующей сборки.
Стенд использует fixture test_stream_service: четыре живых ядра, два канала,
подписанные owner/member записи, настоящий Merkle, UDP/TCP и NCD.
Все четыре группы проверок PASS:
- Неполный профиль/placeholder остаётся WAIT; отсутствие и deleted дают
NOT_MEMBER; state/public_key не меняются на отказе. Неверная подпись владельца
отвергается, старая версия не снимает запрет, более свежая разрешает участие.
- Исключение источника сразу закрывает receive с MEMBER и инвалидирует reader;
новых DATA callback нет. Запрет через Merkle останавливает сам publisher.
Handle другого канала и транспорт с дополнительным NCD-владельцем сохраняются.
- Исключение поставщика закрывает только его локальную выдачу; reader источника,
другой канал и upstream другого ядра в этот момент остаются активными.
- Исключение self закрывает handles данного канала и запрещает новые publish/
subscribe. Другой канал остаётся активен. После разрешения новый open работает,
прежние отсоединённые handles не оживают. Новая публикация получает новую эпоху.
Итоговый exit=0, teardown flows=0 и ноль оставшихся tracked allocations.
Сборка: /tmp/stream-arch.mk, /tmp/stream-arch-build-final.log.
Кейсы: /tmp/stream-arch-cases.h; harness: /tmp/stream-arch-service.c.
Результат: /tmp/stream-arch-results-final.log.
Запуск: /tmp/stream-arch-service --architecture-only, вне sandbox.
Первый прогон /tmp/stream-arch-results.log неверно пытался повторно использовать
publisher после распространённого через Merkle запрета: старый publisher уже
правильно закрыт. Ожидание стенда исправлено на новый запуск после разрешения,
сама временная проверка deleted при повторе не менялась.
Гарантии и ограничения
Уведомления/освобождение остаются отложенными; нужен обычный uasync loop.
Проверено отсутствие новых DATA callback у исключённого reader. Уже принятые
ETCP пакеты не отзываются, удалённые узлы узнают запрет при его доставке через
Merkle. Архитектурная проверка не обещает мгновенного глобального отзыва данных.
Полный check.sh, санитайзеры и платформенные/GUI runtime не запускались.
На этапе архитектурной проверки C-код репозитория не менялся; последующее
внедрение и постоянная регрессия описаны ниже.
5. Внедрение согласованного исправления — 26.10.2026
В stream_member_load существующий SELECT дополнен deleted. Для удалённой записи
до crypto возвращается STREAM_ERR_NOT_MEMBER, с отдельным DEBUG_WARN о запрете
владельца, group_id/member_id. Другого SQL-запроса, нового API или формата нет.
Остальные результаты loader, включая WAIT, и атомарность выходов сохранены.
Предупреждение о неисправленном дефекте в stream_member.h заменено действующим
контрактом. Задача deleted в doc/tasks.md закрыта, текущий статус stream.txt уточнён.
Постоянная регрессия — test_stream_member в check_PROGRAMS. Тест использует
test_stream_service.c с теми же четырьмя ядрами/подписанными записями/сетью,
а STREAM_MEMBER_TEST_ONLY выбирает изолированный сценарий в отдельном процессе.
Fixture не дублируется, её изменённые member-версии/активность не влияют на
обычный test_stream_service. Сценарий доступен также через --member-only.
Проверены подписи/версии владельца, WAIT/NOT_MEMBER и сохранность выходов,
удаление источника/поставщика/self, причины закрытия, отсутствие новых DATA
callback после запрета, распространение через Merkle до остановки publisher,
сохранность чужого NCD-владения и работоспособность другого канала после
исключения self. Разрешение не оживляет старые handles; новая публикация
проверяется на новую эпоху. Ожидание удалённой остановки зависит от состояния,
а не фиксированного sleep; teardown подтверждает отсутствие tracked allocations.
./build.sh --no-gui -j4 PASS: /tmp/stream-member-fix-build.log.
Профиль7/7 PASS: stream_member, stream_service, stream_core, stream_media,
stream_playback, stream_tx_stats, android_stream.
Сводка /tmp/stream-member-fix-profile-results.log,
подробности /tmp/stream-member-fix-test_*.log.
Последующий перенос длинной строки лога не меняет содержимое сообщения;
обе test-stream цели пересобраны, test_stream_member повторно PASS:
/tmp/stream-member-fix-test-rebuild.log, /tmp/stream-member-fix-test_stream_member-final.log.
Полный Linux check.sh:195/195 PASS,0 failed/0 skipped; proxy9/9,burst PASS,
load8/8,134217728 байт,167 секунд. Лог /tmp/stream-member-fix-full-check.log.
git diff --check PASS. Sanitizers и нативные Windows/FreeBSD/GUI/Android runtime
в этом этапе не запускались; test_android_stream — тест общего C bridge на Linux.
Отметки полного аудита заголовков не расширены: устранение одного дефекта
не завершает оставшуюся сверку src/stream и всего doc/stream.txt.
6. Продолжение аудита: помещающийся Opus не упаковывается — 26.10.2026
Полностью прочитаны stream_playback_audio.cpp, stream_viewer.c, stream_publisher.c
и их заголовки. Всего прочитаны13 реализаций; полные stream.c/network/optimizer
пока не сверены. До исправления документов трёх новых модулей подтверждён дефект,
поэтому publisher.h/viewer.h/playback_audio.h отмечены[~], не[V].
Теперь4 заголовка[V],10[~],4[ ]; doc/stream.txt[~] сохраняется.
Причина
В publisher_block (stream_publisher.c:145) перед каждым Opus проверяется
used + STREAM_MEDIA_AUDIO_SIZE + packet_length + STREAM_MEDIA_VIDEO_SIZE >= capacity.
Резерв video descriptor добавляется всегда, включая полностью пустую video-очередь.
В результате допустимая audio-only запись, которая целиком помещается в uAV2,
не попадает в block. При отсутствии видео publisher_block возвращает WAIT,
actor повторяет попытки и после MEDIA_AGE_US=250000 удаляет принятый пакет.
Штатный лог говорит audio expired, не объясняя блокировку из-за лишнего резерва.
Фактическое воспроизведение
Один настоящий локальный publisher, отдельный автоматически созданный канал,
SQLite в памяти, stream_subblocks=1/stream_rs_redundant=0. Ёмкость содержимого
FEC-пачки1125 байт. Видео в очереди нет. Codec Opus48kHz stereo/20мс:
штатный opus_codec_encode плюс opus_packet_pad; оба тестовых пакета дают
opus_packet_get_nb_samples=960 и действительно декодируются в960 frames/channel.
Самостоятельный stream_media_encode успешно помещает оба блока в ту же ёмкость.
После publisher feed принимаются настоящие RS-пачки, проверяется подпись,
содержимое декодируется штатным stream_media_parse в DATA callback.
packet=1044 valid_samples=960 media_bytes=1092 capacity=1125 feed=1 delivered=1 queued=0 state=LIVE
packet=1045 valid_samples=960 media_bytes=1093 capacity=1125 feed=1 delivered=0 queued=0 state=LIVE
В логе между этими строками: publisher audio expired pts=1020000.
Результат: expected=2 received=1, exit=1; после destroy нет оставшихся tracked allocations.
Для каждой проверки отсчитано500мс: больше TTL и периода обновления TX snapshot.
Это не переполнение TX, не испорченный Opus и не нехватка места в полной пачке.
Стенд: /tmp/stream-audit-audio-pack.c, дополнительная цель /tmp/stream-audit-audio-pack.mk.
Сборка через штатные Makefile/LIBUTUN/COMMON_LIBS/test_clock.o PASS:
/tmp/stream-audit-audio-pack-build.log. Полный лог /tmp/stream-audit-audio-pack.log.
Первый диагностический запуск остановился на неверной константе capacity1133
в стенде; она заменена вычислением границы из фактической ёмкости1125.
Реализация проекта при диагностике не менялась.
Предлагаемый вариант исправления
Упаковка должна учитывать фактически добавляемые описания/данные и допускать
audio-only пачку, если целый Opus в неё помещается. Резерв под видео не должен
навсегда блокировать принятую audio-запись. Простой вариант — заполнять audio
до его реальной ёмкости, затем видео в оставшееся место; нехватка места под видео
означает следующую пачку, а не потерю audio. Wire и неделимость Opus сохраняются.
Если важно гарантировать часть видео в каждой смешанной пачке, нужен явный
переход к отдельной audio-only пачке для такой головы очереди; это более сложный вариант.
Проверить1044/1045 байтов, максимальное audio-only содержимое1077 для FEC1+0,
точное заполнение capacity, очередь видео/пустую очередь и прежний default8+0.
Отдельно различать запись, которая вообще не помещается в доступную FEC-пачку,
и запись, не поместившуюся только в текущий остаток: диагностика должна показывать
packet_length, остаток/полную ёмкость и причину, не маскируя всё как expiry.
Реализация не исправлялась; дальнейший аудит остановлен по AGENTS.md:10.
В publisher.h добавлено предупреждение, задача открыта в doc/tasks.md.
Новый полный build/check, sanitizers и Qt/Android runtime в этом продолжении
не запускались; прежний check195/195 из раздела5 новый дефект не покрывает.
Оставшиеся уточнения новых заголовков (не выполнены после остановки):
- playback_audio: create/destroy lifetime, producer/consumer дисциплина всех getters,
потребление через needs_input, feed0/1/-1/-2, PLC до100мс, prepare после finish,
EOF в empty означает извлечённый PCM, а не проигранный аппаратурой звук.
- viewer: единый callback manager, notify без reentry, асинхронный stop до освобождения ID,
пределы demand/video<=records/audio_ahead, нулевой снимок status до открытия,
уничтожение manager требует остановки callers и живого instance.
- publisher: чтение настроек FEC при actor-open, snapshot смешивает локальные очереди
текущего источника и общие ETCP-счётчики ядра, PTS/revision/KEY admission,
ошибки/состояния и условное сохранение кольца локальным readers после FIN.
7. Исправление упаковки и продвижения треков — 26.10.2026
Принят локальный вариант без изменения wire, публичного API и неделимости Opus.
Без ожидающего видео рассчитывается только фактическая стоимость audio: header32,
descriptor16, payload. Условие допуска допускает равенство полной ёмкости.
Для обычной смешанной пачки сохранён резерв descriptor32 и хотя бы байта видео.
Если первая audio-запись помещается только отдельно, публикуется audio-only пачка.
Локальный video_turn отдаёт второй слот того же actor видео; coalescing20ms в этом
слоте не применяется. Между actor-вызовами состояние не добавлено; лимит2 пачки,
mutex, порядок PTS и смещения фрагментов сохраняются. Публикуемые записи расходуются
только после успешных encode/publish; очередь при ошибке закрывается прежним путём.
Opus длиннее capacity-header32-descriptor16 actor удаляет с WARN, содержащим ID,
PTS, bytes, max и capacity. Это окончательный отказ конкретной записи, не WAIT
до истечения TTL. Следующая запись продолжает обрабатываться. Проверка находится
в actor: во время OPENING producer ещё не знает выбранную FEC-ёмкость.
feed=1 означает копирование в ограниченную очередь; асинхронный отказ и TTL
оговорены в publisher.h и stream.txt. Удаление ошибочной записи не меняет video KEY.
DEBUG показывает успешную отдельную аудиопачку и отложенное видео, TRACE — состав
каждой опубликованной пачки; WARN сообщает точную причину невозможности упаковки.
Практический масштаб
Воспроизведение использует валидный Opus, искусственно увеличенный opus_packet_pad.
Обычный desktop/Android encoder настроен AUDIO128kbit/s/20ms; средний бюджет320 байт
не является пределом VBR. Потеря обычного естественно закодированного звука не
подтверждена. При default8+0 capacity9833 одиночный Opus<=1275 помещается.
Ёмкость1125 относится ко всем конфигурациям с n-r=1 при штатном RS-size, включая2/1.
Проверки
Временная копия и реальный publisher/RS/uAV2:21/21 PASS, без tracked leaks;
/tmp/stream-plan-check.log. Старый бинарник проваливает границу1045:
/tmp/stream-plan-baseline.log. Исходники проекта при этой проверке плана не менялись.
Штатный tests/test_stream_publisher.c и цель tests/Makefile.am:23/23 PASS.
Границы1044/1045/1077/1078/1275, точная пачка из двух audio-записей, раздельные
пачки, крупная audio-очередь32 записи, unfit в начале/середине/подряд, video-only,
mixed и продолжения видео, OPENING, Stop/FIN, n/r1/0,2/1,2/0,8/0, лимит8 Opus.
Callback проверяет фактические PTS/длины, декодирует Opus960 frames/channel,
проверяет каждый байт/offset видео. Все ожидаемые данные получены до TTL250ms.
На Linux wrap uasync_set_timeout запрещает publisher_media timer после крупного
Opus и короткого P-кадра: оба должны публиковаться в том же actor. Проверка не
сравнивает задержки процессора с порогом5ms. Cleanup каждого ядра восстанавливает
tracked allocation count. Другие платформы сохраняют проверки содержимого/порядка.
./build.sh --no-gui -j4 PASS: /tmp/stream-publisher-fix-build.log.
./tests/test_stream_publisher PASS: /tmp/stream-publisher-fix-regression.log.
Перед итоговой проверкой origin/master8e746195 объединён без конфликтов в ced9dc23.
Повторная ./build.sh --no-gui -j4 PASS: /tmp/stream-publisher-fix-merged-build.log.
Профиль8/8 PASS: stream_publisher,core,media,playback,tx_stats,android_stream,service,member;
/tmp/stream-publisher-fix-profile-results.log и /tmp/stream-publisher-fix-test_*.log.
Полный ./check.sh PASS:200 passed,0 failed,0 skipped; новый publisher-тест764ms.
Proxy9/9,burst PASS,load8/8,134217728 байт; полное время175с.
Лог /tmp/stream-publisher-fix-full-check.log. git diff --check PASS.
Sanitizers, Qt GUI и Android на устройстве в этом исправлении не запускались.
Отметки аудита4[V]/10[~]/4[ ] не расширяются исправлением кода.
+6 -2
View File
@@ -31,7 +31,8 @@ nat=debug
Без `enabled=yes` модуль выключен. Без порта используется 3478. Список сохраняет порядок конфига.
При включении нужны минимум две строки `server`; для измерения выбираются два ответивших сервера с различными IP.
Совпадающие IP после DNS пропускаются. DNS асинхронный, с использованием существующего IPv4 A-резолвера.
IPv6, SOCKS UDP и `only_local` пропускаются с явным статусом; TCP не проверяется.
IPv6 и отсутствие выбранного IPv4 дают `SKIPPED` с причиной `unsupported socket`.
TCP, служебные `probe_only`, SOCKS UDP и `only_local` исключены из списка проб: API возвращает для них -1.
## Результат
@@ -40,6 +41,8 @@ IPv6, SOCKS UDP и `only_local` пропускаются с явным стат
Имя сначала разрешается в текущий рабочий сокет, затем в группу. API ядра
`stun_nat_get_socket_result(instance, socket, &result)` дополнительно проверяет
текущие IP/netif и параметры маршрутизации. Несколько рабочих портов получают общий снимок.
Нулевой возврат означает наличие снимка, включая PENDING/FAILED/SKIPPED: вызывающий проверяет статус
и возраст `measured_at_tb` относительно `STUN_NAT_TTL_TB`. -1 не меняет выходную структуру.
- `local` — тестовый IP:port; `servers[2]` и `mapped[2]` — реальные серверы и наблюдавшиеся внешние адреса;
- `translated`: -1 неизвестно, 0 трансляция не наблюдалась, 1 внешний адрес отличается;
@@ -89,7 +92,8 @@ TTL результата 300 секунд, refresh 180 секунд; непол
Во время штатного refresh доступен прежний результат до TTL; его возраст не обновляется.
Использованные тестовые порты не переиспользуются в течение жизни ядра, включая reload STUN.
История портов освобождается только при окончательном destroy ядра.
`stun_nat_recheck(instance)` явно повторяет измерение на новых портах, в том числе после смены маршрута без смены IP.
`stun_nat_recheck(instance)` сверяет текущие пути и явно повторяет измерение каждого ровно один раз
с новым generation/портом и без старого кэша, в том числе после смены маршрута без смены IP.
Публичные операции выполняются в uasync-потоке экземпляра.
INFO содержит начало измерения и итог с адресами, классификацией и причинами UNKNOWN.
+866 -1001
View File
File diff suppressed because it is too large. Load diff
+92
View File
@@ -0,0 +1,92 @@
Аудит оставшихся контрактов transport_layer — 25–26.10.2026
Область и итог
Закрыты оставшиеся auto_socket, socket_monitor, socks_client, call_ring,
reality/reality_fingerprint/reality_record/reality_io/reality_relay и stun_nat.
Полностью прочитаны их реализации, заголовки и связанные участки владельцев/API.
С учётом ранее завершённых ETCP/STCP/crypto/normalizer/NCD/dummynet/packet_dump/CRC
сверены все 32 собственных заголовка и 17 документов src/transport_layer, а также doc/stun_nat.md.
Вложенный BBR/bbr_v3.c не имеет отдельного заголовка/документа и относится к ранее сверенной
реализации etcp_bbr. Это закрытие аудита описаний и найденных
при нём дефектов, а не утверждение об отсутствии любых возможных ошибок транспорта.
socket_monitor
- Удалён разбор FreeBSD sockaddr, безусловно писавший gw[0] при переданном NULL.
Уведомления разбираются по родному типу if_msghdr/ifa_msghdr/if_announcemsghdr/rt_msghdr;
проверяются длина и версия, используется только нужный индекс интерфейса.
- При исчезновении адреса сбрасываются старые IPv4/IPv6, сохраняется порт, корректируется IPv6 scope.
- Обход callbacks использует ID/поколение: удаление/пересоздание сокетов не оставляет висячий next.
- Linux проверяет источник, границы netlink, EINTR/overflow; init откатывает каждый отказ.
- Windows объединяет уведомления и отменяет pending post при остановке после завершения native producers.
auto_socket
- UDP factory возвращает именно созданный endpoint; один общий reconcile заменяет дубли IPv4/IPv6 × UDP/TCP.
- При замене сначала снимается старый слот, неудачная попытка не оставляет dangling pointer;
частичный начальный запуск откатывается, последующие ошибки сохраняют работающие endpoints.
- Для wildcard TCP listener отдельно хранится опубликованный IP интерфейса: неизменённый listener стабилен,
реальная смена адреса/типа пересоздаёт его. DOWN/исчезнувшие интерфейсы удаляются после полного scan.
- Порты SQLite проверяются по типу/диапазону, атомарный INSERT не оставляет открытую транзакцию.
RNG проверяется, 65535 доступен. Остановка сохраняет кеш портов, удаление интерфейса очищает его.
- Внешние/Windows scans объединяются в отменяемый post; native источники отключаются до освобождения.
Внешние producers должны завершиться до destroy согласно контракту.
- Android reload сохраняет одинаковые port=0 endpoints, учитывает scope/опции и обновляет ссылки client links
до освобождения старого config. Ошибка частичного reload логируется, новый config остаётся владельцу instance.
- Проверены длины/границы Linux/FreeBSD уведомлений, ошибки nonblocking/register и Windows API.
socks_client
- CONNECT читает ровно control frame и сохраняет следующий TCP banner для владельца.
- Start не вызывает completion inline при немедленном connect; timer/nonblocking обязательны с полным rollback.
- Проверяются пределы строк, VER/RSV/REP, relay port/ATYP; wildcard UDP BND IP берётся из TCP peer.
- Отправка учитывает EINTR, SIGPIPE и нулевой прогресс, проверяются переключения write monitoring.
- UDP send возвращает payload bytes, не wire bytes; размер проверяется до сложения, NULL/0 допустимы.
Unwrap сохраняет выходы при отказе и допускает in-place payload.
- DNS start остаётся синхронным getaddrinfo с первым адресом; это явно отражено в контракте.
call_ring
- История отдельная для каждого потока; насыщенный count сохраняет последние 1024 записи после wrap idx.
- Указатели печатаются через PRIxPTR, включая Win64. Dump учитывает short write/EINTR и отказ файла,
продолжает stderr без обычного logger/выделений памяти. snprintf не объявлен async-signal-safe.
REALITY
- Уточнены одноразовость session, zero-init/cleanup, сохранение traffic после Finished,
границы codec, отсутствие replay-защиты в stateless обёртках и отдельное владение owner/I/O.
- Traffic init сбрасывает generation, KeyUpdate увеличивает его; replay entries различают пустой slot и Unix time=0.
- Ошибки client_start полностью удаляют новое I/O; send забирает вход только при успехе.
- Resume сохраняет штатный finish при close_notify; transient recv EINTR не закрывает поток.
- Relay строго проверяет destination port/timeout/длину prefix, mandatory nonblocking/timer/monitor setup.
SIGPIPE/EINTR/send=0 обрабатываются, счётчики учитывают каждый отправленный байт.
- HUP дочитывает остаток/EOF: прежде uasync снимал регистрацию без except handler и relay ждал idle timeout.
Регрессия отключает idle timeout и требует полного двустороннего half-close.
- Закрытые relay остаются в owner до call_soon; отказ его выделения не теряет контекст.
Shutdown отменяет отложенные frees и синхронно освобождает оба списка до уничтожения owner/UASYNC.
STUN
- Recheck одним scan перезапускает старые и создаёт новые пути по одному разу; раньше новый путь
немедленно открывал второй тестовый fd, расходуя первый свежий порт без измерения.
- Уточнены исключение TCP/probe_only/SOCKS/only_local из API, SKIPPED IPv6/неизвестного IPv4,
статусы результата, неизменность выхода при -1 и обязанность caller проверять TTL.
- Контракты и RFC-последовательность сверены с https://www.rfc-editor.org/rfc/rfc5780
и https://www.rfc-editor.org/rfc/rfc8489; обычный MATCH не объявляется доказательством EIM.
Проверки
- ./build.sh --no-gui -j4: PASS, /tmp/transport-audit-final-build.log.
- ./check.sh: 194/194 PASS, 0 failed/0 skipped; proxy 9/9, burst 1/1, load 8/8,
134217728 проверенных байт, 170 секунд; /tmp/transport-audit-final-check.log.
- Динамические IPv4/IPv6 сценарии auto_socket во всех UDP/TCP режимах и 8 циклах: PASS,
/tmp/transport-audit-auto-dynamic-final.log, в отдельном Linux netns.
- Проверки скрипта сборки: 44/44 PASS, /tmp/transport-audit-build-script.log.
- ASAN/UBSan/LSan: 12/12 PASS с detect_leaks=1/ halt_on_error=1:
socket_monitor, auto_socket_faults, socks_client, call_ring, reality_record, reality_hello,
reality_tls, reality_fallback, reality_relay_faults, stcp_reality_rotation, stun_nat, tun_route.
Все встроенные lib/src/Opus инструментированы согласованно в /tmp/utun-packet-dump-asan-20261025.
Сборка: /tmp/transport-audit-asan-build.log и /tmp/transport-audit-asan-tests-build.log.
Первый прогон /tmp/transport-audit-asan-results.log: 10 PASS и 2 отказа тестовых перехватов glibc FORTIFY.
Добавлены checked recvfrom/poll wrappers; оба повторных прогона PASS, без отчётов санитайзеров:
/tmp/transport-audit-asan-test_socket_monitor-final.log,
/tmp/transport-audit-asan-test_tun_route-final.log. Последние изменения только стенда проверены адресно.
Остальные индивидуальные логи: /tmp/transport-audit-asan-test_*.log.
- Нативные runtime/сборки Windows и FreeBSD недоступны и не заявляются.
FreeBSD типы уведомлений сверены с официальными заголовками releng/14.3:
https://github.com/freebsd/freebsd-src/blob/releng/14.3/sys/net/if.h
https://github.com/freebsd/freebsd-src/blob/releng/14.3/sys/net/route.h
Полнота [V] означает сверку описаний по исходникам; платформенные runtime-ограничения сохранены здесь явно.
+104
View File
@@ -0,0 +1,104 @@
Аудит utun_instance — 26.10.2026
Объём
Полностью прочитаны utun_instance.c/.h и прежний utun_instance_doc.md; сверены
вызовы конструкторов/start/stop/reload из CLI/встраивания и связанные механизмы
ETCP/NCD/STCP, sockets, logger, очередей, SQLite и независимых сервисов.
Заголовок и документ переписаны по фактическому коду. Core-разделы
doc/service_lifecycle.md согласованы; весь поперечный документ остаётся [~].
Это не полный аудит реализаций chat/call/radio/stream или GUI/Android.
Исправления
- Activity/peer-sleep подписки удерживают снятые записи до выхода из внешней
рассылки. Безопасны удаление себя/соседа, вложенное событие и добавление новой
подписки. Повтор fn+arg не дублируется; более новая эффективная activity
отменяет остаток старой рассылки. OOM обеих подписок логируется.
- Конструкторы проверяют UASYNC/config/path до побочных действий; общий путь
создания освобождает частичные ресурсы, сохраняя caller config при отказе.
Проверяются crypto derivation, запись/close временного файла, создание каталогов
и индекс сетей. Старый TCP-реестр индексируется по node_id, а не указателю link.
- Выбранный путь общего logger копируется в постоянный буфер; освобождение config
больше не оставляет заимствованную строку в logger. Уже открытый файловый logger
сохраняет первый путь при создании/reload другого instance.
- Reload готовит config/firewall/networks до изменения ресурсов. Смена владельцев
(в том числе db_path, control, TUN/NAT/proxy), подсетей или automatic server-конфига
пересоздаёт ядро с прежними core/UTUN/chat/running. Повторные имена server также
выбирают полный путь, сохраняя отдельные секции parser без неоднозначного matching.
- Manual reload сравнивает все server-параметры и IPv6 scope по конфигурации,
сохраняя неизменённые sockets/ephemeral ports. TCP listener закрывается вместе
с правильной TCP-моделью; UDP создаётся/удаляется своим API. Устранён лог через
освобождённое имя. Probe и endpoints других владельцев не удаляются по отсутствию
server-секции. Конфигурационные clients обновляются также без UTUN; неизменённые
handles/таймеры сохраняются, копии не ссылаются на освобождённый config.
- Destroy обходит sockets/connections от свежей головы после callbacks,
освобождает старые TCP queue entries и оставшиеся transport-подписки.
Повторный destroy/start/reload при разрушении отклоняется; SQLite close ошибки
видны, BUSY завершается close_v2 до финализации заимствованных SQL-объектов.
- Три headless bind разбираются одинаково: IPv4 literal, полный decimal port
0..65535. Port=0 выбирает ОС; плохое значение не открывает порт по умолчанию.
Отказ listener не фатален для чата, частичный контекст очищается.
- Call/radio headless stop освобождает контекст/кодеки, снимает клиентов с UASYNC
и отменяет cleanup/mix таймеры. Их отказы при старте дают rollback.
- UBSan нашёл невыровненный PCM после 11-байтового headless заголовка и uint64_t
hop-list в radio_recv. Оба читаются через выровненное хранилище; тест рации также
использует memcpy вместо int16_t* к невыровненному payload. Wire не менялся.
- Из заголовка удалено объявление несуществующего utun_instance_run; poll принадлежит
caller. Уточнены владение, состояния, ошибки init/reload, подписки и диагностика.
Контракты и ограничения
Все lifecycle операции выполняются в потоке своего UASYNC, stop/destroy/reload —
вне callbacks останавливаемых модулей. UASYNC принадлежит caller и живёт до конца
очистки. Core start при отказе сохраняет подготовленные ресурсы для retry/destroy;
init при отказе позднего сервиса сохраняет ранее запущенные сервисы.
Create уже выполняет I/O и создаёт listeners/SQLite/router/proxy/monitoring.
Create_from_str требует временного файла /tmp и не является чистым buffer parser.
Logger общий для процесса; его изменения и config updater не входят в транзакцию reload.
Partial reload не атомарен: отказ endpoint сохраняет новый config и отсутствие
endpoint для следующей попытки. Подготовка clients при ошибке сохраняет прежние
самостоятельные handles. Full reload возвращает NULL, если новое ядро не удалось
создать/запустить после уничтожения старого; caller использует возвращённый указатель.
Порядок списков/padding модели может консервативно выбрать restart. IPv6 selector
temporary/permanent меняет адрес в модели при разрешении, поэтому повторный reload
может консервативно пересоздать такой endpoint.
Caller завершает borrowed statements/backups; close_v2 не разрешает использовать
уничтоженный instance. Живые ресурсы в diagnose_leaks сами по себе не доказывают утечку.
Отменённые heap-таймеры UASYNC освобождаются при следующем poll, а pool cache — при
уничтожении UASYNC; тест сверяет active/counters и общий баланс после обоих шагов.
Проверки
- Полная обычная сборка PASS: /tmp/utun-instance-audit-build-final.log.
- Первый полный check.sh: 196/196 PASS, 0 failed/0 skipped; proxy9/9, burst1/1,
load8/8, 134217728 байт, 189с; /tmp/utun-instance-audit-final-check.log.
Он предшествовал последней правке выравнивания radio hop-list.
- Профиль после исправлений: test_instance_contract, test_services, test_ncd_config,
test_tun_route, test_etcp_transport, test_stcp, test_node_conn_direct,
test_call_headless, test_radio_headless PASS; три изменённых адресных теста
повторены в /tmp/utun-instance-audit-final-test_*.log.
- Новый test_instance_contract проверяет nested callbacks/OOM, constructor ownership,
правильный TCP queue index, неизменённые ephemeral endpoints, malformed config,
отказ network queue/create endpoint с retry, UDP/TCP замену и удаление listener,
duplicate server names, full reload с сохранением stopped/core-only состояния,
SQLite BUSY/finalize, другой UASYNC, db_path restart, recursive mkdir и жизнь
logger path после освобождения config. Headless: три port=0, реальные TCP-клиенты,
stop/start с сохранением SQLite, три timer faults и malformed binds.
- ASAN/UBSan/LSan 11/11 PASS, detect_leaks=1, halt_on_error=1:
те же девять instance/transport/headless тестов плюс config_parser/config_updater.
Итог: /tmp/utun-instance-audit-asan-results-complete.log; отдельные логи
/tmp/utun-instance-audit-asan-complete-test_*.log.
Отдельная копия /tmp/utun-config-instance-asan-20261026, все lib/src/Opus
инструментированы согласованно. Встроенный Opus не перекрывает CFLAGS в этой
тестовой копии; рабочая normal-сборка с sanitizer-объектами не смешивалась.
Сборки: /tmp/utun-instance-audit-asan-build.log,
/tmp/utun-instance-audit-asan-core-final.log и ...-asan-tests-build.log.
Ранние наборы 9/11 и 10/11 выявили описанные alignment defects; окончательный
набор после исправления обеих причин полностью PASS.
- git diff --check PASS. Нативные Windows/FreeBSD сборки/runtime, GUI/Android и
аппаратные TUN/media в этом этапе не запускались; Linux TUN lifecycle проверен mock-тестом.
Окончательный штатный check.sh после последней правки: 196/196 PASS,
0 failed/0 skipped, proxy9/9, burst1/1, load8/8, 134217728 байт, 173с.
Лог /tmp/utun-instance-audit-complete-check.log. После этого изменены только комментарии
о повторном core_start (он сохраняет running) и итоговые документы/чек-лист.
utun_instance.h и utun_instance_doc.md отмечены [V]; ограничения общего документа
service_lifecycle.md и прежние записи остальных модулей сохранены.
+2
View File
@@ -15,6 +15,8 @@ libuasync_a_SOURCES = \
socket_compat.h \
platform_compat.c \
platform_compat.h \
file_io.c \
file_io.h \
getmyip.c \
getmyip.h \
swm_min.c \
+7 -3
View File
@@ -34,23 +34,27 @@ struct adns_result {
struct sockaddr_in addrs[ADNS_MAX_ADDRS]; /* AF_INET, sin_port = 0 */
};
/* res заимствован до возврата callback; handle запроса к этому моменту уже освобождён. */
typedef void (*adns_done_cb)(const struct adns_result* res, void* arg);
struct adns_opts {
struct sockaddr_in server; /* sin_family != 0 → использовать этот сервер (иначе системные) */
int timeout_ms; /* таймаут на сервер, по умолчанию 2000 */
int timeout_ms; /* Таймаут на сервер в мс; <=0 → 2000, округляется вверх до целых секунд. */
int max_attempts; /* попыток на сервер, по умолчанию 2 */
};
struct adns_query; /* opaque */
/* Запускает асинхронный A-запрос. Коллбэк вызывается в потоке event loop.
Возвращает handle (NULL при немедленной ошибке: невалидные аргументы/нет памяти). */
* Возвращает handle, автоматически освобождаемый до callback либо при отмене.
* NULL: неверные аргументы, нехватка памяти, отсутствие DNS-серверов, ошибка создания
* резолвера, постановки запроса или регистрации отложенного запуска; callback не вызывается. */
struct adns_query* adns_resolve(struct UASYNC* ua, const char* name,
const struct adns_opts* opts,
adns_done_cb cb, void* arg);
/* Отмена ожидающего запроса: коллбэк НЕ вызывается. Только из потока loop. */
/* Отмена ожидающего запроса: освобождает handle, коллбэк НЕ вызывается. Только из потока loop.
* После завершения/отмены handle недействителен; NULL допустим. */
void adns_cancel(struct adns_query* q);
/* Системные DNS-серверы (IPv4). Возвращает количество; out[].sin_port = htons(53).
+36 -28
View File
@@ -272,6 +272,10 @@ void debug_set_masks(debug_category_t category_idx, debug_level_t level) {
debug_set_category_level(category_idx, level);
}
void debug_enable_timestamp(int enable) {
g_debug_config.timestamp_enabled = !!enable;
}
void debug_enable_function_name(int enable) {
g_debug_config.function_name_enabled = enable;
}
@@ -323,15 +327,24 @@ void debug_disable_file_output(void) {
}
}
/* Reopen log file (append mode) for SIGHUP. Complete operation with full error logging. */
/* Переоткрыть лог в append-режиме; при ошибке открытия сохранить прежний поток. */
int debug_reopen_log(void) {
if (!g_debug_config.output_file) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "No log path for reopen");
return -1;
}
const char* path = g_debug_config.output_file;
debug_disable_file_output();
debug_enable_file_output(path, 0); // append
FILE* new_file = fopen(path, "a");
if (!new_file) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot reopen log '%s': %s", path, strerror(errno));
return -1;
}
FILE* old_file = g_debug_config.file_output;
g_debug_config.file_output = new_file;
if (old_file && fclose(old_file) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Cannot close previous log '%s': %s", path, strerror(errno));
return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Log reopened: %s", path);
return 0;
}
@@ -391,13 +404,15 @@ void debug_output(debug_level_t level, debug_category_t category_idx,
va_start(args, format);
/* Add timestamp with microseconds: hh:mm:ss-xxx.yyy */
struct timeval tv;
utun_gettimeofday(&tv, NULL);
time_t tv_sec = (time_t)tv.tv_sec;
struct tm* tm_info = localtime(&tv_sec);
char time_str[32];
strftime(time_str, sizeof(time_str), "%H:%M:%S", tm_info);
BUF_ADVANCE(snprintf(buffer + offset, remaining, "[%s-%03ld.%03ld] ", time_str, tv.tv_usec / 1000, tv.tv_usec % 1000));
if (g_debug_config.timestamp_enabled) {
struct timeval tv;
utun_gettimeofday(&tv, NULL);
time_t tv_sec = (time_t)tv.tv_sec;
struct tm* tm_info = localtime(&tv_sec);
char time_str[32];
strftime(time_str, sizeof(time_str), "%H:%M:%S", tm_info);
BUF_ADVANCE(snprintf(buffer + offset, remaining, "[%s-%03ld.%03ld] ", time_str, tv.tv_usec / 1000, tv.tv_usec % 1000));
}
/* Add thread marker if set */
if (g_debug_config.thread_marker > 0) {
@@ -460,7 +475,7 @@ void debug_set_log_hook(debug_log_hook_fn hook) {
g_log_hook = hook;
}
/* Parse debug configuration from string (e.g., "ll_queue:debug,uasync:info,memory:warn,etcp:error") */
/* Настройка отдельных категорий: "sys=debug,etcp=error"; разделитель ':' также допустим. */
int debug_parse_config(const char* config_string) {
if (!config_string || !*config_string) {
return 0;
@@ -468,12 +483,10 @@ int debug_parse_config(const char* config_string) {
char* str = u_strdup(config_string);
if (!str) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to allocate debug configuration copy");
return -1;
}
debug_category_t categories = 0;
debug_level_t level = DEBUG_LEVEL_NONE;
char* token = strtok(str, ",");
while (token) {
/* Trim leading spaces */
@@ -481,15 +494,16 @@ int debug_parse_config(const char* config_string) {
token++;
}
char* colon = strchr(token, ':');
if (!colon) {
char* separator = strpbrk(token, "=:");
if (!separator) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Missing '=' or ':' in debug setting '%s'", token);
u_free(str);
return -1;
}
*colon = '\0';
*separator = '\0';
char* category_name = token;
char* level_name = colon + 1;
char* level_name = separator + 1;
/* Trim leading spaces in level_name */
while (isspace(*level_name)) {
@@ -497,22 +511,16 @@ int debug_parse_config(const char* config_string) {
}
debug_category_t cat = get_category_by_name(category_name);
if (cat == DEBUG_CATEGORY_NONE) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Unknown debug category '%s'", category_name);
if (cat <= DEBUG_CATEGORY_NONE || cat >= DEBUG_CATEGORY_COUNT) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Unknown or unsupported debug category '%s' (expected an individual category)", category_name);
u_free(str);
return -1;
}
debug_level_t lev = debug_level_from_name(level_name);
/* Set per-category level */
int idx = 0;
while (!(cat & (1ULL << idx)) && idx < DEBUG_CATEGORY_COUNT) {
idx++;
}
if (idx < DEBUG_CATEGORY_COUNT) {
g_debug_config.category_levels[idx] = lev;
}
debug_set_category_level(cat, lev);
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Applied debug setting: %s=%s (category=%d, level=%d)", category_name, level_name, cat, lev);
token = strtok(NULL, ",");
}
+35 -17
View File
@@ -1,6 +1,12 @@
/**
* Runtime debug configuration system
* Provides flexible control over debug output without recompilation
* Управление диагностикой: debug_config_init → уровни категорий и настройки вывода → DEBUG_*.
* NONE наследует общий уровень, DISABLED отключает категорию. Имена настроек — category=level.
* Открытый файл имеет приоритет над консолью; hook получает сообщения независимо от выбора выхода.
* Уровни: disabled, none, error, warn, info, debug, trace; отдельный уровень категории заменяет общий, не max().
* Пример: debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); debug_parse_config("etcp=trace,config=info").
* DEBUG_* фильтруют до форматирования; прямой debug_output фильтр не проверяет.
* Строка с префиксами ограничена буфером 4096 байт и обрезается; каждый вывод fflush.
* Модуль и смена глобальных настроек/файлов не синхронизированы; параллельный доступ согласует владелец.
*/
#ifndef DEBUG_CONFIG_H
@@ -86,9 +92,9 @@ typedef struct {
int timestamp_enabled; // Include timestamps in output
int function_name_enabled; // Include function names
int file_line_enabled; // Include file:line info
const char* output_file; // NULL = stdout, otherwise file path
const char* output_file; // Заимствованный путь для reopen; строка должна жить, пока поле её хранит
// Dual output support
// Выход: файл, иначе консоль при console_enabled != 0
FILE* file_output; // File handle for log file (NULL if not open)
int console_enabled; // Enable console output
int thread_marker; // Thread identification (0=none, e.g. 2 for uasync)
@@ -97,7 +103,8 @@ typedef struct {
/* Global debug configuration */
extern debug_config_t g_debug_config;
/* Initialize debug system with default settings */
/* До использования: ERROR, категории NONE, консоль stdout, timestamp/function/file:line включены.
* Повторный init сбрасывает поля, но не закрывает уже открытый файл; перед ним закрыть файл отдельно. */
void debug_config_init(void);
/* Set debug level */
@@ -116,16 +123,23 @@ void debug_set_categories(debug_category_t categories);
/* Установка уровня одной категории (аргумент categories — индекс, не битовая маска). */
void debug_set_masks(debug_category_t categories, debug_level_t level);
/* Configure output options */
/* Configure output options */
/* Ненулевое enable включает временную метку, 0 отключает. */
void debug_enable_timestamp(int enable);
void debug_enable_function_name(int enable);
void debug_enable_file_line(int enable);
void debug_set_output_file(const char* file_path);
/* Открыть ненулевой file_path в append-режиме, только если файл ещё не открыт; путь заимствован. */
void debug_set_output_file(const char* file_path);
/* Dual output configuration */
/* Заменить файл: truncate != 0 → обрезать, иначе append. NULL ничего не меняет.
* Путь заимствован; успешное открытие также выключает console_enabled. */
void debug_enable_file_output(const char* file_path, int truncate);
/* Закрыть файл; прежнее значение console_enabled не восстанавливается. */
void debug_disable_file_output(void);
/* Переоткрыть текущий путь в append-режиме: 0 — успех, -1 — ошибка с диагностикой.
* При ошибке открытия сохраняет прежний поток; console_enabled не меняет. */
int debug_reopen_log(void);
/* Вывод в консоль разрешён только при отсутствии открытого файла. */
void debug_enable_console(int enable);
@@ -137,7 +151,7 @@ typedef struct {
ip_str_t ip_to_str(const void *addr, int family);// big endian addr
ip_str_t sockaddr_storage_to_str(const struct sockaddr_storage *addr);// автоопределение v4 или v6
// hex dump в лог
// Hex dump: первые min(len, 128) байт; при len > 128 добавляет "...". Применяется фильтр уровня/категории.
void log_dump(int level, int category, const char* prefix, const uint8_t* data, size_t len);
/* Check if debug output should be shown for given level and category */
@@ -146,13 +160,13 @@ int debug_should_output(debug_level_t level, debug_category_t category_idx);
/* Get current debug level */
debug_level_t debug_get_level(void);
/* Get category name by index */
/* Заимствованное имя категории; для неизвестного индекса возвращает "UNKNOWN". */
const char* debug_get_category_name(debug_category_t category_idx);
/* Get category by name string */
/* Поиск без учёта регистра: индекс, ALL для "all", NONE при NULL/неизвестном имени. */
debug_category_t get_category_by_name(const char* name);
/* Get level by name string with error reporting */
/* Поиск без учёта регистра: NULL → NONE; неизвестное имя логируется и даёт INFO (fallback). */
debug_level_t debug_level_from_name(const char* name);
/* Set category level using text names (main API) */
@@ -164,16 +178,20 @@ int debug_apply_category_config(const char* category_name, const char* level_nam
/* Apply global debug level from text name */
void debug_apply_global_level(const char* level_name);
/* Format and output debug message (internal use by macros) */
/* Форматирует сообщение printf-формата с необязательными метками; фильтрацию выполняет вызывающий.
* Время календарное, локальное, hh:mm:ss-миллисекунды.микросекунды; положительный thread_marker выводится отдельно. */
void debug_output(debug_level_t level, debug_category_t category_idx,
const char* function, const char* file, int line,
const char* format, ...);
/* Log hook — if set, called with every debug message (for JNI/bridge integration) */
/* Hook вызывается синхронно для форматированного сообщения до записи в выход; NULL снимает hook.
* category/message заимствованы на время вызова, message указывает на стековый буфер; для хранения скопировать. */
typedef void (*debug_log_hook_fn)(int level, const char* category, const char* message);
void debug_set_log_hook(debug_log_hook_fn hook);
/* Parse debug configuration from string (e.g. "etcp=trace,config=info") */
/* Настройка отдельных категорий: "etcp=trace,config=info" (также ':' вместо '=').
* NULL/пустая строка ничего не меняет. Возвращает 0 при успехе, -1 при ошибке;
* настройки до ошибочного элемента остаются применёнными. */
int debug_parse_config(const char* config_string);
/* Convenience macros for debug output */
@@ -202,8 +220,8 @@ int debug_parse_config(const char* config_string);
debug_output(DEBUG_LEVEL_TRACE, category, __FUNCTION__, __FILE__, __LINE__, fmt, ##__VA_ARGS__); \
} } while(0)
/* Backward compatibility - default to ERROR level if not specified */
#define DEBUG_OUTPUT(fmt, ...) DEBUG_ERROR(DEBUG_CATEGORY_ALL, fmt, ##__VA_ARGS__)
/* Сообщение без явной категории: ERROR в категории GENERAL, с обычной фильтрацией. */
#define DEBUG_OUTPUT(fmt, ...) DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, fmt, ##__VA_ARGS__)
#ifdef __cplusplus
}
-112
View File
@@ -1,112 +0,0 @@
# debug_config — Runtime-система отладочного логирования
## 1. Назначение
Модуль обеспечивает гибкое управление отладочным выводом без перекомпиляции. Позволяет:
- Задавать **глобальный уровень** логирования (error/warn/info/debug/trace).
- **Покатегорийно** включать/выключать или переопределять уровень (26 категорий: etcp, crypto, tun, routing, bgp, bbr, …).
- Вести **двойной вывод**: в консоль (stdout) и/или в файл.
- Настраивать формат сообщений (временные метки с микросекундами, имя функции, файл:строка).
- Парсить конфигурационную строку вида `"etcp=trace,config=info"` из конфиг-файла.
Логика вывода: итоговый уровень = **max(глобальный уровень, уровень категории)**. Сообщение печатается, если его уровень ≤ итогового.
## 2. Как пользоваться
### 2.1. Инициализация
```c
#include "debug_config.h"
debug_config_init(); // уровень по умолчанию: ERROR, консоль включена
debug_set_level(DEBUG_LEVEL_INFO); // поднять глобальный уровень до INFO
debug_enable_file_output("/tmp/utun.log", 1); // писать в файл (truncate=1)
debug_apply_category_config("etcp", "trace"); // для ETCP-категории — всё
```
### 2.2. Макросы логирования
```c
DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "Decrypt failed: len=%d", len);
DEBUG_WARN(DEBUG_CATEGORY_ETCP, "Retransmit timeout, seq=%u", seq);
DEBUG_INFO(DEBUG_CATEGORY_CONNECTION, "Socket created on port %d", port);
DEBUG_DEBUG(DEBUG_CATEGORY_ROUTING, "Route added: %s -> %s", dst, gw);
DEBUG_TRACE(DEBUG_CATEGORY_TUN, "Packet recv: %zu bytes", n);
```
Макросы делают проверку `debug_should_output()` **перед** вызовом `debug_output()`, поэтому дорогой `vsnprintf` не вызывается, если вывод не нужен.
### 2.3. Конфигурационная строка
Формат: `"категория=уровень,категория=уровень,..."`. Парсится вызовом `debug_parse_config()`. Пример из конфиг-файла:
```ini
debug = etcp=trace,config=info,crypto=error
```
Категории: `none, uasync, ll_queue, connection, etcp, crypto, memory, timing, config, tun, routing, timers, normalizer, bgp, socket, control, dump, traffic, debug, general, nat, keepalive, etcp_route, bbr, etcp_dump, connectivity, all`.
Уровни: `disabled, none, error, warn, info, debug, trace`.
**Важно:** старый формат `ll_queue:debug` (двоеточие) тоже работает через `debug_parse_config`, но в конфиге рекомендуется `=`.
### 2.4. Двойной вывод (консоль + файл)
- По умолчанию вывод в **stdout**.
- `debug_enable_file_output(path, truncate)` — перенаправляет вывод в файл, консоль отключается.
- `debug_disable_file_output()` — закрывает файл.
- `debug_reopen_log()` — переоткрывает лог-файл (используется по SIGHUP для ротации).
- `debug_enable_console(1/0)` — ручное управление консольным выводом.
### 2.5. Вспомогательные утилиты
```c
log_dump(DEBUG_LEVEL_TRACE, DEBUG_CATEGORY_DUMP, "payload", data, len); // hex-дамп (до 128 байт)
ip_str_t s = sockaddr_storage_to_str(&addr); // "192.168.1.1:8080" или "[::1]:443"
ip_str_t s = ip_to_str(&addr, AF_INET); // "192.168.1.1" (без порта)
```
### 2.6. Нюансы
- **Потокобезопасность:** модуль не потокобезопасен. Несколько потоков, одновременно вызывающих макросы, могут перемешать вывод (но не упадут). Файловый вывод использует `fflush()` после каждой строки.
- **Буфер:** размер строки лога ограничен 4096 байт (`BUFFER_SIZE`), длинные сообщения обрезаются.
- **`debug_set_output_file`** против `debug_enable_file_output`: первая вызывается один раз (кто первый — того и тапки), вторая позволяет переоткрыть файл.
- **Флаг `-f` (foreground):** в foreground-режиме вывод идёт в stdout, в daemon-режиме — в файл.
## 3. API
| Функция/макрос | Назначение |
|---|---|
| `debug_config_init()` | Инициализация: глобальный уровень ERROR, консоль включена, метки времени/функций/файлов — да |
| `debug_set_level(level)` | Задать глобальный уровень (ERROR..TRACE) |
| `debug_set_category_level(cat, level)` | Задать уровень конкретной категории (0 = использовать глобальный) |
| `debug_set_category_level_by_name("etcp", "trace")` | То же, но именами строк |
| `debug_apply_category_config("etcp", "trace")` | Установить уровень категории с логированием результата |
| `debug_apply_global_level("info")` | Установить глобальный уровень по имени строки |
| `debug_parse_config("etcp=trace,config=info")` | Разобрать конфигурационную строку |
| `debug_should_output(level, cat)` | Проверить, нужно ли выводить сообщение данного уровня/категории |
| `debug_output(level, cat, func, file, line, fmt, ...)` | Форматировать и вывести сообщение (вызывается макросами) |
| `debug_enable_file_output(path, truncate)` | Включить вывод в файл (консоль отключается) |
| `debug_disable_file_output()` | Закрыть файл вывода |
| `debug_reopen_log()` | Переоткрыть лог-файл (для SIGHUP/ротации) |
| `debug_enable_console(1/0)` | Включить/выключить консольный вывод |
| `debug_get_level()` | Получить текущий глобальный уровень |
| `get_category_by_name("etcp")` | Получить ID категории по имени строки |
| `debug_level_from_name("trace")` | Получить уровень по имени строки |
| `debug_get_category_name(cat)` | Получить имя категории по ID |
| `log_dump(level, cat, prefix, data, len)` | Hex-дамп данных (первые 128 байт) в лог |
| `ip_to_str(addr, family)` | IP-адрес → строка (без порта). Статический буфер |
| `sockaddr_storage_to_str(addr)` | sockaddr_storage → "ip:port" (IPv6 в скобках). Статический буфер |
| `DEBUG_ERROR/WARN/INFO/DEBUG/TRACE(cat, fmt, ...)` | Макросы логирования с проверкой `debug_should_output()` |
| `DEBUG_OUTPUT(fmt, ...)` | Устаревший макрос совместимости (ERROR, категория ALL) |
**Глобальная переменная:** `g_debug_config` (тип `debug_config_t`) — хранит всё состояние системы логирования.
**Структура `debug_config_t`:**
- `level` — глобальный уровень (по умолчанию ERROR)
- `category_levels[26]` — уровни по категориям (NONE = использовать глобальный, DISABLED = никогда не выводить)
- `timestamp_enabled` / `function_name_enabled` / `file_line_enabled` — формат вывода
- `file_output` — FILE* файла (NULL если вывод в консоль)
- `console_enabled` — вывод в stdout
- `thread_marker` — числовой маркер потока (0 = нет)
+331
View File
@@ -0,0 +1,331 @@
#include "file_io.h"
#include "platform_compat.h"
#include "debug_config.h"
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <limits.h>
#ifdef _WIN32
static SRWLOCK temp_mutex = SRWLOCK_INIT;
#define TEMP_LOCK() AcquireSRWLockExclusive(&temp_mutex)
#define TEMP_UNLOCK() ReleaseSRWLockExclusive(&temp_mutex)
#else
#include <pthread.h>
static pthread_mutex_t temp_mutex = PTHREAD_MUTEX_INITIALIZER;
#define TEMP_LOCK() pthread_mutex_lock(&temp_mutex)
#define TEMP_UNLOCK() pthread_mutex_unlock(&temp_mutex)
#endif
/* Реестр заимствует стабильный адрес владельца, не выделяет память и общий для workers/instances. */
static struct file_io_temp *owned_temps;
static void file_io_temp_untrack(struct file_io_temp *output) {
struct file_io_temp **p = &owned_temps;
while (*p && *p != output) p = &(*p)->next_owned;
if (*p) *p = output->next_owned;
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary owner missing from registry path=%s", output->temporary);
output->next_owned = NULL;
}
#ifdef _WIN32
/* Общие пути API — UTF-8; ANSI Win32 API не применяется. */
int file_io_wide(const char *path, wchar_t wide[FILE_IO_PATH_MAX]) {
if (path && *path && strlen(path) < FILE_IO_PATH_MAX
&& MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, path, -1, wide, FILE_IO_PATH_MAX)) return 0;
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file invalid UTF-8/oversized path winerr=%lu", GetLastError());
return -1;
}
#endif
int file_io_inspect(const char *path, int follow, struct file_io_info *info) {
if (!path || !*path || strlen(path) >= FILE_IO_PATH_MAX || !info) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect invalid path/output"); return -1;
}
struct file_io_info result = {0};
#ifdef _WIN32
wchar_t wide[FILE_IO_PATH_MAX];
if (file_io_wide(path, wide)) return -1;
DWORD attributes = GetFileAttributesW(wide);
if (attributes == INVALID_FILE_ATTRIBUTES) {
DWORD error = GetLastError();
if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) return 1;
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect failed path=%s winerr=%lu", path, error); return -1;
}
if (!follow && (attributes & FILE_ATTRIBUTE_REPARSE_POINT)) result.type = FILE_IO_LINK;
else {
HANDLE handle = CreateFileW(wide, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
BY_HANDLE_FILE_INFORMATION data;
int valid = handle != INVALID_HANDLE_VALUE && GetFileInformationByHandle(handle, &data);
if (valid) {
result.type = GetFileType(handle) != FILE_TYPE_DISK ? FILE_IO_OTHER
: (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? FILE_IO_DIRECTORY : FILE_IO_REGULAR;
uint64_t size = ((uint64_t)data.nFileSizeHigh << 32) | data.nFileSizeLow;
valid = size <= INT64_MAX;
result.size = (int64_t)size;
result.identity[0] = data.dwVolumeSerialNumber;
result.identity[1] = ((uint64_t)data.nFileIndexHigh << 32) | data.nFileIndexLow;
}
DWORD error = GetLastError();
if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle);
if (!valid) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file handle inspect failed path=%s winerr=%lu", path, error); return -1; }
}
#else
struct stat data;
if ((follow ? stat(path, &data) : lstat(path, &data))) {
if (errno == ENOENT) return 1;
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file inspect failed path=%s errno=%d", path, errno); return -1;
}
result.type = S_ISREG(data.st_mode) ? FILE_IO_REGULAR : S_ISDIR(data.st_mode) ? FILE_IO_DIRECTORY
: S_ISLNK(data.st_mode) ? FILE_IO_LINK : FILE_IO_OTHER;
if (data.st_size < 0 || (uint64_t)data.st_size > INT64_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file size out of range path=%s", path); return -1;
}
result.size = (int64_t)data.st_size;
result.identity[0] = data.st_dev; result.identity[1] = data.st_ino;
#endif
*info = result;
return 0;
}
FILE *file_io_open_read(const char *path) {
struct file_io_info info;
if (file_io_inspect(path, 1, &info) || info.type != FILE_IO_REGULAR) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file source is not regular/readable path=%s", path ? path : "(null)"); return NULL;
}
#ifdef _WIN32
wchar_t wide[FILE_IO_PATH_MAX];
if (file_io_wide(path, wide)) return NULL;
FILE *file = _wfopen(wide, L"rb");
#else
FILE *file = fopen(path, "rb");
#endif
if (!file) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file open read failed path=%s errno=%d", path, errno);
return file;
}
FILE *file_io_open_update(const char *path) {
struct file_io_info info;
if (file_io_inspect(path, 0, &info) || info.type != FILE_IO_REGULAR) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file update requires regular file path=%s", path ? path : "(null)"); return NULL;
}
#ifdef _WIN32
wchar_t wide[FILE_IO_PATH_MAX];
if (file_io_wide(path, wide)) return NULL;
FILE *file = _wfopen(wide, L"r+b");
#else
FILE *file = fopen(path, "r+b");
#endif
if (!file) DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file update open failed path=%s errno=%d", path, errno);
return file;
}
int file_io_paths(const char *source, const char *destination) {
struct file_io_info input, output;
if (file_io_inspect(source, 1, &input) || input.type != FILE_IO_REGULAR) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file invalid source path=%s", source ? source : "(null)"); return -1;
}
int status = file_io_inspect(destination, 0, &output);
if (status == 1) return 0;
if (status < 0) return -1;
if (output.type != FILE_IO_REGULAR) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file destination is not regular path=%s type=%d", destination, output.type); return -1;
}
return !memcmp(input.identity, output.identity, sizeof(input.identity));
}
/* Синхронизировать именно открытый файл; повторного открытия имени нет. */
static int file_io_sync_descriptor(FILE *file, const char *path) {
#ifdef _WIN32
int result = _commit(_fileno(file));
#else
int result = fsync(fileno(file));
#endif
if (result) {
int error = errno ? errno : EIO;
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file sync failed path=%s errno=%d", path, error);
errno = error;
}
return result ? -1 : 0;
}
int file_io_sync_file(const char *path) {
FILE *file = file_io_open_update(path);
if (!file) return -1;
int result = file_io_sync_descriptor(file, path);
if (fclose(file)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file sync close failed path=%s errno=%d", path, errno); result = -1;
}
return result;
}
int file_io_sync_directory(const char *path) {
if (!path || !*path || strlen(path) >= FILE_IO_PATH_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file directory sync invalid path"); return -1;
}
#ifndef _WIN32
char parent[FILE_IO_PATH_MAX];
memcpy(parent, path, strlen(path) + 1);
char *slash = strrchr(parent, '/');
if (!slash) memcpy(parent, ".", 2);
else if (slash == parent) slash[1] = 0;
else *slash = 0;
int fd = open(parent, O_RDONLY | O_DIRECTORY);
if (fd < 0) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file directory open failed path=%s errno=%d", parent, errno); return -1; }
int error = fsync(fd) ? (errno ? errno : EIO) : 0;
if (close(fd) && !error) error = errno ? errno : EIO;
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file directory sync/close failed path=%s errno=%d", parent, error); return -1; }
#endif
return 0;
}
/* Только rename; владелец temp снимает регистрацию до длительного directory sync. */
static int file_io_rename(const char *source, const char *destination, unsigned flags) {
#ifdef _WIN32
wchar_t from[FILE_IO_PATH_MAX], to[FILE_IO_PATH_MAX];
if (file_io_wide(source, from) || file_io_wide(destination, to)) return -1;
DWORD options = MOVEFILE_REPLACE_EXISTING | ((flags & FILE_IO_DURABLE) ? MOVEFILE_WRITE_THROUGH : 0);
if (!MoveFileExW(from, to, options)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file rename failed source=%s destination=%s winerr=%lu", source, destination, GetLastError());
return -1;
}
#else
(void)flags;
if (rename(source, destination)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file rename failed source=%s destination=%s errno=%d", source, destination, errno); return -1;
}
#endif
return 0;
}
int file_io_publish(const char *source, const char *destination) {
if (file_io_paths(source, destination)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish invalid source/destination"); return -1;
}
if (file_io_rename(source, destination, FILE_IO_DURABLE)) return -1;
if (file_io_sync_directory(destination)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file installed but directory durability unconfirmed destination=%s", destination); return -1;
}
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file durably installed destination=%s", destination);
return 0;
}
int file_io_temp_open(struct file_io_temp *output, const char *destination, unsigned flags) {
if (!output || output->file || output->temporary[0] || !destination || !*destination
|| (flags & ~FILE_IO_DURABLE) || strlen(destination) + 38 >= FILE_IO_PATH_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary invalid owner/path"); return -1;
}
output->flags = flags;
for (unsigned attempt = 0; attempt < 8; attempt++) {
uint8_t random[16]; char suffix[33];
if (random_bytes(random, sizeof(random))) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary RNG failed"); return -1; }
for (unsigned i = 0; i < sizeof(random); i++) snprintf(suffix + i * 2, 3, "%02x", random[i]);
char temporary[FILE_IO_PATH_MAX];
snprintf(temporary, sizeof(temporary), "%s.tmp.%s", destination, suffix);
#ifdef _WIN32
wchar_t wide[FILE_IO_PATH_MAX];
if (file_io_wide(temporary, wide)) return -1;
TEMP_LOCK();
int fd = _wopen(wide, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
#else
TEMP_LOCK();
int fd = open(temporary, O_CREAT | O_EXCL | O_RDWR, 0600);
#endif
if (fd < 0) {
int error = errno; TEMP_UNLOCK();
if (error == EEXIST) continue;
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary open failed destination=%s errno=%d", destination, error); return -1;
}
memcpy(output->temporary, temporary, strlen(temporary) + 1);
output->next_owned = owned_temps; owned_temps = output;
TEMP_UNLOCK(); /* Файл виден очистке только вместе с зарегистрированным владельцем. */
#ifdef _WIN32
output->file = _fdopen(fd, "wb+");
if (!output->file) _close(fd);
#else
output->file = fdopen(fd, "wb+");
if (!output->file) close(fd);
#endif
if (!output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file fdopen failed path=%s errno=%d", output->temporary, errno); return -1; }
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file temporary opened path=%s durable=%u", output->temporary, flags & FILE_IO_DURABLE);
return 0;
}
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary collision limit destination=%s", destination); return -1;
}
int file_io_temp_finish(struct file_io_temp *output) {
if (!output || !output->file) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file finish without open file"); return -1; }
int error = fflush(output->file) ? (errno ? errno : EIO) : 0;
if (!error && (output->flags & FILE_IO_DURABLE) && file_io_sync_descriptor(output->file, output->temporary))
error = errno ? errno : EIO;
if (fclose(output->file) && !error) error = errno ? errno : EIO;
output->file = NULL;
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file flush/sync/close failed path=%s errno=%d", output->temporary, error); return -1; }
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file temporary finished path=%s durable=%u", output->temporary, output->flags & FILE_IO_DURABLE);
return 0;
}
void file_io_temp_discard(struct file_io_temp *output) {
if (!output) return;
if (output->file) {
if (fclose(output->file)) DEBUG_WARN(DEBUG_CATEGORY_SYS, "file cleanup close failed errno=%d", errno);
output->file = NULL;
}
if (!output->temporary[0]) return;
#ifdef _WIN32
wchar_t wide[FILE_IO_PATH_MAX];
int valid = file_io_wide(output->temporary, wide) == 0;
TEMP_LOCK();
int error = valid ? _wremove(wide) : -1;
#else
TEMP_LOCK();
int error = remove(output->temporary);
#endif
if (error && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_SYS, "file cleanup remove failed path=%s errno=%d", output->temporary, errno);
file_io_temp_untrack(output);
output->temporary[0] = 0;
TEMP_UNLOCK();
}
int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination) {
if (!output || output->file || !output->temporary[0] || file_io_paths(source, destination)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish invalid state/paths"); return -1;
}
TEMP_LOCK();
if (file_io_rename(output->temporary, destination, output->flags)) { TEMP_UNLOCK(); return -1; }
file_io_temp_untrack(output);
output->temporary[0] = 0;
TEMP_UNLOCK();
if ((output->flags & FILE_IO_DURABLE) && file_io_sync_directory(destination)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary installed but directory durability unconfirmed destination=%s", destination); return -1;
}
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file published destination=%s durable=%u", destination, output->flags & FILE_IO_DURABLE);
return 0;
}
int file_io_remove_orphan(const char *path) {
if (!path || !*path) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file orphan removal without path"); return -1; }
#ifdef _WIN32
wchar_t wide[FILE_IO_PATH_MAX];
if (file_io_wide(path, wide)) return -1;
#endif
TEMP_LOCK();
for (struct file_io_temp *owner = owned_temps; owner; owner = owner->next_owned) {
if (!strcmp(owner->temporary, path)) {
TEMP_UNLOCK();
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file orphan cleanup keeps active temporary path=%s", path);
return 0;
}
}
#ifdef _WIN32
int result = _wremove(wide);
#else
int result = remove(path);
#endif
int error = errno; TEMP_UNLOCK();
if (!result) return 1;
if (error == ENOENT) return 0;
DEBUG_WARN(DEBUG_CATEGORY_SYS, "file orphan removal failed path=%s errno=%d", path, error);
return -1;
}
+63
View File
@@ -0,0 +1,63 @@
/* file_io — синхронные файловые операции: UTF-8 пути, идентичность и публикация через соседний temp.
* inspect различает найденный/отсутствующий файл и ошибку; paths сравнивает идентичность файлов.
* Temp обнулить → open → писать в file → finish → publish; discard обязателен также при ошибке.
* Активный temp автоматически защищён от remove_orphan во всех потоках до publish/discard, включая время после finish.
* Пока temp активен, структуру нельзя копировать/перемещать; с одним temp работает один владелец.
* Исходник и каталоги должны оставаться стабильными на время операции. Это не защита от подмены родительских каталогов.
* FILE_IO_DURABLE сохраняет данные при finish и имя при publish. POSIX temp имеет права 0600.
* После успешного rename temp отсоединён даже при ошибке синхронизации каталога: отката публикации нет. */
#ifndef FILE_IO_H
#define FILE_IO_H
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#define FILE_IO_PATH_MAX 4096
#define FILE_IO_DURABLE 1u
enum file_io_type { FILE_IO_REGULAR = 1, FILE_IO_DIRECTORY, FILE_IO_LINK, FILE_IO_OTHER };
struct file_io_info {
int type; // обычный файл, каталог, ссылка/reparse point или другой объект
int64_t size; // размер в байтах, без сужения до int
uint64_t identity[3]; // идентификатор файла внутри файловой системы, сравнивать весь массив
};
struct file_io_temp {
char temporary[FILE_IO_PATH_MAX]; // только собственный temp; пусто после discard/publish
FILE *file; // открытый wb+; NULL после finish
struct file_io_temp *next_owned; // внутренний реестр живых temp; изменяет только file_io под блокировкой
unsigned flags; // режим, заданный open: 0 либо FILE_IO_DURABLE на весь срок операции
};
// path — UTF-8; follow!=0 разрешает конечную ссылку. info обязателен, заполняется только при успехе.
int file_io_inspect(const char *path, int follow, struct file_io_info *info);
#ifdef _WIN32
// Преобразовать непустой UTF-8 путь в wide[FILE_IO_PATH_MAX]; 0/-1.
int file_io_wide(const char *path, wchar_t wide[FILE_IO_PATH_MAX]);
#endif
// Открыть обычный файл для чтения; путь UTF-8. NULL при ошибке, fclose принадлежит вызывающему.
FILE *file_io_open_read(const char *path);
// Открыть существующий regular-файл для чтения/записи без усечения; конечные ссылки запрещены. UTF-8, NULL при ошибке.
FILE *file_io_open_update(const char *path);
// Источник должен быть regular; назначение отсутствует или regular без конечной ссылки. 1 — один файл, 0 — разные, -1 — ошибка.
int file_io_paths(const char *source, const char *destination);
// Сохранить существующий закрытый regular-файл без конечной ссылки; UTF-8, 0/-1.
int file_io_sync_file(const char *path);
// Сохранить каталог, содержащий path; без разделителя используется '.', для '/file' — '/'. UTF-8, 0/-1.
int file_io_sync_directory(const char *path);
// Переместить отдельный готовый файл с durable заменой destination; caller владеет source. 0/-1.
// Ошибка после rename не откатывает замену; сохраняется каталог назначения. Файлы/каталоги не подменяются извне.
int file_io_publish(const char *source, const char *destination);
// Создать эксклюзивный temp рядом с destination; структура заранее обнулена, даже после ошибки вызвать discard.
// flags=0 — flush/close и atomic rename; FILE_IO_DURABLE — также sync файла и каталога назначения.
int file_io_temp_open(struct file_io_temp *output, const char *destination, unsigned flags);
// Проверить flush/[sync]/close, обнулить file при любом результате; temp остаётся собственностью output.
int file_io_temp_finish(struct file_io_temp *output);
// Закрыть/удалить собственный temp; повторный вызов допустим. Ошибки очистки логируются.
void file_io_temp_discard(struct file_io_temp *output);
// Повторно проверить paths и заменить destination закрытым соседним temp; одинаковые файлы отклоняются. 0/-1.
// При ошибке до rename temporary сохраняется; после rename он пуст, в том числе при отказе directory sync.
int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination);
/* Удалить уже признанный сиротой файл, атомарно проверив владение file_io_temp во всех потоках.
* 1 — удалён, 0 — активный temp или уже отсутствует, -1 — ошибка (логируется).
* path должен иметь то же нормализованное написание, что и путь при temp_open; рабочий каталог не меняется. */
int file_io_remove_orphan(const char *path);
#endif
+6 -4
View File
@@ -3,6 +3,8 @@
*
* Определяет локальный IP, который ОС выберет для отправки
* на заданный удаленный адрес через заданный локальный адрес.
* Передать local/remote одного семейства AF_INET или AF_INET6 и буфер result.
* Создаёт и закрывает временный UDP-сокет; предназначен для выбора маршрута, не для обработки каждого пакета.
*/
#ifndef GETMYIP_H
@@ -26,13 +28,13 @@ extern "C" {
* Определяет локальный IP-адрес, который ОС выберет для отправки
* на заданный удаленный адрес через заданный локальный адрес.
*
* Создает временный UDP-сокет, выполняет bind на local_addr,
* Создает временный UDP-сокет, выполняет bind на копию local_addr с портом 0,
* connect к remote_addr и получает выбранный локальный IP через getsockname.
*
* @param local_addr Локальный адрес (0.0.0.0 / :: для автоопределения)
* @param remote_addr Удаленный адрес (для определения маршрута)
* @param local_addr Локальный адрес (0.0.0.0 / :: для автоопределения); входной порт игнорируется
* @param remote_addr Удаленный адрес того же семейства (для определения маршрута)
* @param result Заполняемый локальный IP (порт = 0)
* @return 0 при успехе, -1 при ошибке
* @return 0 при успехе, -1 при NULL, неподдерживаемом/разном семействе или ошибке socket/bind/connect/getsockname
*/
int get_outgoing_local_ip(const struct sockaddr_storage *local_addr,
const struct sockaddr_storage *remote_addr,
-34
View File
@@ -1,34 +0,0 @@
# Get My IP (getmyip.h)
## 1. Назначение
Определение локального IP-адреса, который ОС выберет для исходящего UDP-соединения на заданный удалённый хост. Используется в `etcp_connections.c` при установке соединения: зная remote-адрес, модуль узнаёт, какой source IP ядро назначит пакетам (нужно для проверки NAT и привязки сокета к правильному интерфейсу).
## 2. Как пользоваться
Передать локальный адрес-подсказку (или `0.0.0.0` для автоопределения) и удалённый адрес. Функция создаст временный UDP-сокет, сделает bind+connect, считает выбранный IP через `getsockname` и вернёт его в `result` (порт = 0, только IP).
```c
struct sockaddr_storage local_hint; // AF_INET6, sin6_addr = in6addr_any
struct sockaddr_storage remote; // AF_INET6, адрес сервера
struct sockaddr_storage chosen_ip;
// ... заполнить local_hint и remote ...
if (get_outgoing_local_ip(&local_hint, &remote, &chosen_ip) == 0) {
// chosen_ip.ss_family == AF_INET6, sin6_addr содержит выбранный source IP
// chosen_ip.sin6_port == 0
}
```
**Нюансы:**
- Функция временно создаёт и закрывает UDP-сокет — нельзя вызывать в hot path
- `local_addr` и `remote_addr` должны быть одного семейства (оба AF_INET или оба AF_INET6)
- Порт в `local_addr` заменяется на 0 (чтобы не конфликтовать с основными сокетами)
- Порт в `result` всегда 0 — только IP
## 3. API
### Функция
- **`get_outgoing_local_ip(local_addr, remote_addr, result)`** — создаёт временный UDP-сокет, привязывает к `local_addr` (с портом 0), делает `connect` к `remote_addr` для вычисления маршрута ядром, получает выбранный source IP через `getsockname`. Возвращает 0 при успехе, -1 при ошибке (NULL-аргументы, несовпадение семейств, ошибки сокета).
### Зависимости
- `socket_compat.h` — кроссплатформенные обёртки: `socket_create_udp`, `SOCKET_INVALID`, `socket_close_wrapper`, `socket_get_error`, `socket_strerror`
+2
View File
@@ -5,6 +5,8 @@
* Только строковые значения и обычные escapes; \uXXXX не поддерживается.
* Callback получает полные key/value, действительные только до его возврата.
* parse возвращает 0 при успехе/остановке callback, -1 при ошибке; остановка не проверяет остаток ввода.
* Синтаксис проверяется нестрого: допускаются пропущенные и завершающие запятые;
* хвост после закрывающей } игнорируется даже без остановки callback.
* get ищет первый ключ и копирует значение с NUL, при малом буфере обрезает; 0 найден, -1 ошибка/нет ключа.
*/
+26 -14
View File
@@ -129,15 +129,7 @@ struct ll_entry* ll_alloc_lldgram(uint16_t len) {
return entry;
}
void queue_free(struct ll_queue* q) {
if (!q) return;
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "queue_free: freeing queue %p, head=%p, tail=%p, count=%d",
q, q->head, q->tail, q->count);
// ВАЖНО: Не освобождаем элементы в очереди - они должны быть извлечены отдельно
// Это упрощает архитектуру и предотвращает double-u_free
static void queue_cancel_notifications(struct ll_queue* q) {
while (q->deferred_waiters) queue_waiter_cancel(q, q->deferred_waiters);
// Освободить список waiters (сбросить handle->internal, отменить отложенные вызовы)
struct queue_waiter* w = q->waiter_head;
@@ -150,11 +142,6 @@ void queue_free(struct ll_queue* q) {
q->waiter_head = NULL;
q->waiter_tail = NULL;
// Освободить хеш-таблицу
if (q->hash_table) {
u_free(q->hash_table);
}
// Отменить отложенное возобновление
if (q->resume_timeout_id) {
uasync_call_soon_cancel(q->ua, q->resume_timeout_id);
@@ -170,6 +157,31 @@ void queue_free(struct ll_queue* q) {
q->on_get_arg = NULL;
if (q->on_get_call_soon_id) uasync_call_soon_cancel(q->ua, q->on_get_call_soon_id);
q->on_get_call_soon_id = NULL; q->on_get_pending = 0;
q->callback_suspended = 1;
}
void queue_clear(struct ll_queue* q) {
if (!q) return;
#ifdef QUEUE_THREAD_CHECK
queue_check_thread(q);
#endif
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "[%s] discard queue: packets=%d bytes=%zu", q->name, q->count, q->total_bytes);
queue_cancel_notifications(q);
struct ll_entry* entry = q->head;
q->head = q->tail = NULL; q->count = 0; q->total_bytes = 0;
if (q->hash_table) memset(q->hash_table, 0, q->hash_size * sizeof(*q->hash_table));
while (entry) {
struct ll_entry* next = entry->next;
entry->owner = NULL; entry->next = entry->prev = entry->hash_next = NULL;
queue_dgram_free(entry); queue_entry_free(entry); entry = next;
}
}
void queue_free(struct ll_queue* q) {
if (!q) return;
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "queue_free: freeing queue %p, head=%p, count=%d", (void*)q, (void*)q->head, q->count);
queue_cancel_notifications(q);
if (q->hash_table) u_free(q->hash_table);
if (q->head) DEBUG_WARN(DEBUG_CATEGORY_SYS, "[%s] freeing nonempty queue: count=%d; entries remain caller-owned", q->name, q->count);
for (struct ll_entry* e = q->head; e; e = e->next) e->owner = NULL;
+97 -49
View File
@@ -41,8 +41,8 @@ extern "C" {
* @note Важные правила использования автозабора (queue_set_callback):
* 1. Коллбэк получает управление только если в очереди есть элементы И !callback_suspended
* 2. Внутри коллбэка ОБЯЗАТЕЛЬНО: queue_data_get() → обработать элемент → queue_resume_callback()
* 3. Без вызова queue_resume_callback очередь навсегда застрянет
* 4. Не извлекайте элементы вручную вне коллбэка — это нарушит логику
* 3. Без queue_resume_callback автозабор остаётся приостановленным
* 4. Ручное извлечение допустимо; для продолжения автозабора после него тоже нужен resume
* 5. queue_data_get() автоматически suspend'ит коллбэки для предотвращения рекурсии
*
* @note Память:
@@ -61,7 +61,7 @@ struct queue_waiter_handle;
* @brief Элемент очереди (переменного размера).
*
* Память выделяется одним блоком: [struct ll_entry + data[size]].
* Поле dgram — отдельный блок (malloc / пул / внешняя память).
* Поле dgram — отдельный блок (u_malloc / пул / внешняя память со своим dgram_free_fn).
*/
struct ll_entry {
char* name;
@@ -75,9 +75,9 @@ struct ll_entry {
uint8_t* dgram; // Указатель на данные пакета
void (*dgram_free_fn)(uint8_t* data); // Кастомная функция освобождения dgram
struct memory_pool* dgram_pool; // Пул для dgram (если используется)
uint32_t index_hash; // Хеш по первым 4 байтам индекса
uint32_t index_hash; // FNV-1a (32 бита) по всем index_size байтам data[] от index_offset
struct ll_entry* hash_next; // Следующий в хеш-цепочке
struct memory_pool* pool; // Пул, из которого выделен сам entry (NULL = malloc)
struct memory_pool* pool; // Пул, из которого выделен сам entry (NULL = u_malloc)
uint8_t data[0]; // Гибкий массив пользовательских данных (размер = size)
};
@@ -181,7 +181,7 @@ struct ll_queue {
#endif
};
/* ==================== Создание / уничтожение ==================== */
// ==================== Создание / уничтожение ====================
/**
* @brief Создаёт новую пустую очередь.
@@ -189,8 +189,8 @@ struct ll_queue {
* @param hash_size размер хеш-таблицы (0 = поиск по id отключён)
* @param index_offset смещение ключа индекса в data[] каждого entry (0 если hash_size=0)
* @param index_size размер ключа индекса (0 если hash_size=0)
* @param name имя очереди для отладки
* @return указатель на очередь или NULL при ошибке выделения памяти
* @param name заимствованное имя для отладки, должно жить до queue_free
* @return указатель на очередь или NULL при NULL ua / ошибке выделения памяти
*/
struct ll_queue* queue_new(struct UASYNC* ua, size_t hash_size, uint16_t index_offset, uint16_t index_size, char* name);
@@ -200,42 +200,55 @@ struct ll_queue* queue_new(struct UASYNC* ua, size_t hash_size, uint16_t index_o
*
* @warning НЕ ОСВОБОЖДАЕТ элементы в очереди!
* Их нужно извлечь через queue_data_get() и освободить через queue_entry_free().
* Перед дренажем снять callback и отменить ожидания владельцев, затем для каждого entry освободить dgram и entry.
* queue_free отменяет свои отложенные уведомления и waiters; user_arg не освобождает. NULL допустим.
*/
void queue_free(struct ll_queue* q);
/* ==================== Конфигурация ==================== */
/* Аварийный сброс: освободить все entry/dgram, отменить waiters и отложенные уведомления без их вызова.
* Не выделяет память (кроме действий пользовательского dgram_free_fn). Очередь/индекс/обычный callback/пороги сохраняются.
* empty/on_get обработчики снимаются, callback приостановлен; владельцы заново ставят ожидания после восстановления.
* Вызывать в потоке ua; NULL допустим. Для продолжения автозабора нужен queue_resume_callback(). */
void queue_clear(struct ll_queue* q);
// ==================== Конфигурация ====================
/**
* @brief Устанавливает максимальное количество элементов в очереди.
* @param q очередь
* @param lim лимит (-1 = без ограничения)
* @param lim лимит (любое отрицательное значение = без ограничения)
*
* При превышении лимита новые элементы автоматически освобождаются.
*/
void queue_set_size_limit(struct ll_queue* q, int lim);
/* ==================== Автозабор элементов ==================== */
// ==================== Автозабор элементов ====================
/**
* @brief Устанавливает коллбэк для автоматического извлечения элементов.
* @param q очередь
* @param cbk_fn функция-коллбэк
* @param cbk_fn функция-коллбэк (NULL отключает автозабор)
* @param arg пользовательский аргумент
*
* Коллбэк вызывается автоматически при наличии элементов и разрешённом состоянии.
* Только заменяет функцию и аргумент: не запускает обработку, не снимает callback_suspended
* и не отменяет уже запланированный resume. Отложенный resume использует новые функцию и аргумент.
* Для запуска автозабора из уже заполненной очереди вызовите queue_resume_callback().
*/
void queue_set_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
/**
* @brief Возобновляет работу коллбэка после обработки элемента в следующем event loop.
* @brief Разрешает автозабор и планирует отложенный вызов коллбэка.
* @param q очередь
*
* @warning ОБЯЗАТЕЛЬНО вызывать после завершения обработки элемента в коллбэке.
* Иначе автозабор элементов навсегда остановится.
* Сразу снимает callback_suspended. Если есть элементы и callback, планирует uasync_call_soon,
* если resume ещё не запланирован. При исполнении повторно проверяет наличие элементов,
* callback и отсутствие приостановки. Последующий put в пустую очередь может вызвать callback
* синхронно, если callback_defer == 0.
* @note Вызывать после завершения обработки извлечённого элемента для продолжения автозабора.
*/
void queue_resume_callback(struct ll_queue* q);
/* ==================== Пороговое ожидание (backpressure) ==================== */
// ==================== Пороговое ожидание (backpressure) ====================
/**
* @brief Устанавливает общий порог освобождения очереди.
@@ -244,11 +257,15 @@ void queue_resume_callback(struct ll_queue* q);
* @param max_bytes максимальный объём данных (0 = не проверять)
*
* Порог — общий на всю очередь. Все waiters в связном списке используют этот порог.
* При достижении порога вызывается следующий waiter из списка (FIFO / round-robin).
* После успешного get/remove при выполнении порога вызывается один следующий waiter из FIFO-списка.
* Сам setter старые ожидания не проверяет. Байт-порог использует int_len, зафиксированный при put.
* По умолчанию: max_packets=0, max_bytes=0 (пустая очередь).
*/
void queue_set_threshold(struct ll_queue* q, int max_packets, size_t max_bytes);
// enable=0 — синхронный waiter (по умолчанию), !=0 — через uasync_call_soon.
// Deferred-уведомление не резервирует место и повторно порог не проверяет: очередь к исполнению может заполниться.
// Повторную регистрацию producer из его callback при свободной очереди откладывать, чтобы избежать рекурсии.
void queue_set_waiter_defer(struct ll_queue* q, int enable);
/**
@@ -268,28 +285,38 @@ void queue_set_callback_defer(struct ll_queue* q, int enable);
* @param cbk_fn коллбэк (NULL для отмены)
* @param arg пользовательский аргумент
*
* Коллбэк вызывается асинхронно (через uasync_call_soon) когда count становится 0.
* После вызова сбрасывается — для повторного использования нужно вызвать снова.
* Заменяет регистрацию и отменяет её прежний отложенный вызов. При count == 0 сразу планирует
* uasync_call_soon; иначе вызов планируется при опустошении через queue_data_get/queue_remove_data.
* При исполнении снова проверяет count: если очередь заполнилась, регистрация сохраняется
* до следующего опустошения. Функция и аргумент сбрасываются ДО вызова callback,
* поэтому внутри него можно зарегистрировать новое уведомление.
*/
void queue_set_empty_callback(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
/**
* @brief Устанавливает коллбэк вызываемый после каждого queue_data_get (deferred через call_soon).
* @brief Устанавливает коллбэк после каждого успешного queue_data_get (deferred через call_soon).
* @param q очередь
* @param cbk_fn коллбэк (NULL для отмены)
* @param arg пользовательский аргумент
*
* Уведомления подсчитываются, не сливаются в одно; remove их не создаёт.
* NULL отменяет отложенный вызов и обнуляет накопленные уведомления. Замена использует новые функцию/arg.
*/
void queue_set_on_get(struct ll_queue* q, queue_callback_fn cbk_fn, void* arg);
/**
* @brief Регистрирует ожидание освобождения очереди до общего порога.
* @param q очередь
* @param h полностью обнулённый handle: `struct queue_waiter_handle h = {0}`
* @param h handle, полностью обнулённый перед первым использованием: `struct queue_waiter_handle h = {0}`
* @param callback функция, вызываемая при достижении порога
* @param arg аргумент коллбэка
* @return 1 — условие выполнено (callback вызван или отложен), 0 — ожидает порог, -1 — ошибка
*
* Если handle уже ожидает или имеет отложенный callback — старое ожидание отменяется.
* При корректных аргументах повторная регистрация в той же очереди отменяет прежнее ожидание.
* Если h->internal != NULL, перед переносом в другую очередь обязательно вызовите
* queue_waiter_cancel(old_q, h): очередь старого ожидания в handle не хранится.
* Уже отложенный callback (h->call_soon_id != NULL) отменяется по сохранённому h->defer_q,
* поэтому в этом состоянии handle можно передать другой очереди без отдельной отмены.
* При queue_free отменяются также уже запланированные уведомления.
*/
int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
@@ -297,44 +324,61 @@ int queue_waiter_wait(struct ll_queue* q, struct queue_waiter_handle* h,
/**
* @brief Отменяет ожидание (удаляет из списка, если зарегистрирован).
* @param q очередь
* @param q очередь исходного ожидания (для h->internal); не NULL
* @param h указатель на handle
*
* Безопасно вызывать в любом состоянии: если waiter не зарегистрирован — ничего не делает.
* Отложенный callback отменяется через сохранённую h->defer_q. Если ожидания и отложенного
* callback нет — ничего не делает. При h->internal != NULL передавать другую очередь нельзя.
*/
void queue_waiter_cancel(struct ll_queue* q, struct queue_waiter_handle* h);
/* ==================== Работа с данными ==================== */
// ==================== Работа с данными ====================
/**
* Общие правила владения для всех четырёх queue_data_put*:
* - При q == NULL или entry == NULL возвращается -1 без освобождения памяти.
* - Элемент должен быть извлечён из прежней очереди: owner, prev и next должны быть NULL.
* Повторная вставка связанного элемента приводит к abort().
* - При остальных отказах (лимит, неверный вид put или границы индекса) освобождаются
* сначала dgram через queue_dgram_free(), затем entry через queue_entry_free().
* - При успехе владение передаётся очереди/потребителю. Put может синхронно вызвать callback,
* который обработает и освободит entry и даже очередь до возврата из put.
* Использовать эти указатели после put можно только при гарантированном времени жизни.
*/
/**
* @brief Добавляет элемент в конец очереди (FIFO).
* @param q очередь
* @param entry элемент
* @return 0 — успех, -1 — превышен лимит (элемент освобождён)
* @param q очередь без хеш-таблицы (hash_size == 0)
* @param entry элемент; правила передачи владения см. выше
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или hash_size > 0
*/
int queue_data_put(struct ll_queue* q, struct ll_entry* entry);
/**
* @brief Добавляет элемент в начало очереди (LIFO, высокий приоритет).
* @param q очередь
* @param entry элемент
* @return 0 — успех, -1 — превышен лимит (элемент освобождён)
* @param q очередь без хеш-таблицы (hash_size == 0)
* @param entry элемент; правила передачи владения см. выше
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или hash_size > 0
*/
int queue_data_put_first(struct ll_queue* q, struct ll_entry* entry);
/**
* @brief Добавляет элемент в конец очереди с хеш-индексом (использует index_offset/index_size из очереди).
* @param q очередь
* @param entry элемент
* @return 0 — успех, -1 — ошибка
* @param q очередь с настроенной хеш-таблицей (hash_size > 0, index_size > 0)
* @param entry элемент; ключ занимает index_size байт в data[] от index_offset и должен помещаться в entry->size
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или неверные границы/нулевой размер индекса
* @note Владение см. выше. При hash_size == 0 реализация может вернуть 0, добавив элемент без индексации;
* наличие хеш-таблицы — предусловие, а не гарантированная проверка с возвратом -1.
*/
int queue_data_put_with_index(struct ll_queue* q, struct ll_entry* entry);
/**
* @brief Добавляет элемент в начало очереди с хеш-индексом (использует index_offset/index_size из очереди).
* @param q очередь
* @param entry элемент
* @return 0 — успех, -1 — ошибка
* @param q очередь с настроенной хеш-таблицей (hash_size > 0, index_size > 0)
* @param entry элемент; ключ занимает index_size байт в data[] от index_offset и должен помещаться в entry->size
* @return 0 — добавлен, -1 — NULL-аргумент, переполнение или неверные границы/нулевой размер индекса
* @note Владение см. выше. При hash_size == 0 реализация может вернуть 0, добавив элемент без индексации;
* наличие хеш-таблицы — предусловие, а не гарантированная проверка с возвратом -1.
*/
int queue_data_put_first_with_index(struct ll_queue* q, struct ll_entry* entry);
@@ -356,35 +400,35 @@ struct ll_entry* queue_data_get(struct ll_queue* q);
*/
int queue_entry_count(struct ll_queue* q);
/* ==================== Управление памятью элементов ==================== */
// ==================== Управление памятью элементов ====================
// проверить правильность очереди (корректность цепочки, счетчики элементов и байтов)
int queue_check_consistency(struct ll_queue* q);
/**
* @brief Выделяет элемент с отдельным буфером dgram (malloc).
* @brief Выделяет элемент с отдельным буфером dgram через u_malloc (entry->len=0, memlen=len).
* @param len требуемый размер dgram
* @return элемент или NULL при ошибке
*/
struct ll_entry* ll_alloc_lldgram(uint16_t len);
/**
* @brief Создаёт элемент с пользовательским буфером data[] (malloc одним блоком).
* @param data_size размер data[]
* @brief Создаёт обнулённый элемент с пользовательским буфером data[] (u_malloc одним блоком).
* @param data_size размер data[], должен помещаться в uint16_t
* @return элемент или NULL при ошибке
*/
struct ll_entry* queue_entry_new(size_t data_size);
/**
* @brief Создаёт элемент из пула памяти.
* @param pool пул
* @return элемент или NULL, если пул пуст
* @param pool пул с object_size >= sizeof(struct ll_entry); разность размеров должна помещаться в uint16_t
* @return элемент или NULL при ошибке выделения; пустой кеш пула пополняется автоматически
*/
struct ll_entry* queue_entry_new_from_pool(struct memory_pool* pool);
/**
* @brief Освобождает структуру элемента (возвращает в пул или free).
* @param entry элемент
* @brief Освобождает структуру элемента (возвращает в пул или u_free).
* @param entry извлечённый из очереди элемент; NULL допустим
*
* @note НЕ освобождает dgram! Используйте queue_dgram_free() отдельно.
*/
@@ -393,10 +437,14 @@ void queue_entry_free(struct ll_entry* entry);
/**
* @brief Освобождает буфер dgram элемента.
* @param entry элемент
*
* При dgram != NULL вызывает dgram_free_fn, если она задана; иначе возвращает буфер в dgram_pool,
* если задан пул; иначе вызывает u_free. Затем сбрасывает dgram и len. Сам entry не освобождает.
* Внешнему буферу нужен подходящий dgram_free_fn.
*/
void queue_dgram_free(struct ll_entry* entry);
/* ==================== Поиск и удаление ==================== */
// ==================== Поиск и удаление ====================
/**
* @brief Находит элемент по ключу индекса (требуется hash_size > 0 при создании).
@@ -426,12 +474,12 @@ struct ll_entry* queue_find_next_by_index(struct ll_queue* q,
*/
int queue_remove_data(struct ll_queue* q, struct ll_entry* entry);
/* ==================== Утилиты ==================== */
// ==================== Утилиты ====================
/**
* @brief Возвращает суммарный объём данных в очереди.
* @param q очередь
* @return количество байт
* @return сумма int_len (снимки len при put); изменение len связанного entry счётчик не пересчитывает
*/
static inline size_t queue_total_bytes(struct ll_queue* q) {
return q->total_bytes;
-235
View File
@@ -1,235 +0,0 @@
# ll_queue — Двусвязная FIFO-очередь с автозабором и backpressure
## 1. Назначение
Двусвязная очередь элементов (FIFO) с автоматическим вызовом callback при появлении данных, пороговым ожиданием освобождения (backpressure), хеш-индексом для поиска и поддержкой пулов памяти. Используется как основной механизм передачи данных между компонентами внутри одного потока uasyc: TUN ↔ ETCP ↔ routing ↔ normalizer и т.д.
Работает **строго в одном потоке** (один uasyc). Для многопоточного доступа не предназначена.
## 2. Как пользоваться
### 2.1. Создание очереди
```c
// Без хеша (простая очередь)
struct ll_queue* q = queue_new(ua, 0, 0, 0, "my_queue");
// С хешем — для быстрого поиска по ключу внутри data[]
// hash_size=256, ключ лежит в data[] по смещению 0, размер ключа 4 байта
struct ll_queue* q = queue_new(ua, 256, 0, 4, "my_hash_queue");
```
### 2.2. Создание элемента
Три способа, по убыванию производительности:
```c
// 1. Из memory pool (самый быстрый)
struct ll_entry* e = queue_entry_new_from_pool(instance->pkt_pool);
// 2. Через malloc (data[] фиксированного размера одним блоком)
struct ll_entry* e = queue_entry_new(sizeof(struct my_data));
// 3. Элемент + отдельный буфер dgram (для переменного размера данных)
struct ll_entry* e = ll_alloc_lldgram(dgram_len);
memcpy(e->dgram, src_data, dgram_len);
e->len = dgram_len;
```
### 2.3. Запись в очередь (backpressure)
**Главное правило: не забиваем очередь!** Добавляем следующий элемент только когда очередь опустела до заданного порога. Для этого используется Пороговое ожидание:
```c
// 1. Настроить порог (например: ждать пока count<=0, т.е. очередь совсем пуста)
queue_set_threshold(q, 0, 0);
// 2. В структуре-производителе завести handle:
struct queue_waiter_handle my_waiter = {0};
// 3. При готовности отправить — зарегистрировать ожидание:
static void my_send_cb(struct ll_queue* q, void* arg) {
struct my_producer* p = (struct my_producer*)arg;
struct ll_entry* e = ... // создать элемент
e->len = data_len;
queue_data_put_with_index(q, e);
// Снова зарегистрироваться на следующую порцию
queue_waiter_wait(q, &p->waiter, my_send_cb, p);
}
// Первичная регистрация:
queue_waiter_wait(q, &my_waiter, my_send_cb, producer);
// 4. При деинициализации — отменить ожидание:
queue_waiter_cancel(q, &my_waiter);
```
### 2.4. Чтение из очереди (callback)
```c
// Установить callback — будет вызываться при появлении элементов
queue_set_callback(q, my_callback, my_data);
static void my_callback(struct ll_queue* q, void* arg) {
// 1. ИЗВЛЕЧЬ элемент
struct ll_entry* e = queue_data_get(q);
if (!e) { queue_resume_callback(q); return; }
// 2. ОБРАБОТАТЬ
if (e->dgram && e->len > 0) {
do_something(e->dgram, e->len);
queue_dgram_free(e); // сначала освободить dgram
}
queue_entry_free(e); // потом освободить сам entry
// 3. ОБЯЗАТЕЛЬНО возобновить callback
queue_resume_callback(q);
}
```
**Критически важно:** `queue_data_get()` автоматически приостанавливает callback-и (`callback_suspended = 1`). Без вызова `queue_resume_callback()` очередь **навсегда застрянет** — callback больше никогда не вызовется.
Во **всех ветках** (включая ранние return по ошибке) должен быть `queue_resume_callback(q)`:
```c
// ПРАВИЛЬНО:
if (!e) { queue_resume_callback(q); return; }
if (!e->dgram) { queue_dgram_free(e); queue_entry_free(e); queue_resume_callback(q); return; }
// НЕПРАВИЛЬНО — очередь зависнет:
if (!e) return;
```
### 2.5. Поиск по индексу
```c
// Создать очередь с хешем (ключ 4 байта по смещению 0):
struct ll_queue* q = queue_new(ua, 256, 0, 4, "indexed_q");
// Добавлять элементы через _with_index (хеш вычисляется автоматически):
queue_data_put_with_index(q, entry);
// Искать:
uint32_t key = 42;
struct ll_entry* found = queue_find_data_by_index(q, &key);
// Если несколько элементов с одним ключом — продолжать поиск:
struct ll_entry* next = queue_find_next_by_index(q, &key, found);
```
### 2.6. Освобождение ресурсов
```c
// 1. Снять callback
queue_set_callback(q, NULL, NULL);
// 2. Отменить всех waiter-ов
queue_waiter_cancel(q, &my_waiter);
// 3. Извлечь и освободить ВСЕ оставшиеся элементы
struct ll_entry* e;
while ((e = queue_data_get(q)) != NULL) {
queue_dgram_free(e);
queue_entry_free(e);
}
// После извлечения всех элементов resume не нужен — callback уже снят.
// 4. Освободить очередь
queue_free(q);
```
**Важно:** `queue_free()` **не освобождает элементы**. Их нужно извлечь через `queue_data_get()` и освободить вручную.
### 2.7. Приоритетная вставка (LIFO)
```c
// Вставить в начало очереди (высокий приоритет):
queue_data_put_first(q, entry);
queue_data_put_first_with_index(q, entry);
```
### 2.8. Удаление из середины
```c
queue_remove_data(q, entry); // удаляет из очереди, НЕ освобождает память
queue_dgram_free(entry); // освободить dgram отдельно
queue_entry_free(entry); // освободить entry отдельно
```
После `queue_remove_data()` элемент больше не принадлежит очереди — вызывающий сам отвечает за его освобождение.
### 2.9. Callback при опустошении
```c
static void on_empty(struct ll_queue* q, void* arg) {
// Очередь стала пустой (count==0)
// Вызывается однократно, после вызова сбрасывается
}
queue_set_empty_callback(q, on_empty, my_data);
```
## 3. API
### Создание / уничтожение
| Функция | Назначение |
|---------|-----------|
| `queue_new(ua, hash_size, index_offset, index_size, name)` | Создать очередь. `hash_size=0` — без хеша. `ua` обязателен (для uasync_call_soon). |
| `queue_free(q)` | Освободить очередь (НЕ элементы — их надо извлечь и освободить отдельно). |
### Создание / освобождение элементов
| Функция | Назначение |
|---------|-----------|
| `queue_entry_new(data_size)` | Выделить entry с data[] через malloc. |
| `queue_entry_new_from_pool(pool)` | Выделить entry из memory pool. |
| `ll_alloc_lldgram(len)` | Выделить entry + отдельный буфер dgram (malloc). |
| `queue_entry_free(entry)` | Освободить entry (возврат в pool или free). НЕ освобождает dgram! |
| `queue_dgram_free(entry)` | Освободить dgram (через dgram_free_fn, pool или free). |
### Чтение (callback)
| Функция | Назначение |
|---------|-----------|
| `queue_set_callback(q, cbk_fn, arg)` | Установить callback автозабора. Вызывается при наличии элементов. |
| `queue_data_get(q)` | Извлечь элемент из головы. Приостанавливает callback до `queue_resume_callback()`. |
| `queue_resume_callback(q)` | **Обязательно** вызвать после обработки элемента. Планирует отложенный вызов callback через uasync_call_soon. |
| `queue_set_callback_defer(q, enable)` | `enable=1` — первый запуск callback из `put` откладывается через uasync_call_soon (очистка стека). По умолчанию `0` (синхронно). Повторные запуски всегда deferred. |
### Запись
| Функция | Назначение |
|---------|-----------|
| `queue_data_put(q, entry)` | Добавить в конец (FIFO). Для очередей БЕЗ хеша. |
| `queue_data_put_with_index(q, entry)` | Добавить в конец с хеш-индексом. |
| `queue_data_put_first(q, entry)` | Добавить в начало (LIFO, высокий приоритет). |
| `queue_data_put_first_with_index(q, entry)` | Добавить в начало с хеш-индексом. |
### Backpressure
| Функция | Назначение |
|---------|-----------|
| `queue_set_threshold(q, max_packets, max_bytes)` | Задать общий порог. Когда `count <= max_packets` И `total_bytes <= max_bytes`, waiter-ы пробуждаются (FIFO). |
| `queue_waiter_wait(q, h, callback, arg)` | Зарегистрировать ожидание. Возвращает 1 (уже выполнено), 0 (в очереди), -1 (ошибка). |
| `queue_waiter_cancel(q, h)` | Отменить ожидание. Безопасно вызывать в любом состоянии. |
| `queue_set_waiter_defer(q, enable)` | Отложенный вызов waiter-ов (через uasync_call_soon). |
| `queue_set_empty_callback(q, cbk_fn, arg)` | Одноразовый callback при count==0. |
| `queue_set_on_get(q, cbk_fn, arg)` | Callback после каждого queue_data_get (deferred). |
### Поиск / удаление
| Функция | Назначение |
|---------|-----------|
| `queue_find_data_by_index(q, index_key)` | Найти по ключу (требуется hash_size>0). Размер ключа = q->index_size. |
| `queue_find_next_by_index(q, index_key, prev_entry)` | Следующий элемент с тем же ключом. |
| `queue_remove_data(q, entry)` | Удалить из очереди (память НЕ освобождает). |
### Утилиты
| Функция | Назначение |
|---------|-----------|
| `queue_entry_count(q)` | Текущее количество элементов. |
| `queue_total_bytes(q)` | Суммарный объём данных (сумма len). |
| `queue_set_size_limit(q, lim)` | Ограничение на количество элементов (при превышении новые удаляются). |
| `queue_check_consistency(q)` | Проверка целостности (счётчики, циклы, prev/next). Только для отладки. |
+15 -6
View File
@@ -13,6 +13,7 @@
#define CANARY 0xDEADBEEF
#define PADDING_FILL 0xAA
#define METADATA_SIZE 128
#define ALLOCATION_OVERHEAD (12 + (size_t)2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE)
#define POINTER_SIZE sizeof(void*)
#define LOCATION_MAX (METADATA_SIZE - POINTER_SIZE * 2)
#define PREV_OFFSET LOCATION_MAX
@@ -89,7 +90,7 @@ void u_check(void* ptr, const char* text, const char* location) {
uint8_t* prefix_start = base + METADATA_SIZE;
uint32_t* pre_canary = (uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE);
uint32_t size = *(uint32_t*)(prefix_start + BOUNDARY_CHECK_SIZE + 4);
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
size_t total = (size_t)size + ALLOCATION_OVERHEAD;
uint8_t* user = (uint8_t*)ptr;
bool error = false;
for (size_t i = 0; i < BOUNDARY_CHECK_SIZE; i++) {
@@ -117,7 +118,7 @@ void u_check(void* ptr, const char* text, const char* location) {
}
if (error) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Allocated at: %s", location, text, alloc_loc);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Memory block size (user): %u bytes, total allocated: %u bytes", location, text, size, total);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Memory block size (user): %u bytes, total allocated: %zu bytes", location, text, size, total);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s: %s: Full memory dump at base %p:", location, text, base);
hex_dump(base, total);
exit(EXIT_FAILURE);
@@ -125,13 +126,21 @@ void u_check(void* ptr, const char* text, const char* location) {
}
void* u_malloc_impl(uint32_t size, const char* location) {
size_t total = size;
if (total > SIZE_MAX - ALLOCATION_OVERHEAD) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "MALLOC OVERFLOW: %u user bytes + %zu overhead bytes at %s", size, ALLOCATION_OVERHEAD, location);
return NULL;
}
if (size >= LARGE_ALLOC_LOG_THRESHOLD) {
DEBUG_WARN(DEBUG_CATEGORY_SYS, "LARGE ALLOC: %u bytes (%.1f MB) at %s",
size, (double)size / (1024.0 * 1024.0), location);
}
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
total += ALLOCATION_OVERHEAD;
uint8_t* base = (uint8_t*)malloc(total);
if (!base && size > 0) return NULL;
if (!base) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "MALLOC FAILED: %u user bytes, %zu total bytes at %s", size, total, location);
return NULL;
}
memset(base, PADDING_FILL, total);
// Set metadata
size_t loc_len = strlen(location);
@@ -209,7 +218,7 @@ void u_free_impl(void* ptr, const char* location) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Base: %p", (void*)base);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Allocated at: %s", alloc_loc);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "User size: %u bytes", size);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Total allocated: %u bytes", size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Total allocated: %zu bytes", (size_t)size + ALLOCATION_OVERHEAD);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Active allocs: %zu", active_count);
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "--- Metadata dump at %p (%u bytes) ---", (void*)base, (unsigned)(METADATA_SIZE + BOUNDARY_CHECK_SIZE + 8));
hex_dump(base, METADATA_SIZE + BOUNDARY_CHECK_SIZE + 8);
@@ -231,7 +240,7 @@ void u_free_impl(void* ptr, const char* location) {
allocated_count--;
frees_count++;
UNLOCK();
uint32_t total = size + 12 + 2 * BOUNDARY_CHECK_SIZE + METADATA_SIZE;
size_t total = (size_t)size + ALLOCATION_OVERHEAD;
memset(base, 0, total);
free(base);
}
+16 -1
View File
@@ -2,7 +2,10 @@
* mem — выделение памяти с защитными полями, местом аллокации и учётом живых блоков.
* Использовать u_malloc/u_calloc/u_realloc/u_strdup и парный u_free; обычный free к этим блокам неприменим.
* Макросы передают LOCATION автоматически. OOM возвращает NULL; обнаруженное повреждение завершает процесс.
* realloc при ошибке сохраняет старый блок, при size=0 освобождает его.
* realloc живого блока при ошибке сохраняет его, при size=0 освобождает; NULL обрабатывается как malloc.
* Размеры аргументов _impl — uint32_t; до вызова проверить, что исходные size_t помещаются в этот диапазон.
* Список аллокаций защищён мьютексом; доступ к одному блоку и чтение счётчика согласует вызывающий.
* Защитные поля проверяются для живых блоков; обращение к уже освобождённому/чужому указателю недопустимо.
* Отчёт u_report_unfreed_blocks вызывать после остановки потоков, меняющих список аллокаций.
*/
#ifndef MEM_H
@@ -15,18 +18,30 @@ extern "C" {
#define XSTR(s) STR(s)
#define STR(s) #s
#define LOCATION __FILE__ ":" XSTR(__LINE__)
// Проверяет canary/padding живого блока; text/location — строки диагностики. NULL ptr допустим.
// Обнаруженное повреждение логируется в SYS и завершает процесс через exit(EXIT_FAILURE).
void u_check(void* ptr, const char* text, const char* location);
#define u_malloc(size) u_malloc_impl(size, LOCATION)
#define u_calloc(nmemb, size) u_calloc_impl(nmemb, size, LOCATION)
#define u_realloc(ptr, size) u_realloc_impl(ptr, size, LOCATION)
#define u_free(ptr) u_free_impl(ptr, LOCATION)
#define u_strdup(s) u_strdup_impl(s, LOCATION)
// Выделяет size байт с метаданными; NULL при OOM/переполнении полного размера. size=0 тоже создаёт учтённый блок.
// location — обязательная строка места вызова, копируется в метаданные; payload не обнуляется.
void* u_malloc_impl(uint32_t size, const char* location);
// Выделяет и обнуляет nmemb*size байт; NULL при произведении > UINT32_MAX или ошибке malloc.
void* u_calloc_impl(uint32_t nmemb, uint32_t size, const char* location);
// NULL ptr → malloc(size), включая size=0. Для живого ptr: size=0 → free и NULL;
// иначе новый блок с min(старый размер, size) байтами данных. При OOM возвращает NULL, старый блок сохранён.
// Результат сохранять временно: tmp = u_realloc(ptr, size); if (tmp) ptr = tmp; (при size > 0).
void* u_realloc_impl(void* ptr, uint32_t size, const char* location);
// Проверяет и освобождает живой блок из этих обёрток; NULL допустим. Обычный free к нему неприменим.
void u_free_impl(void* ptr, const char* location);
// Копирует ASCIIZ-строку (strlen(s)+1 должно помещаться в uint32_t); NULL при NULL s/OOM. Освобождать u_free.
char* u_strdup_impl(const char* s, const char* location);
// После остановки изменяющих список потоков: статистика и дампы живых блоков; сами блоки не освобождает.
void u_report_unfreed_blocks(void);
// Число живых блоков, чтение без блокировки; параллельные аллокации/освобождения нужно исключить извне.
size_t u_get_allocated_count(void);
#ifdef __cplusplus
}
-48
View File
@@ -1,48 +0,0 @@
# mem (Memory Wrappers с отслеживанием утечек)
## 1. Назначение
Обёртки над стандартными `malloc`/`calloc`/`realloc`/`free`/`strdup` с детекцией:
- **Buffer overflow/underflow** — каждый блок окружён канарейками (`0xDEADBEEF`) и padding-областями, которые проверяются при `u_free` и `u_realloc`. При повреждении — подробный дамп блока и аварийное завершение.
- **Double free** — двусвязный список всех живых аллокаций; повторное освобождение детектируется по отсутствию блока в списке.
- **Утечки памяти** — `u_report_unfreed_blocks()` при завершении выводит все незакрытые блоки с местом аллокации (`файл:строка`), размером и hex-дампом данных.
Функции потокобезопасны (мьютекс/критическая секция).
## 2. Как пользоваться
```c
#include "mem.h"
// Вместо malloc/calloc/realloc/free/strdup используй соответствующие макросы:
void* buf = u_malloc(data_size);
char* str = u_strdup("hello");
buf = u_realloc(buf, new_size);
u_free(buf);
u_free(str);
// В конце main (перед return) — отчёт о незакрытых блоках:
u_report_unfreed_blocks();
// Текущее число живых аллокаций (для диагностики):
size_t count = u_get_allocated_count();
```
**Важно:**
- Макросы (`u_malloc`, `u_calloc`, `u_free`, и т.д.) автоматически подставляют `__FILE__:__LINE__` через `LOCATION` — это нужно для диагностики утечек и повреждений. Напрямую вызывай `_impl`-функции только если нужно явно передать location.
- `u_realloc(NULL, size)` эквивалентен `u_malloc(size)`, `u_realloc(ptr, 0)` эквивалентен `u_free(ptr)`.
- Потокобезопасность гарантирована, но аллокации с метаданными заметно дороже обычного `malloc` — используй `memory_pool` для hot-path объектов.
- При обнаружении повреждения (overflow/underflow/double-free) программа завершается через `exit(EXIT_FAILURE)` после вывода отладочной информации.
## 3. API
| Функция/макрос | Описание |
|---|---|
| `u_malloc(size)` | Аналог `malloc` с boundary check и трекингом |
| `u_calloc(nmemb, size)` | Аналог `calloc` (выделяет + обнуляет) |
| `u_realloc(ptr, size)` | Аналог `realloc`, с проверкой целостности старого блока |
| `u_free(ptr)` | Освобождение с проверкой canary, double-free и удалением из списка |
| `u_strdup(s)` | Аналог `strdup` через u_malloc |
| `u_report_unfreed_blocks()` | Выводит все незакрытые блоки с дампом; вызывается при shutdown |
| `u_get_allocated_count()` | Возвращает количество живых аллокаций |
| `u_check(ptr, text, location)` | Ручная проверка целостности блока (canary + padding) |
+13 -5
View File
@@ -4,6 +4,10 @@
* object_size должен вмещать void*: свободный блок хранит next в начале payload.
* Перед destroy вернуть все выданные блоки: destroy освобождает только кеш и сам пул.
* name заимствован до destroy; блок возвращать только своему пулу, внутренней синхронизации нет. */
/* Хвост блока хранит canary, места alloc/free и метку занятости. Повреждение или повторный возврат
* кешированного блока логируется в SYS и навсегда останавливает текущий поток циклом диагностики.
* Пример для очередей: init(sizeof(struct ll_entry) + inline_size, name), затем queue_entry_new_from_pool.
* Объекты, уже переданные в u_free при переполнении кеша, больше нельзя проверять или возвращать. */
#ifndef MEMORY_POOL_H
#define MEMORY_POOL_H
@@ -25,21 +29,25 @@ struct memory_pool {
void* free_head; // Голова linked list свободных блоков (next хранится в начале каждого блока)
int free_count; // Количество свободных блоков
size_t object_size; // Размер объектов в пуле (пользовательские данные)
size_t allocations; // Общее количество аллокаций (включая новые malloc)
size_t allocations; // Попытки выделить новый блок, включая неудачные; выдачи из кеша не учитываются
size_t reuse_count; // Количество повторных использований из пула
uint8_t alloc_tag_counter; // монотонный счётчик для детекции double-free (1 байт)
uint8_t alloc_tag_counter; // Не используется; init устанавливает 1. Double-free проверяется по метке в хвосте блока
const char* name; // Имя пула для диагностики (напр. "data_pool", "pkt_pool")
};
// сам пул:
// если используем с элементами ll_queue то не забываем что к object_size надо прибавить sizeof(struct ll_entry)
// Создаёт пустой пул: object_size >= sizeof(void*), name живёт до destroy; NULL при OOM.
// Для ll_queue размер включает sizeof(struct ll_entry) и пользовательский data[].
struct memory_pool* memory_pool_init(size_t object_size, const char* name);
// Освобождает кеш и пул, но не выданные объекты; NULL допустим. Все владельцы должны предварительно вернуть блоки.
void memory_pool_destroy(struct memory_pool* pool);
// элементы пула:
// Выдаёт обнулённый payload из кеша или нового блока; NULL при NULL pool/OOM. location — место вызова в хвосте.
void* memory_pool_alloc_impl(struct memory_pool* pool, const char* location);
// Возвращает свой живой obj в кеш или u_free, если кеш заполнен; NULL pool/obj ничего не делает.
void memory_pool_free_impl(struct memory_pool* pool, void* obj, const char* location);
// Копирует счётчики попыток новых аллокаций и выдач из кеша; любой выход может быть NULL, NULL pool ничего не меняет.
void memory_pool_get_stats(struct memory_pool* pool, size_t* allocations, size_t* reuse_count);
// Общее число кешированных блоков всех пулов; глобальный счётчик не синхронизирован.
size_t memory_pool_get_total_free_blocks(void);
#define memory_pool_alloc(pool) memory_pool_alloc_impl(pool, PLOCATION)
-57
View File
@@ -1,57 +0,0 @@
# memory_pool (Object Pool Allocator)
## 1. Назначение
Быстрый аллокатор объектов фиксированного размера. Вместо постоянных `malloc`/`free` хранит до 64 освобождённых объектов в linked list для повторного использования. Применяется для hot-path объектов, которые часто создаются и уничтожаются: пакеты, фрагменты, ACK-пакеты, структуры маршрутизации, TCP-сегменты lwIP и т.д.
Встроенная защита:
- **Buffer overflow** — канарейка `0xDEADBEAF` после пользовательских данных проверяется при освобождении. При повреждении — бесконечный цикл (hang) для отладки.
- **Double free** — однобайтный счётчик в метаданных; повторное освобождение детектируется и вызывает hang.
- Вся диагностика пишется через `DEBUG_ERROR(DEBUG_CATEGORY_MEMORY, ...)`.
Не потокобезопасен — каждый пул используется из одного потока (обычно в рамках одного u_async event loop).
## 2. Как пользоваться
```c
#include "memory_pool.h"
// 1. Создать пул (обычно в init-функции инстанса):
struct memory_pool* pkt_pool = memory_pool_init(sizeof(struct ETCP_DGRAM), "pkt_pool");
// 2. Выделить объект (если есть свободный — вернёт из пула, иначе u_calloc):
struct ETCP_DGRAM* dgram = memory_pool_alloc(pkt_pool);
// 3. Освободить объект (возвращается в пул, если там меньше 64 блоков):
memory_pool_free(pkt_pool, dgram);
// 4. Получить статистику:
size_t allocs, reuse;
memory_pool_get_stats(pkt_pool, &allocs, &reuse);
// reuse много → эффективно; reuse мало → пул слишком мал для нагрузки
// 5. Проверить, в пуле ли объект (0 — не в пуле, 1 — в пуле):
if (memory_pool_is_freed(pkt_pool, obj)) { /* уже освобождён */ }
// 6. Уничтожить пул (при shutdown):
memory_pool_destroy(pkt_pool);
// реально освобождает все кэшированные блоки через u_free() и сам пул.
```
**Важно:**
- Если объекты пула используются как элементы `ll_queue`, в `object_size` нужно закладывать `sizeof(struct ll_entry)`. Например: `memory_pool_init(sizeof(struct ll_entry) + sizeof(struct dummynet_pkt), "pkt_pool")`.
- `memory_pool_alloc` обнуляет объект перед возвратом (защита от утечки старых данных).
- При заполнении пула (64 свободных блока) лишние `memory_pool_free` реально вызывают `u_free` — блок не кэшируется.
- `memory_pool_destroy` проходит по всем свободным блокам и вызывает `u_free` для каждого, затем освобождает сам `struct memory_pool`.
## 3. API
| Функция/макрос | Описание |
|---|---|
| `memory_pool_init(object_size, name)` | Создаёт пул для объектов заданного размера. `name` — для диагностики. |
| `memory_pool_alloc(pool)` | Выделяет объект: из кэша (если есть) или через `u_calloc`. Обнуляет перед возвратом. |
| `memory_pool_free(pool, obj)` | Возвращает объект в пул (до 64) или вызывает `u_free`. Проверяет canary и double-free. |
| `memory_pool_destroy(pool)` | Уничтожает пул: освобождает все кэшированные блоки и сам пул. |
| `memory_pool_get_stats(pool, &allocs, &reuse)` | Статистика: общее число аллокаций и число повторных использований из кэша. |
| `memory_pool_get_total_free_blocks()` | Глобальный счётчик свободных блоков во всех пулах (для диагностики). |
| `memory_pool_is_freed(pool, obj)` | Проверяет, находится ли объект в свободном списке пула (уже освобождён). |
+20 -12
View File
@@ -2,6 +2,12 @@
* platform_compat — общие заголовки POSIX/Windows и адаптеры отсутствующих POSIX-функций.
* Также предоставляет криптографический random_bytes, адреса интерфейсов, выбор default route
* и классификацию публичных IPv4. Сокетные типы/ошибки и nonblocking API — в socket_compat.h.
* Подключать этот заголовок вместо прямых POSIX includes для общего кода.
* Windows: htobe16/32/64 и be16/32/64toh через byteswap, strcasecmp/strncasecmp через _stricmp/_strnicmp,
* memmem через inline-поиск (пустой образец возвращает начало), poll через WSAPoll, mkdir через _mkdir.
* utun_gettimeofday — календарное Unix-время (не монотонное); Windows игнорирует tz.
* Windows pipe — _pipe(4096, _O_BINARY). fcntl: F_GETFL возвращает 0, F_SETFL с O_NONBLOCK включает
* неблокирующий режим; остальные флаги игнорируются, обратное переключение этим адаптером не выполняется.
*/
#ifndef PLATFORM_COMPAT_H
@@ -178,34 +184,36 @@ extern "C" {
#include <stdint.h>
// Generate cryptographically secure random bytes
// Returns 0 on success, -1 on error
// Заполняет buffer криптографическими случайными байтами: POSIX /dev/urandom, Windows BCryptGenRandom.
// buffer не NULL, len > 0 (Windows: помещается в ULONG). 0 — всё заполнено, -1 — ошибка/неполное чтение.
int random_bytes(uint8_t *buffer, size_t len);
// Get IPv4 address of interface by its index (ifindex)
// Returns IPv4 address in network byte order, 0 on error
// POSIX: getifaddrs; Windows: IP Helper. Адрес в network byte order, 0 при ошибке/отсутствии адреса.
uint32_t get_interface_ip_by_index(uint32_t netif_index);
// Get IPv6 address of interface by its index (ifindex)
// temporary=1: get temporary (privacy) IPv6, temporary=0: get permanent IPv6
// POSIX фильтрует по IFA_F_TEMPORARY в ifa_flags, но getifaddrs не гарантирует адресные флаги;
// для точного выбора temporary/stable на Linux использовать get_interface_ipv6_addr_nl().
// Windows игнорирует temporary: предпочитает глобальный адрес, затем локальный, исключая link-local/loopback/::.
// Returns 0 on success (fills addr_out with 16 bytes), -1 on error or not found
int get_interface_ipv6_by_index(uint32_t netif_index, int temporary, uint8_t* addr_out);
// Get IPv6 address using netlink (Linux-only, accurate IFA_F_* flags)
// prefer_stable=1: prefer permanent/stable (IFA_F_MANAGETEMPADDR) over temporary
// prefer_stable=0: prefer temporary over stable
// Выбрать IPv6: Linux использует netlink с IFA_F_*, прочие POSIX — getifaddrs(), Windows — IP Helper.
// POSIX: prefer_stable=1 предпочитает постоянный адрес, 0 — temporary; допускается fallback на другой тип.
// Windows игнорирует prefer_stable, выбор как у get_interface_ipv6_by_index().
// Returns 0 on success (fills addr_out with 16 bytes), -1 on error
int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t* addr_out);
// Determine the interface index for the default route (outgoing traffic)
// Creates a temp UDP socket, connects to a well-known address to force
// kernel routing, then matches the chosen source IP to getifaddrs()
// POSIX: UDP connect к 8.8.8.8:53 или [2001:4860:4860::8888]:53, сопоставление локального IP с getifaddrs().
// Windows: GetBestInterfaceEx к тому же адресу. Результат — выбранный маршрут к этому адресу,
// который может отличаться от default route при более специфичных маршрутах.
// family: AF_INET or AF_INET6, returns ifindex (0 on failure)
uint32_t get_default_route_netif_index(int family);
// Классифицирует IPv4-адрес (network byte order):
// 1 = глобальный/публичный (маршрутизируемый в интернете),
// 0 = приватный, CGNAT, loopback, link-local, multicast или зарезервированный.
// Классифицирует IPv4 (network byte order) по встроенному списку: 0 для 0/8, 10/8, 127/8, 169.254/16,
// 172.16/12, 192.168/16, 100.64/10, 224/4 и 240/4; 1 для остальных. Это не проверка достижимости адреса.
int ip_is_public(uint32_t addr_be);
-81
View File
@@ -1,81 +0,0 @@
# platform_compat — Cross-platform compatibility layer
## 1. Назначение
Унифицирует различия между POSIX (Linux/FreeBSD) и Windows (MSYS2 UCRT64) на уровне системных вызовов и типов. Предоставляет единый API для байтового порядка, строковых функций, энтропии, работы с сетевыми интерфейсами и времени. Полностью заголовочный (inline) для макросов и тривиальных обёрток, `.c` только для нетривиальных реализаций.
## 2. Как пользоваться
Подключить `"platform_compat.h"` — всё остальное разрешается автоматически в зависимости от `_WIN32`.
### Байтовый порядок
```c
uint16_t v16 = htobe16(x); // host → big-endian
uint32_t v32 = be32toh(x); // big-endian → host
uint64_t v64 = be64toh(x); // big-endian → host
// На Linux использует <endian.h>, на Windows — _byteswap_*
```
### Криптостойкий random
```c
uint8_t salt[8];
if (random_bytes(salt, sizeof(salt)) != 0) { /* ошибка */ }
// Linux: /dev/urandom, Windows: BCryptGenRandom
```
### Сетевые интерфейсы
```c
// Определить интерфейс маршрута по умолчанию
uint32_t ifidx = get_default_route_netif_index(AF_INET); // или AF_INET6
// Получить IPv4 адрес интерфейса по индексу
uint32_t ipv4 = get_interface_ip_by_index(ifidx); // network byte order
// Получить IPv6 адрес (постоянный или временный)
uint8_t ipv6[16];
if (get_interface_ipv6_by_index(ifidx, 1, ipv6) == 0) { // temporary=1
// ipv6 содержит 16 байт адреса
}
```
**Windows**: `get_interface_ip_by_index`, `get_interface_ipv6_by_index`, `get_default_route_netif_index` — заглушки (возвращают 0/-1).
### Время
```c
struct timeval tv;
utun_gettimeofday(&tv, NULL); // макрос: на Linux → gettimeofday, на Windows → inline-реализация
```
### Прочее
- `strcasecmp`/`strncasecmp` — на Windows `_stricmp`/`_strnicmp`
- `memmem` — на Windows inline-реализация `compat_memmem`
- `pipe` — на Windows `_pipe` с `_O_BINARY`
- `poll` — на Windows `WSAPoll`, флаги `POLLIN`/`POLLOUT` и т.д. определены если отсутствуют
- `ssize_t` — определён если отсутствует
- `fcntl` — на Windows упрощённая реализация только для `F_SETFL`/`O_NONBLOCK`
- `utun_mkdir` — кроссплатформенный `mkdir`
### Ограничения
- Функции сетевых интерфейсов работают только на POSIX, на Windows — заглушки
- `get_default_route_netif_index` создаёт временный UDP-сокет к `8.8.8.8` (IPv4) или `2001:4860:4860::8888` (IPv6) для определения интерфейса
- Только IPv4/IPv6; `family` только `AF_INET` или `AF_INET6`
## 3. API
### Байтовый порядок
| Макрос | Назначение |
|--------|------------|
| `htobe16(x)` / `be16toh(x)` | 16-bit host ↔ big-endian |
| `htobe32(x)` / `be32toh(x)` | 32-bit host ↔ big-endian |
| `htobe64(x)` / `be64toh(x)` | 64-bit host ↔ big-endian |
### Основные функции
| Функция | Назначение |
|---------|------------|
| `random_bytes(buffer, len)` | Криптостойкие случайные байты. 0 — успех, -1 — ошибка |
| `get_interface_ip_by_index(ifindex)` | IPv4 адрес интерфейса в network byte order, 0 при ошибке |
| `get_interface_ipv6_by_index(ifindex, temporary, out)` | IPv6 адрес (temporary=1 — временный privacy-адрес), 0 — успех |
| `get_default_route_netif_index(family)` | ifindex дефолтного маршрута через connect() к внешнему IP |
### Макросы-заменители (условные)
| Макрос | Назначение |
|--------|------------|
| `utun_gettimeofday(tv, tz)` | gettimeofday (inline на Windows) |
| `utun_mkdir(path, mode)` | mkdir |
+23 -7
View File
@@ -24,12 +24,16 @@
* • Для linked list в буфере сохраняется только количество узлов + сырые данные узлов
* (включая байты next/padding; decode заменяет next восстановленными ссылками).
* • serialize_decode принимает буфер БЕЗ заголовка.
* • serialize_free освобождает ВСЮ структуру и все вложенные динамические объекты.
* • serialize_free освобождает структуру, динамические поля схемы и узлы списков; обнуляет указатель на структуру.
* Указатели внутри элементов массивов и узлов списков рекурсивно не освобождаются.
* • Поля кодируются в порядке schema.fields; фиксированные данные копируются без смены endian.
* • max_size (если > 0) — лимит encode; header при header_len > 0 обязателен.
* • Переменные counts ограничены 65535; elem_size=1 добавляет NUL и для byte-массивов.
* • При encode counts больше 65535 молча усекаются: сохраняются первые 65535 элементов массива/списка или байтов строки.
* • elem_size=1 добавляет NUL и для byte-массивов.
* • Linked nodes копируются целиком, без рекурсивного кодирования вложенных указателей.
* • Этот формат зависит от ABI сырых полей; сериализация не нормализует размер/выравнивание структур.
* • Схема доверенная: offsets, типы, размеры и выравнивание полей/next проверяет её автор, не decode.
* • Независимые операции не используют общее изменяемое состояние; доступ к одной структуре согласует владелец.
*
* @example
* // 1. Описание структуры
@@ -103,7 +107,7 @@ struct SerializeField {
uint16_t offset; ///< смещение поля в структуре
uint8_t elem_size; ///< размер элемента (для fixed = размер данных, для array/linked = размер одного элемента)
uint8_t type; ///< SERIALIZE_TYPE_*
uint16_t len_offset; ///< смещение поля-счётчика (игнорируется для type 0,1,2)
uint16_t len_offset; ///< type 3–5: смещение счётчика в структуре; type 6: смещение next в узле; type 0–2: игнорируется
};
/**
@@ -114,25 +118,37 @@ struct SerializeSchema {
uint16_t struct_size; ///< размер основной структуры
uint16_t max_size; ///< максимальный размер сериализованных данных (0 = без ограничения)
uint16_t header_len; ///< длина заголовка
const struct SerializeField* fields;
const struct SerializeField* fields; ///< field_count описателей в порядке записи; схема заимствуется на время вызова
};
#define SERIALIZE_ERR_OK 0 ///< успех
#define SERIALIZE_ERR_BUF -1 ///< ошибка выделения памяти
#define SERIALIZE_ERR_BUF -1 ///< нехватка памяти; decode также: обрезанные данные или неизвестный тип поля
#define SERIALIZE_ERR_NULL -2 ///< NULL-указатель
#define SERIALIZE_ERR_SIZE -3 ///< превышен max_size
#define SERIALIZE_ERR_NOTSUP -4 ///< неподдерживаемый тип
#define SERIALIZE_ERR_SIZE -3 ///< encode: превышен max_size
#define SERIALIZE_ERR_NOTSUP -4 ///< encode: неподдерживаемый тип поля
/* Кодирует structure по schema; при header_len > 0 header указывает минимум на header_len байт.
* При успехе *out_buf — новый блок u_malloc (освободить u_free), *out_len — фактически записанная длина с header.
* Входная структура и её вложенные данные остаются у владельца. На ошибке выходные значения не меняются.
* Возврат: OK, NULL (обязательные аргументы), BUF (OOM), SIZE (max_size), NOTSUP (неизвестный тип).
* ASCIIZ требует elem_size=1; порядок fields задаёт формат, сортировка по offset не требуется. */
int serialize_encode(const void* structure,
const struct SerializeSchema* schema,
const uint8_t* header,
uint8_t** out_buf,
size_t* out_len);
/* Декодирует in_len байт БЕЗ header: передавать buf + header_len, len - header_len.
* При успехе *structure — новая структура с выделенными полями; очищать serialize_free с той же схемой.
* При ошибке частично созданные объекты освобождаются, *structure = NULL. Старый *structure не освобождает.
* max_size не ограничивает decode; хвост после последнего поля игнорируется.
* Возврат: OK, NULL (аргументы), BUF (OOM, обрезанные данные или неизвестный тип). */
int serialize_decode(const uint8_t* in_buf, size_t in_len,
const struct SerializeSchema* schema,
void** structure);
// Освобождает выделенную через u_malloc структуру, поля схемы и узлы списков, обнуляет *structure; NULL допустим.
// Вложенные указатели внутри элементов массивов/узлов не обходит; к структуре на стеке применять нельзя.
void serialize_free(const struct SerializeSchema* schema, void** structure);
-117
View File
@@ -1,117 +0,0 @@
# Binary Serialization Library
## 1. Назначение
Библиотека бинарной сериализации C-структур с динамическими полями (строки, массивы, singly-linked списки) в компактный бинарный буфер и обратно. Предназначена для кодирования/декодирования сообщений сетевого протокола (контрольный сервер, синхронизация БД, обмен конфигурацией).
Что сериализуется:
- Фиксированные поля (целые, флаги, структуры) — копируются «как есть».
- ASCIIZ-строки (`char*`) — длина определяется через `strlen`, сохраняется с терминальным нулём.
- Динамические массивы (`uint8_t*`) — счётчик элементов в поле `UINT8`/`UINT16`/`UINT32` или ровно 1 элемент (`ARRAY_FIXED`).
- Singly-linked списки — `next`-указатели не сериализуются, сохраняются только `count+data`.
Формат: header (`header_len` байт, передаётся отдельно) + данные полей. Длина переменных полей кодируется 2 байтами (`uint16_t`, big-endian).
Потокобезопасность: нет, однопоточное использование.
## 2. Как пользоваться
```c
#include "../lib/serialize.h"
// --- 1. Определяем структуру ---
typedef struct Node {
uint32_t id;
struct Node *next;
} Node;
typedef struct {
uint8_t version;
uint8_t name_len;
char *name; // ASCIIZ (elem_size=1)
uint16_t addrs_cnt;
uint8_t *addrs; // массив байт (elem_size=1)
Node *list; // linked list
} Message;
// --- 2. Описываем схему ---
static const struct SerializeField msg_fields[] = {
{offsetof(Message, version), sizeof(uint8_t), SERIALIZE_TYPE_FIXED, 0},
{offsetof(Message, name), 1, SERIALIZE_TYPE_ARRAY_U8, offsetof(Message, name_len)},
{offsetof(Message, addrs), 1, SERIALIZE_TYPE_ARRAY_U16, offsetof(Message, addrs_cnt)},
{offsetof(Message, list), sizeof(Node), SERIALIZE_TYPE_LINKED, offsetof(Node, next)}
};
static const struct SerializeSchema msg_schema = {
.field_count = 4,
.struct_size = sizeof(Message),
.max_size = 4096, // 0 = без лимита
.header_len = 4, // байты заголовка (напр. версия протокола)
.fields = msg_fields
};
// --- 3. Сериализация ---
Message msg = { .version = 1, .name = "test", .name_len = 4, .list = NULL };
msg.addrs_cnt = 2;
msg.addrs = u_malloc(2); msg.addrs[0] = 0xAA; msg.addrs[1] = 0xBB;
uint8_t header[] = {0x01, 0x00, 0x00, 0x00}; // 4-байтный заголовок
uint8_t *buf = NULL;
size_t len;
if (serialize_encode(&msg, &msg_schema, header, &buf, &len) != SERIALIZE_ERR_OK)
goto fail;
// buf содержит: header (4) + version (1) + [len=2]name(4) + '\0' + [len=2]addrs(2)
// --- 4. Десериализация (buf без заголовка!) ---
Message *restored = NULL;
uint8_t *data_ptr = buf + msg_schema.header_len; // пропускаем header
if (serialize_decode(data_ptr, len - msg_schema.header_len, &msg_schema, (void**)&restored) != SERIALIZE_ERR_OK)
goto fail;
// restored->name == "test", restored->name_len == 4
// restored->addrs[0] == 0xAA, restored->addrs_cnt == 2
// restored->list == NULL (список пуст — не падает)
// --- 5. Очистка ---
serialize_free(&msg_schema, (void**)&restored); // освобождает всё: name, addrs, list, структуру
u_free(buf);
```
**Ключевые нюансы:**
- Поля в `schema.fields` должны идти в порядке возрастания `offset` в структуре.
- `serialize_decode` принимает буфер **без заголовка** — передаётся `buf + schema.header_len`.
- `serialize_free` освобождает структуру и все вложенные динамические поля (строки, массивы, списки).
- При ошибке `serialize_decode` **автоматически освобождает** всю частично выделенную память → `*structure = NULL`.
- `max_size > 0` — жёсткий лимит, при превышении `SERIALIZE_ERR_SIZE`.
- Длина переменных полей всегда 2 байта (максимум 65535 элементов).
- ASCIIZ-строки (`elem_size=1`) сохраняются с терминальным `\0`; пустая строка → 1 байт (`\0`).
- Для linked list поле `len_offset` указывает смещение `next`-указателя внутри узла.
- Передавать буфер в `serialize_decode` **без заголовка** — он не знает про `header_len`.
## 3. API
| Функция | Назначение |
|---------|-----------|
| `serialize_encode(structure, schema, header, &buf, &len)` | Кодирует структуру. `header` копируется в начало `buf`. |
| `serialize_decode(in_buf, in_len, schema, &structure)` | Декодирует буфер **без заголовка**. При ошибке всё освобождает. |
| `serialize_free(schema, &structure)` | Рекурсивно освобождает структуру и динамические поля, затем обнуляет указатель. |
**Типы полей (`SERIALIZE_TYPE_*`):**
| Тип | `data` | `elem_size` | `len_offset` |
|-----|--------|-------------|--------------|
| `FIXED` (0) | Встроенные данные | Размер поля | Не исп. |
| `ASCIIZ` (1) | `char*` | 1 | Не исп. (длина = `strlen`) |
| `ARRAY_FIXED` (2) | `uint8_t*` (ровно 1 элемент) | Размер элемента | Не исп. |
| `ARRAY_U8` (3) | `uint8_t*` | Размер элемента | Смещение `uint8_t`-счётчика |
| `ARRAY_U16` (4) | `uint8_t*` | Размер элемента | Смещение `uint16_t`-счётчика |
| `ARRAY_U32` (5) | `uint8_t*` | Размер элемента | Смещение `uint32_t`-счётчика |
| `LINKED` (6) | `void*` (голова списка) | Размер узла | Смещение `next` внутри узла |
**Коды возврата:**
- `SERIALIZE_ERR_OK (0)` — успех.
- `SERIALIZE_ERR_BUF (-1)` — ошибка выделения памяти.
- `SERIALIZE_ERR_NULL (-2)` — NULL-указатель во входных параметрах.
- `SERIALIZE_ERR_SIZE (-3)` — превышен `max_size`.
- `SERIALIZE_ERR_NOTSUP (-4)` — неподдерживаемый тип поля.
+3 -2
View File
@@ -82,7 +82,8 @@ static int vad_read_file(const char* path, uint8_t** out, size_t* out_len) {
}
/* Общая инициализация детектора из байтов модели (файл или вшитая модель).
* data может указывать на внешний буфер (вшитая модель) — тогда не освобождается. */
* owned_buf передаётся детектору только при успехе; при ошибке остаётся у вызывающего.
* Для вшитой модели owned_buf == NULL, её байты не освобождаются. */
static silero_vad_t* silero_vad_create_from_bytes(const uint8_t* data, size_t len,
const uint8_t* owned_buf, const char* label) {
silero_vad_t* vad = NULL;
@@ -107,7 +108,6 @@ static silero_vad_t* silero_vad_create_from_bytes(const uint8_t* data, size_t le
return NULL;
}
vad->ort = ort;
vad->model_data = (uint8_t*)owned_buf;
vad->model_len = len;
status = ort->CreateEnv(ORT_LOGGING_LEVEL_WARNING, "utun_silero_vad", &vad->env);
@@ -127,6 +127,7 @@ static silero_vad_t* silero_vad_create_from_bytes(const uint8_t* data, size_t le
if (status) { vad_report_ort_error(ort, status, "CreateCpuMemoryInfo"); goto fail; }
ort->ReleaseSessionOptions(opts);
vad->model_data = (uint8_t*)owned_buf;
DEBUG_INFO(DEBUG_CATEGORY_VAD, "silero_vad_create(%s): ok bytes=%zu window=%d context=%d input=%d", label, len,
SILERO_VAD_WINDOW_SAMPLES, SILERO_VAD_CONTEXT_SAMPLES, SILERO_VAD_INPUT_SAMPLES);
+22
View File
@@ -4,6 +4,7 @@
#include "socket_compat.h"
#include "debug_config.h"
#include <limits.h>
#ifdef _WIN32
// ==================== Windows Implementation ====================
@@ -211,6 +212,27 @@ int socket_set_mark(socket_t sock, int mark) {
#endif
}
/* Ошибки send остаются у вызывающего; закрытый пир не завершает процесс сигналом. */
ssize_t socket_send(socket_t sock, const void* buf, size_t len) {
#ifdef _WIN32
if (len > INT_MAX) { WSASetLastError(WSAEMSGSIZE); return SOCKET_ERROR_CODE; }
return send(sock, (const char*)buf, (int)len, 0);
#elif defined(MSG_NOSIGNAL)
return send(sock, buf, len, MSG_NOSIGNAL);
#elif defined(SO_NOSIGPIPE)
int enabled = 1;
if (setsockopt(sock, SOL_SOCKET, SO_NOSIGPIPE, &enabled, sizeof(enabled)) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "cannot disable SIGPIPE for socket=%d: %s", (int)sock, strerror(errno));
return SOCKET_ERROR_CODE;
}
return send(sock, buf, len, 0);
#else
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "safe TCP send is unsupported on this platform");
errno = ENOTSUP;
return SOCKET_ERROR_CODE;
#endif
}
ssize_t socket_sendto(socket_t sock, const void* buf, size_t len,
const struct sockaddr* dest, socklen_t dest_len) {
#ifdef _WIN32
+21 -6
View File
@@ -2,6 +2,8 @@
* Единые socket_t, коды ошибок и wrappers сокетов POSIX/Windows (включая MSYS2 UCRT64).
* platform_init/cleanup обслуживают Winsock; UDP/nonblocking/options/sendto/recvfrom — операции ОС.
* Созданные сокеты закрывает владелец через socket_close_wrapper; event loop находится в u_async.h.
* Использование: platform_init → create_udp → set_nonblocking/options → sendto/recvfrom → close → cleanup.
* До регистрации в uasync вызывающий должен включить nonblocking. Winsock init/cleanup согласовать между потоками.
*/
#ifndef SOCKET_COMPAT_H
@@ -45,13 +47,16 @@ extern "C" {
#define ERR_INTR EINTR
#endif
// Platform initialization/cleanup
// POSIX: 0 без системной инициализации. Windows: WSAStartup при первом init, счётчик вложенности; 0 / -1.
int socket_platform_init(void);
// Парный cleanup: Windows уменьшает счётчик и вызывает WSACleanup на нуле; POSIX ничего не освобождает.
void socket_platform_cleanup(void);
// Socket operations
// UDP-сокет для family (обычно AF_INET/AF_INET6); SOCKET_INVALID при ошибке. Владение передаётся вызывающему.
socket_t socket_create_udp(int family);
// Включает nonblocking: 0 — успех, -1 — ошибка.
int socket_set_nonblocking(socket_t sock);
/* Закрыть сокет средствами ОС; на POSIX sock=0 оставляет дескриптор открытым и возвращает 0. */
int socket_close_wrapper(socket_t sock);
static inline int socket_get_error(void) {
#ifdef _WIN32
@@ -61,22 +66,32 @@ static inline int socket_get_error(void) {
#endif
}
// Socket options
// Пробует установить оба буфера SO_SNDBUF/SO_RCVBUF: 0 при успехе обоих, -1 при любой ошибке; отката нет.
int socket_set_buffers(socket_t sock, int sndbuf, int rcvbuf);
// Устанавливает SO_REUSEADDR в значение reuse; 0 / -1.
int socket_set_reuseaddr(socket_t sock, int reuse);
// SO_BINDTODEVICE для строки ifname (не NULL); 0 / -1. Без поддержки опции, в том числе Windows, возвращает -1.
int socket_bind_to_device(socket_t sock, const char* ifname);
// SO_MARK в значение mark; 0 / -1. Без поддержки опции, в том числе Windows, возвращает -1.
int socket_set_mark(socket_t sock, int mark);
// I/O operations
/* Один TCP send без SIGPIPE и изменения обработчиков процесса. Буфер заимствован;
* возвращает число байт либо SOCKET_ERROR_CODE, включая EAGAIN. Ошибку логирует владелец.
* MSG_NOSIGNAL либо SO_NOSIGPIPE; без поддержки возвращает ENOTSUP. Windows: len <= INT_MAX. */
ssize_t socket_send(socket_t sock, const void* buf, size_t len);
// Один sendto/recvfrom с flags=0: число байт или SOCKET_ERROR_CODE; errno/WSA-код читать через socket_get_error.
// буферами владеет вызывающий; для Windows len должен помещаться в int. dest_len — размер адреса назначения;
// src_len при приёме — входная ёмкость/выходная длина адреса (может быть NULL по правилам recvfrom ОС).
ssize_t socket_sendto(socket_t sock, const void* buf, size_t len,
const struct sockaddr* dest, socklen_t dest_len);
ssize_t socket_recvfrom(socket_t sock, void* buf, size_t len,
struct sockaddr* src, socklen_t* src_len);
// Utility
// Заимствованный текст ошибки: Windows — один статический буфер (перезаписывается следующим вызовом), POSIX — strerror.
const char* socket_strerror(int err);
// взять порт из sockaddr_storage
// IPv4/IPv6-порт в host byte order; 0 при NULL или другом семействе.
uint16_t ss_get_port(const struct sockaddr_storage *addr);
// Фактический IPv4/IPv6-порт уже привязанного сокета; 0 при ошибке.
uint16_t socket_get_bound_port(socket_t sock);
-99
View File
@@ -1,99 +0,0 @@
# socket_compat — Cross-platform socket abstraction
## 1. Назначение
Унифицирует работу с UDP-сокетами между POSIX и Windows (MSYS2 UCRT64). Скрывает различия в типах дескрипторов (`int` vs `SOCKET`), инициализации подсистемы (WSAStartup/WSACleanup), кодах ошибок и сигнатурах системных вызовов.
## 2. Как пользоваться
### Инициализация (однократно при старте)
```c
if (socket_platform_init() != 0) { /* фатальная ошибка */ }
// ... работа с сокетами ...
socket_platform_cleanup(); // при завершении
```
На Windows WSAStartup/WSACleanup с refcount — можно вызывать init/cleanup вложенно.
### Создание и настройка сокета
```c
socket_t sock = socket_create_udp(AF_INET); // или AF_INET6
if (sock == SOCKET_INVALID) { /* ошибка */ }
socket_set_nonblocking(sock); // обязательно для u_async
socket_set_reuseaddr(sock, 1);
socket_set_buffers(sock, 256*1024, 256*1024); // SO_SNDBUF/SO_RCVBUF
socket_bind_to_device(sock, "eth0"); // только Linux, SO_BINDTODEVICE
socket_set_mark(sock, 42); // только Linux, SO_MARK
```
### Отправка и приём
```c
ssize_t sent = socket_sendto(sock, buf, len, (struct sockaddr*)&addr, addr_len);
ssize_t recv = socket_recvfrom(sock, buf, sizeof(buf), (struct sockaddr*)&src, &src_len);
uint16_t port = ss_get_port(&src); // порт из sockaddr_storage
```
### Обработка ошибок
```c
if (sent == SOCKET_ERROR_CODE) {
int err = socket_get_error();
if (err == ERR_WOULDBLOCK) { /* нормально для неблокирующего */ }
// на POSIX ERR_WOULDBLOCK == EWOULDBLOCK (может совпадать с EAGAIN)
DEBUG_ERROR(..., "%s", socket_strerror(err));
}
```
### Закрытие
```c
socket_close_wrapper(sock); // на POSIX: close() с защитой от fd 0
```
### Ключевые нюансы
- **Всегда вызывать `socket_platform_init()` перед работой** — иначе на Windows сокеты не будут работать
- **Всегда `socket_set_nonblocking()`** — u_async требует неблокирующих сокетов
- `socket_t` — на POSIX `int`, на Windows `SOCKET` (unsigned)
- `SOCKET_INVALID` — константа для невалидного сокета (`-1` на POSIX, `INVALID_SOCKET` на Windows)
- `SOCKET_ERROR_CODE` — код ошибки сокетных вызовов (`-1` на POSIX, `SOCKET_ERROR` на Windows)
- `socket_strerror()` на Windows использует статический буфер — не thread-safe для параллельного использования
- `socket_bind_to_device` и `socket_set_mark` — только Linux, на остальных платформах возвращают -1 с DEBUG-сообщением
- `socket_close_wrapper` на POSIX не закроет fd 0 (защита)
## 3. API
### Типы и константы
| Имя | Назначение |
|-----|------------|
| `socket_t` | Кроссплатформенный тип дескриптора сокета (`int` / `SOCKET`) |
| `SOCKET_INVALID` | Невалидный сокет (`-1` / `INVALID_SOCKET`) |
| `SOCKET_ERROR_CODE` | Код ошибки сокетного вызова (`-1` / `SOCKET_ERROR`) |
| `ERR_WOULDBLOCK` / `ERR_AGAIN` / `ERR_INTR` | Кроссплатформенные коды ошибок |
### Инициализация
| Функция | Назначение |
|---------|------------|
| `socket_platform_init()` | Инициализация сокетной подсистемы (WSAStartup на Windows). 0 — успех |
| `socket_platform_cleanup()` | Деинициализация (WSACleanup на Windows), refcount |
### Создание и настройка
| Функция | Назначение |
|---------|------------|
| `socket_create_udp(family)` | Создать UDP-сокет (AF_INET или AF_INET6) |
| `socket_set_nonblocking(sock)` | Перевести в неблокирующий режим |
| `socket_set_buffers(sock, snd, rcv)` | Установить SO_SNDBUF и SO_RCVBUF |
| `socket_set_reuseaddr(sock, reuse)` | Установить SO_REUSEADDR |
| `socket_bind_to_device(sock, ifname)` | Привязать к интерфейсу (Linux SO_BINDTODEVICE) |
| `socket_set_mark(sock, mark)` | Установить SO_MARK (Linux) |
### I/O
| Функция | Назначение |
|---------|------------|
| `socket_sendto(sock, buf, len, dest, dest_len)` | Отправить UDP-датаграмму |
| `socket_recvfrom(sock, buf, len, src, src_len)` | Принять UDP-датаграмму |
### Утилиты
| Функция | Назначение |
|---------|------------|
| `socket_get_error()` | Текущий код ошибки (inline: `WSAGetLastError()` / `errno`) |
| `socket_strerror(err)` | Текстовое описание ошибки (Windows: статический буфер, не thread-safe) |
| `socket_close_wrapper(sock)` | Закрыть сокет (POSIX: защита от закрытия fd 0) |
| `ss_get_port(addr)` | Извлечь порт в host byte order из `sockaddr_storage` |
+1 -1
View File
@@ -37,7 +37,7 @@ char *strbuf_detach(struct strbuf *sb);
/* reset len=0, keep allocation */
void strbuf_reset(struct strbuf *sb);
/* printf-like append. auto-grows. returns written bytes or -1 on OOM */
/* printf-добавление с ростом буфера: число байтов без NUL или < 0 при ошибке форматирования/роста. */
int strbuf_addf(struct strbuf *sb, const char *fmt, ...)
__attribute__((format(printf, 2, 3)));
+8 -3
View File
@@ -1,5 +1,6 @@
/* swm_min — минимум последних window_size целых значений (окно по числу добавлений, не по времени).
* create → add/get_min → destroy; память окна выделяется при create, внутренняя синхронизация отсутствует. */
* create → add/get_min → destroy; память O(window_size) выделяется при create, внутренней синхронизации нет.
* Монотонный deque: add амортизированно O(1), get_min O(1); значения int, включая отрицательные. */
#ifndef SLIDING_WINDOW_MIN_H
#define SLIDING_WINDOW_MIN_H
@@ -14,24 +15,28 @@ typedef struct SlidingWindowMin SlidingWindowMin;
/**
* Создаёт новую структуру скользящего минимума.
* @param window_size размер окна (должен быть > 0)
* @return указатель на структуру или NULL при ошибке выделения памяти
* @return указатель на структуру или NULL при window_size <= 0 / ошибке выделения памяти
*/
SlidingWindowMin* swm_create(int window_size);
/**
* Уничтожает структуру и освобождает всю динамически выделенную память.
* @param swm окно; NULL допустим
*/
void swm_destroy(SlidingWindowMin* swm);
/**
* Добавляет новое значение в окно.
* @param swm окно; при NULL ничего не делает
* @param val новое целое значение
* После вызова swm_get_min() сразу вернёт минимум последних min(window_size, добавленных) значений.
*/
void swm_add(SlidingWindowMin* swm, int val);
/**
* Возвращает текущий минимум в окне.
* Если окно пустое — возвращает INT_MAX.
* @param swm окно
* @return минимум; если окно пустое или swm == NULL — INT_MAX
*/
int swm_get_min(const SlidingWindowMin* swm);
-39
View File
@@ -1,39 +0,0 @@
# Sliding Window Minimum (swm_min.h)
## 1. Назначение
Структура данных для отслеживания минимума в скользящем окне фиксированного размера. Добавление значения и запрос минимума — O(1) амортизированное время. Память — O(window_size). Предназначена для отслеживания минимального RTT в ETCP/BBR (в кодовой базе зарезервирована, ожидает интеграции).
## 2. Как пользоваться
Создать окно заданного размера, добавлять значения по мере поступления, запрашивать текущий минимум.
```c
SlidingWindowMin *swm = swm_create(10); // окно из 10 последних значений
for (int i = 0; i < 100; i++) {
swm_add(swm, get_new_rtt());
int min_rtt = swm_get_min(swm); // минимум последних ≤10 значений
}
swm_destroy(swm);
```
**Нюансы:**
- `swm_get_min` на пустом окне возвращает `INT_MAX`
- `window_size` должен быть > 0, иначе `swm_create` вернёт NULL
- Значения — `int`, отрицательные допустимы
## 3. API
### Структура
- **`SlidingWindowMin`** — непрозрачная структура (opaque pointer). Содержит кольцевой буфер значений, deque индексов, позицию и размер окна.
### Функции
- **`swm_create(window_size)`** — выделяет SlidingWindowMin, инициализирует пустой deque и кольцевой буфер размера `window_size`. Возвращает NULL при ошибке выделения памяти.
- **`swm_destroy(swm)`** — освобождает все ресурсы структуры.
- **`swm_add(swm, val)`** — добавляет значение в окно. Автоматически вытесняет индексы, вышедшие за границу окна, и удаляет из deque значения, которые больше текущего (поддерживает монотонный минимум).
- **`swm_get_min(swm)`** — возвращает минимум текущего окна за O(1). Для пустого окна — `INT_MAX`.
### Алгоритм
Использует монотонный deque: в голове deque всегда индекс минимального элемента текущего окна. При добавлении из хвоста удаляются элементы ≥ нового — они никогда не станут минимумом, пока новый элемент в окне.
Тест: `tests/test_swm_min.c`
+130 -63
View File
@@ -5,11 +5,7 @@
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#ifndef _WIN32
#include <unistd.h>
#include <sys/socket.h>
#include <fcntl.h>
#endif
#include <limits.h>
#ifndef MSG_NOSIGNAL
#define MSG_NOSIGNAL 0
#endif
@@ -28,6 +24,38 @@ static void tcp_conn_handle_error(struct tcp_conn* tc, int err);
static void connect_timeout_cb(void* arg);
static const uint8_t tcp_fin_sentinel;
// Сохраняем fd при HUP, но не оставляем level-triggered событие в poll без работы.
// Очередь восстанавливает регистрацию с новым generation; write_buf обслуживается независимо от чтения.
static int sync_socket_monitoring(struct tcp_conn* tc) {
if (tc->destroyed || tc->error || tc->closed || tc->sock == SOCKET_INVALID) return -1;
int read = !tc->read_paused && !tc->read_stopped && !tc->fin_remote;
int write = !tc->connected || tc->write_buf != NULL;
if (tc->hup_received && !read && !write) {
if (tc->socket_id) {
uasync_remove_socket_t(tc->ua, tc->sock); tc->socket_id = NULL;
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: park HUP fd=%d rq=%d paused=%u stopped=%u fin=%u",
(int)tc->sock, tc->read_queue->count, tc->read_paused, tc->read_stopped, tc->fin_remote);
}
tc->write_monitor = 0;
return 0;
}
if (!tc->socket_id) {
tc->socket_id = uasync_add_socket_t(tc->ua, tc->sock, read_cb, write_cb, error_cb, "tcp_io", tc);
if (!tc->socket_id) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot restore socket registration fd=%d", (int)tc->sock);
tcp_conn_handle_error(tc, ENOMEM); return -1;
}
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: restore HUP fd=%d read=%d write=%d", (int)tc->sock, read, write);
}
if (uasync_set_socket_read(tc->ua, tc->socket_id, read) != ERR_OK ||
uasync_set_socket_write(tc->ua, tc->socket_id, write) != ERR_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: monitoring update failed fd=%d read=%d write=%d", (int)tc->sock, read, write);
tcp_conn_handle_error(tc, EIO); return -1;
}
tc->write_monitor = write;
return 0;
}
struct tcp_conn* tcp_conn_create(
struct UASYNC* ua, socket_t sock,
size_t entry_data_size, size_t write_chunk_size,
@@ -119,10 +147,15 @@ void tcp_conn_destroy(struct tcp_conn* tc) {
if (!tc) return;
if (tc->destroyed) return;
tc->destroyed = 1;
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_conn_destroy: fd=%d connected=%d error=%d fin_remote=%d fin_local=%d closed=%d",
(int)tc->sock, tc->connected, tc->error, tc->fin_remote, tc->fin_local, tc->closed);
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_conn_destroy: fd=%d connected=%d error=%d fin_remote=%d fin_pending=%d fin_local=%d closed=%d",
(int)tc->sock, tc->connected, tc->error, tc->fin_remote, tc->fin_pending, tc->fin_local, tc->closed);
if (tc->connect_timer) { uasync_cancel_timeout(tc->ua, tc->connect_timer); tc->connect_timer = NULL; }
if (tc->connect_timer) {
if (uasync_cancel_timeout(tc->ua, tc->connect_timer) != ERR_OK)
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: destroy could not cancel connect timer fd=%d timer=%p",
(int)tc->sock, tc->connect_timer);
tc->connect_timer = NULL;
}
if (tc->socket_id) {
uasync_remove_socket_t(tc->ua, tc->sock);
@@ -153,6 +186,12 @@ void tcp_conn_destroy(struct tcp_conn* tc) {
static void tcp_conn_handle_error(struct tcp_conn* tc, int err)
{
tc->error = 1;
if (tc->connect_timer) {
if (uasync_cancel_timeout(tc->ua, tc->connect_timer) != ERR_OK)
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: error cleanup could not cancel connect timer fd=%d timer=%p err=%d",
(int)tc->sock, tc->connect_timer, err);
tc->connect_timer = NULL;
}
queue_waiter_cancel(tc->read_queue, &tc->read_waiter);
queue_set_empty_callback(tc->read_queue, NULL, NULL);
@@ -194,7 +233,7 @@ static void tcp_conn_handle_error(struct tcp_conn* tc, int err)
static void read_cb(socket_t sock, void* arg) {
(void)sock;
struct tcp_conn* tc = (struct tcp_conn*)arg;
if (!tc || tc->sock == SOCKET_INVALID || !tc->read_queue) return;
if (!tc || tc->sock == SOCKET_INVALID || !tc->read_queue || tc->read_paused || tc->read_stopped || tc->fin_remote) return;
struct ll_entry* e = queue_entry_new_from_pool(tc->entry_pool);
uint8_t* buf = memory_pool_alloc(tc->data_pool);
@@ -202,8 +241,7 @@ static void read_cb(socket_t sock, void* arg) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: read_cb alloc failed fd=%d", (int)tc->sock);
if (e) queue_entry_free(e);
if (buf) memory_pool_free(tc->data_pool, buf);
uasync_set_socket_read(tc->ua, tc->socket_id, 0);
tc->read_paused = 1;
tcp_conn_handle_error(tc, ENOMEM);
return;
}
@@ -215,9 +253,12 @@ static void read_cb(socket_t sock, void* arg) {
queue_data_put(rq, e);
// tc мог быть уничтожен через callback chain — read_queue обнулён
if (rq && tc->read_queue && tc->read_queue->count >= tc->read_high_water && !tc->read_paused) {
uasync_set_socket_read(tc->ua, tc->socket_id, 0);
tc->read_paused = 1;
queue_waiter_wait(tc->read_queue, &tc->read_waiter, resume_read_cb, tc);
if (sync_socket_monitoring(tc) < 0) return;
if (queue_waiter_wait(tc->read_queue, &tc->read_waiter, resume_read_cb, tc) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot wait for read queue fd=%d", (int)tc->sock);
tcp_conn_handle_error(tc, ENOMEM);
}
}
} else if (n == 0) {
memory_pool_free(tc->data_pool, buf);
@@ -225,8 +266,8 @@ static void read_cb(socket_t sock, void* arg) {
if (tc->fin_remote) return;
tc->fin_remote = 1;
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: FIN fd=%d", (int)tc->sock);
uasync_set_socket_read(tc->ua, tc->socket_id, 0);
queue_waiter_cancel(tc->read_queue, &tc->read_waiter);
if (sync_socket_monitoring(tc) < 0) return;
if (tc->read_queue->count == 0) {
if (tc->on_fin) tc->on_fin(tc, tc->arg);
} else {
@@ -244,9 +285,16 @@ static void read_cb(socket_t sock, void* arg) {
static void resume_read_cb(struct ll_queue* q, void* arg) {
(void)q;
struct tcp_conn* tc = (struct tcp_conn*)arg;
if (!tc || tc->sock == SOCKET_INVALID) return;
uasync_set_socket_read(tc->ua, tc->socket_id, 1);
if (!tc || tc->sock == SOCKET_INVALID || tc->destroyed || tc->read_stopped || tc->fin_remote) return;
if (tc->read_queue->count > tc->read_low_water) {
if (queue_waiter_wait(tc->read_queue, &tc->read_waiter, resume_read_cb, tc) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot renew read waiter fd=%d", (int)tc->sock);
tcp_conn_handle_error(tc, ENOMEM);
}
return;
}
tc->read_paused = 0;
sync_socket_monitoring(tc);
}
static void fin_deferred_cb(struct ll_queue* q, void* arg) {
@@ -306,8 +354,7 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
struct ll_entry* e = queue_data_get(q);
if (!e) {
uasync_set_socket_write(tc->ua, tc->socket_id, 0);
tc->write_monitor = 0;
if (sync_socket_monitoring(tc) < 0) return;
DEBUG_TRACE(DEBUG_CATEGORY_SOCKET, "tcp_io: wq_fetch POLLOUT OFF fd=%d (queue empty)", (int)tc->sock);
queue_resume_callback(q);
notify_flushed(tc);
@@ -325,7 +372,14 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
} else if (e->dgram == &tcp_fin_sentinel) {
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: FIN fd=%d", (int)tc->sock);
queue_entry_free(e); queue_resume_callback(q);
shutdown(tc->sock, SHUT_WR);
tc->fin_pending = 0;
if (shutdown(tc->sock, SHUT_WR) != 0) {
int err = socket_get_error();
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: shutdown write failed fd=%d err=%d (%s)",
(int)tc->sock, err, socket_strerror(err));
tcp_conn_handle_error(tc, err);
return;
}
tc->fin_local = 1;
if (tc->on_fin_sent) tc->on_fin_sent(tc, tc->arg);
} else {
@@ -356,8 +410,7 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
tc->write_offset = 0;
memory_pool_free(tc->data_pool, e->dgram);
queue_entry_free(e);
uasync_set_socket_write(tc->ua, tc->socket_id, 1);
tc->write_monitor = 1;
if (sync_socket_monitoring(tc) < 0) return;
DEBUG_TRACE(DEBUG_CATEGORY_SOCKET, "tcp_io: wq_fetch POLLOUT ON fd=%d (partial send, wbuf=%zu bytes)", (int)tc->sock, tc->write_len);
} else if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)) {
tc->write_buf = memory_pool_alloc(tc->data_pool);
@@ -373,8 +426,7 @@ static void write_queue_fetch_cb(struct ll_queue* q, void* arg) {
tc->write_offset = 0;
memory_pool_free(tc->data_pool, e->dgram);
queue_entry_free(e);
uasync_set_socket_write(tc->ua, tc->socket_id, 1);
tc->write_monitor = 1;
if (sync_socket_monitoring(tc) < 0) return;
DEBUG_TRACE(DEBUG_CATEGORY_SOCKET, "tcp_io: wq_fetch POLLOUT ON fd=%d (EAGAIN, wbuf=%zu bytes)", (int)tc->sock, tc->write_len);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: send error fd=%d errno=%d", (int)tc->sock, errno);
@@ -394,7 +446,12 @@ static void write_cb(socket_t sock, void* arg) {
socklen_t len = sizeof(err);
if (getsockopt(tc->sock, SOL_SOCKET, SO_ERROR, (char*)&err, &len) == 0 && err == 0) {
tc->connected = 1;
if (tc->connect_timer) { uasync_cancel_timeout(tc->ua, tc->connect_timer); tc->connect_timer = NULL; }
if (tc->connect_timer) {
if (uasync_cancel_timeout(tc->ua, tc->connect_timer) != ERR_OK)
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: connected socket could not cancel connect timer fd=%d timer=%p",
(int)tc->sock, tc->connect_timer);
tc->connect_timer = NULL;
}
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: connect ok fd=%d", (int)tc->sock);
if (tc->on_connected) tc->on_connected(tc, tc->arg);
if (tc->destroyed) return;
@@ -408,10 +465,7 @@ static void write_cb(socket_t sock, void* arg) {
if (flush_write_buf(tc)) return;
if (!tc->write_buf) {
DEBUG_TRACE(DEBUG_CATEGORY_SOCKET, "tcp_io: write_cb flushed fd=%d wmon=%d (resume WQ cb)", (int)tc->sock, tc->write_monitor);
if (tc->write_queue->waiter_defer) {
uasync_set_socket_write(tc->ua, tc->socket_id, 0);
tc->write_monitor = 0;
}
if (sync_socket_monitoring(tc) < 0) return;
queue_resume_callback(tc->write_queue);
notify_flushed(tc);
}
@@ -427,33 +481,13 @@ static void error_cb(socket_t sock, void* arg) {
if (!tc || tc->sock == SOCKET_INVALID) return;
int so_err = 0; socklen_t so_len = sizeof(so_err);
if (getsockopt(tc->sock, SOL_SOCKET, SO_ERROR, (char*)&so_err, &so_len) == 0 && so_err == 0) {
// Peer полностью закрыл свой сокет (EPOLLHUP) после того, как FIN уже прочитан (fin_remote=1).
// EPOLLHUP — level-triggered и «always reported»: если оставить сокет в epoll, каждая итерация
// epoll_wait мгновенно вернёт HUP и error_cb зациклится на 100% CPU. Закрываем сокет.
if (tc->fin_remote && tc->fin_local) {
// Обе TCP-половины закрыты, но владелец ещё может передавать ранее прочитанные данные.
// Убираем level-triggered HUP из poll; CLOSE остаётся решением владельца.
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: both FINs fd=%d — detach poll, await owner drain", (int)tc->sock);
if (tc->socket_id) uasync_remove_socket_t(tc->ua, tc->sock);
tc->socket_id = NULL; tc->write_monitor = 0;
return;
}
if (tc->fin_remote) {
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: async HUP (graceful) fd=%d — peer fully closed after FIN, closing",
(int)tc->sock);
tcp_conn_push_close(tc);
return;
}
// Грациозное закрытие peer (FIN, EPOLLHUP) при приостановленном чтении: FIN пришёл не как
// EPOLLIN (чтение отключено backpressure-ом), а как HUP. Возобновляем чтение, чтобы
// read_cb дослил остаток и прочитал FIN (recv()==0 → fin_remote → отложенный on_fin).
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: async HUP (graceful) fd=%d fin_r=%d fin_l=%d read_paused=%d rq=%d — resume read",
(int)tc->sock, tc->fin_remote, tc->fin_local, tc->read_paused,
tc->read_queue ? tc->read_queue->count : 0);
queue_waiter_cancel(tc->read_queue, &tc->read_waiter);
uasync_set_socket_read(tc->ua, tc->socket_id, 1);
tc->read_paused = 0;
read_cb(tc->sock, tc);
if (!tc->hup_received)
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: HUP fd=%d fin_r=%u fin_l=%u paused=%u stopped=%u rq=%d",
(int)tc->sock, tc->fin_remote, tc->fin_local, tc->read_paused, tc->read_stopped, tc->read_queue->count);
tc->hup_received = 1;
if (tc->write_buf && flush_write_buf(tc)) return;
if (!tc->fin_remote && !tc->read_paused && !tc->read_stopped) read_cb(tc->sock, tc);
if (!tc->destroyed && !tc->error && !tc->closed) sync_socket_monitoring(tc);
return;
}
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: async error fd=%d connected=%d err=%d so_err=%d fin_r=%d fin_l=%d closed=%d wmon=%d",
@@ -467,11 +501,13 @@ static void error_cb(socket_t sock, void* arg) {
int tcp_conn_push_fin(struct tcp_conn* tc) {
if (!tc || tc->sock == SOCKET_INVALID) { DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin invalid tc"); return -1; }
if (tc->fin_pending) { DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin already pending fd=%d", (int)tc->sock); return -1; }
if (tc->fin_local) { DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin already fin_local fd=%d", (int)tc->sock); return -1; }
if (tc->closed) { DEBUG_WARN(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin already closed fd=%d", (int)tc->sock); return -1; }
struct ll_entry* e = queue_entry_new_from_pool(tc->entry_pool);
if (!e) { DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin entry_pool exhausted fd=%d", (int)tc->sock); return -1; }
e->dgram = (uint8_t*)&tcp_fin_sentinel; e->len = 0;
tc->fin_pending = 1;
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: push_fin fd=%d wq=%d", (int)tc->sock, tc->write_queue->count + 1);
queue_data_put(tc->write_queue, e);
return 0;
@@ -495,9 +531,9 @@ void tcp_conn_set_flushed(struct tcp_conn* tc, void (*on_flushed)(struct tcp_con
void tcp_conn_pause_read(struct tcp_conn* tc) {
if (!tc || tc->sock == SOCKET_INVALID) return;
uasync_set_socket_read(tc->ua, tc->socket_id, 0);
queue_waiter_cancel(tc->read_queue, &tc->read_waiter);
tc->read_paused = 1;
tc->read_stopped = 1; tc->read_paused = 1;
sync_socket_monitoring(tc);
}
// ====================================================================
@@ -514,11 +550,42 @@ static void connect_timeout_cb(void* arg) {
}
}
void tcp_conn_set_connect_timeout(struct tcp_conn* tc, int timeout_ms) {
if (!tc) return;
if (timeout_ms < 0) timeout_ms = 0;
tc->connect_timeout_ms = timeout_ms;
if (!tc->connected && timeout_ms > 0) {
tc->connect_timer = uasync_set_timeout(tc->ua, timeout_ms * 10, tc, connect_timeout_cb, "tcp_conn_ct");
/* Замена атомарна относительно event loop: сначала резервируем новый таймер, затем отменяем прежний. */
int tcp_conn_set_connect_timeout(struct tcp_conn* tc, int timeout_ms) {
if (!tc) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: set connect timeout: tc=NULL timeout=%d", timeout_ms);
return -EINVAL;
}
if (tc->destroyed || tc->error || tc->closed || tc->sock == SOCKET_INVALID) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: set connect timeout on inactive tc fd=%d destroyed=%d error=%d closed=%d",
(int)tc->sock, tc->destroyed, tc->error, tc->closed);
return -EINVAL;
}
if (timeout_ms > INT_MAX / 10) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: connect timeout=%d exceeds limit=%d ms fd=%d",
timeout_ms, INT_MAX / 10, (int)tc->sock);
return -EINVAL;
}
if (timeout_ms < 0) timeout_ms = 0;
void* timer = NULL;
if (!tc->connected && timeout_ms > 0) {
timer = uasync_set_timeout(tc->ua, timeout_ms * 10, tc, connect_timeout_cb, "tcp_conn_ct");
if (!timer) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot create connect timer fd=%d timeout=%d ms; old timeout=%d retained",
(int)tc->sock, timeout_ms, tc->connect_timeout_ms);
return -ENOMEM;
}
}
void* old_timer = tc->connect_timer;
if (old_timer && uasync_cancel_timeout(tc->ua, old_timer) != ERR_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot cancel connect timer fd=%d timer=%p", (int)tc->sock, old_timer);
if (timer && uasync_cancel_timeout(tc->ua, timer) != ERR_OK)
DEBUG_ERROR(DEBUG_CATEGORY_SOCKET, "tcp_io: cannot cancel replacement connect timer fd=%d timer=%p", (int)tc->sock, timer);
return -EIO;
}
tc->connect_timer = timer;
tc->connect_timeout_ms = timeout_ms;
DEBUG_DEBUG(DEBUG_CATEGORY_SOCKET, "tcp_io: connect timeout updated fd=%d connected=%d timeout=%d ms old_timer=%p timer=%p",
(int)tc->sock, tc->connected, timeout_ms, old_timer, timer);
return 0;
}
+51 -19
View File
@@ -9,7 +9,8 @@
// FIN (recv==0) → on_fin (сразу или через empty_callback после дренажа очереди).
//
// Запись: внешний код кладёт entry в write_queue через queue_data_put().
// write_queue_fetch_cb (deferred) → send(), при EAGAIN → write_buf + EPOLLOUT.
// Первый write_queue_fetch_cb может быть синхронным; продолжение — через uasync_call_soon.
// send(), при EAGAIN → write_buf + EPOLLOUT.
// Всё отправлено → on_flushed.
//
// FIN/Close: tcp_conn_push_fin/close ставят сентинел в write_queue.
@@ -18,6 +19,17 @@
//
// Ошибка: любая ошибка сокета → tcp_conn_handle_error → close + null коллбэков → on_error.
// Реализация on_error обязана вызвать tcp_conn_destroy. После возврата tc недействителен.
//
// Все операции и callbacks — в потоке ua. После create установить consumer на read_queue:
// queue_set_callback(tc->read_queue, on_read, arg).
// Consumer извлекает entry, обрабатывает e->len байт, возвращает dgram в tc->data_pool,
// освобождает entry и вызывает queue_resume_callback(q) для продолжения чтения.
// Для записи выделить entry из tc->entry_pool и dgram из tc->data_pool, заполнить len и dgram_pool;
// затем queue_data_put(tc->write_queue, e). Длина должна помещаться в буфер и uint16_t.
// После принятого FIN новые данные не добавлять; закрывать сокет через push_close/destroy.
// HUP не снимает backpressure/явную pause_read: регистрация без доступной работы временно удаляется,
// low_water восстанавливает её с новым поколением uasync. FD/очереди сохраняются до решения владельца.
// Ошибка восстановления/ожидания очереди вызывает on_error, как остальные ошибки TCP.
#ifndef TCP_IO_H
#define TCP_IO_H
@@ -40,15 +52,18 @@ struct tcp_conn {
struct ll_queue* read_queue; // сокет → данные (блоки до entry_data_size)
struct ll_queue* write_queue; // данные → сокет (блоки до write_chunk_size)
int read_high_water;
int read_low_water;
int rcvbuf_size;
uint8_t read_paused;
int read_high_water; // Число входных entry для приостановки чтения
int read_low_water; // Порог очереди для возобновления чтения
int rcvbuf_size; // Запрошенный SO_RCVBUF; <=0 — не настраивать буферы
uint8_t read_paused; // Чтение приостановлено; low_water снимает только автоматическую паузу
uint8_t read_stopped; // Владелец запретил чтение; low_water/HUP не снимает запрет
uint8_t hup_received; // Получен HUP с SO_ERROR=0; idle registration снимается до готовности очереди
uint8_t write_monitor; // 1 = EPOLLOUT активен
uint8_t connected;
uint8_t error;
uint8_t fin_remote; // FIN получен от удалённой стороны (recv == 0)
uint8_t fin_pending; // FIN поставлен в write_queue, shutdown ещё не выполнен
uint8_t fin_local; // FIN отправлен удалённой стороне (shutdown SHUT_WR)
uint8_t closed; // сокет полностью закрыт (close)
uint8_t destroyed; // 1 = tcp_conn_destroy вызван, tc ожидает отложенного free
@@ -71,14 +86,21 @@ struct tcp_conn {
// Коллбэки
void (*on_connected)(struct tcp_conn* tc, void* arg); // исходящий connect завершён
void (*on_fin)(struct tcp_conn* tc, void* arg); // FIN получен от удалённой стороны (fin_remote=1)
void (*on_fin)(struct tcp_conn* tc, void* arg); // FIN получен; вызов сразу при пустой read_queue, иначе после её дренажа
void (*on_fin_sent)(struct tcp_conn* tc, void* arg); // FIN отправлен удалённой стороне (fin_local=1)
void (*on_error)(struct tcp_conn* tc, int err, void* arg); // фатальная ошибка (вызывается после close сокета; обязан вызвать tcp_conn_destroy)
void (*on_flushed)(struct tcp_conn* tc, void* arg); // все данные записи отправлены (write_queue + write_buf пусты)
void (*on_closed)(struct tcp_conn* tc, void* arg); // сокет закрыт через очередь (после close)
void* arg;
void* arg; // Заимствованный контекст всех callbacks, модуль его не освобождает
};
// Регистрирует подключённый/подключающийся неблокирующий sock в ua; сам connect не вызывает.
// entry_data_size — ёмкость recv-блока, write_chunk_size — размер буфера записи; оба 1..UINT16_MAX,
// max этих размеров должен вмещать void* (требование пула). Пороги: 0 <= low < high, проверяет вызывающий.
// rcvbuf_size > 0 пробует SO_SNDBUF=0/SO_RCVBUF=rcvbuf_size; ошибка опции логируется, create продолжается.
// Создаёт две очереди и два пула; on_fin/on_error и arg сохраняются без передачи владения arg.
// Результат — tc или NULL при неверных ua/sock, OOM/ошибке регистрации; при NULL sock остаётся вызывающему.
// При успехе сокетом владеет tc. Пулы и выданные из них блоки живут до отложенного освобождения tc.
struct tcp_conn* tcp_conn_create(
struct UASYNC* ua,
socket_t sock,
@@ -91,37 +113,47 @@ struct tcp_conn* tcp_conn_create(
void (*on_error)(struct tcp_conn* tc, int err, void* arg),
void* arg);
// Дренирует очереди, закрывает сокет, уничтожает пулы, освобождает tc.
// Сразу отменяет callbacks/таймер, дренирует очереди и закрывает сокет.
// Пулы и tc освобождаются через uasync_call_soon в том же uasync-потоке.
// NULL безопасен; повторный destroy допустим только до отложенного free.
// Владелец после destroy больше не использует tc; event loop должен выполнить отложенную очистку.
// Ранее извлечённые entry/dgram вернуть до освобождения пулов; перед destroy ua дренировать call_soon вне callbacks.
void tcp_conn_destroy(struct tcp_conn* tc);
// Запись: queue_data_put(tc->write_queue, entry) напрямую.
// Автозабор (deferred) сам шлёт когда сокет готов.
// Порог: 32 записи, при заполнении queue_waiter_wait.
// Первый автозабор может быть синхронным; queue_resume_callback откладывает продолжение.
// Порог backpressure: count <= 32, не жёсткий лимит ёмкости; производитель ждёт через queue_waiter_wait.
// Обычные буферы записи должны принадлежать data_pool: отправка возвращает их непосредственно в этот пул.
// Поставить сигналы в очередь отправки. Все данные в очереди перед сигналом
// будут отправлены до его обработки. Кодирование: dgram=NULL → close, len=0+dgram!=NULL → FIN.
// будут переданы ОС до его обработки. Сигналы имеют len=0: dgram=NULL → CLOSE,
// dgram=&tcp_fin_sentinel → FIN (внутренний указатель, сигналы ставить только через API).
// tcp_conn_push_fin(tc) — FIN: после отправки предшествующих данных вызывает
// shutdown(SHUT_WR) и on_fin_sent.
// Повторный вызов игнорируется (fin_sent уже установлен).
// Повторный вызов возвращает -1 при fin_pending или fin_local.
// tcp_conn_push_close(tc) — CLOSE: после отправки предшествующих данных закрывает
// сокет (close), вызывает on_closed.
// После close сокет удалён из uasync, tc->closed=1.
// Повторный вызов игнорируется.
// После фактического закрытия повторный вызов возвращает -1.
// Оба API: 0 = сигнал принят; -1 = ошибка. Callback может выполниться до возврата из push.
int tcp_conn_push_fin(struct tcp_conn* tc);
int tcp_conn_push_close(struct tcp_conn* tc);
// Одноразовый коллбэк: вызывается когда write_queue + write_buf полностью опустели.
// После вызова сбрасывается. Установить повторно можно в любой момент.
// Сбрасывается перед вызовом; callback может установить новое уведомление.
// Setter только заменяет функцию (NULL снимает); для уже пустой очереди сам вызов не планирует.
void tcp_conn_set_flushed(struct tcp_conn* tc, void (*on_flushed)(struct tcp_conn* tc, void* arg));
// Принудительная остановка чтения: убирает EPOLLIN и отменяет read_waiter.
void tcp_conn_pause_read(struct tcp_conn* tc);
// Таймаут установления соединения (мс, 0 = без таймаута). Если соединение ещё не
// установлено (connected=0), взводит таймер; по истечении вызывает
// tcp_conn_handle_error(ETIMEDOUT) → on_error. Таймер гасится при успешном connect
// и при tcp_conn_destroy.
void tcp_conn_set_connect_timeout(struct tcp_conn* tc, int timeout_ms);
// Заменить connect-таймаут: мс от текущего вызова, <=0 отменяет таймер.
// Только для живого tc в его uasync-потоке. Для connected=1 таймер не создаётся.
// Допустимый положительный диапазон: <= INT_MAX/10 (uasync принимает 0.1 мс).
// Возврат: 0 = применено; -EINVAL = аргументы/состояние, -ENOMEM = создание таймера,
// -EIO = нарушение состояния отменяемого таймера. При ошибке прежняя настройка сохраняется.
// Функция не вызывает callbacks. Истечение → on_error(ETIMEDOUT); отмена при connect/error/destroy.
int tcp_conn_set_connect_timeout(struct tcp_conn* tc, int timeout_ms);
#ifdef __cplusplus
-155
View File
@@ -1,155 +0,0 @@
# tcp_io — Управление TCP-соединением на uasync + ll_queue
## 1. Назначение
Асинхронный TCP-коннектор, построенный на event loop `uasync` и lock-free очередях `ll_queue`.
Одна структура `struct tcp_conn` = одно TCP-соединение.
Используется в SOCKS/HTTP-прокси, STCP, TCP-прокси и тестах.
**Ключевые свойства:**
- Чтение, запись, ошибки и connect — через `uasync` (epoll/kqueue/poll).
- Два независимых `ll_queue`: `read_queue` (входящие данные) и `write_queue` (исходящие).
- Два memory pool: `entry_pool` (struct ll_entry) и `data_pool` (буферы), всё выделение на горячем пути через пулы.
- Backpressure на чтение: high_water/low_water + `queue_waiter_wait`, EPOLLIN вкл/выкл по уровню заполнения очереди.
- Backpressure на запись: порог 32 записи в write_queue + `queue_waiter_wait`, EPOLLOUT вкл/выкл при частичной отправке.
- Graceful shutdown: FIN (shutdown SHUT_WR) и CLOSE (close сокета) — оба как сентинелы в write_queue, все предшествующие данные гарантированно отправлены.
## 2. Как пользоваться
### Создание соединения
```c
struct tcp_conn* tc = tcp_conn_create(
ua, sock,
1500, // entry_data_size — размер буфера для одного recv
8192, // write_chunk_size — размер буфера для одной send-операции
32, // read_high_water — порог приостановки EPOLLIN
8, // read_low_water — порог возобновления EPOLLIN
0, // rcvbuf_size — размер буфера сокета (0 = не менять)
on_fin_cb, on_error_cb, arg);
```
После создания нужно установить callback на read_queue и включить отложенную обработку:
```c
queue_set_callback(tc->read_queue, on_read_cb, my_conn);
queue_set_waiter_defer(tc->read_queue, 1);
```
### Чтение данных (в on_read_cb)
```c
static void on_read_cb(struct ll_queue* q, void* arg) {
struct my_conn* c = (struct my_conn*)arg;
struct ll_entry* e = queue_data_get(q);
if (!e) { queue_resume_callback(q); return; }
// обработать e->dgram (e->len байт)
queue_entry_free(e);
queue_resume_callback(q); // обязательно!
}
```
### Запись данных
Пользователь напрямую кладёт entry в `write_queue`, отправка авто (deferred callback сам шлёт когда сокет готов):
```c
struct ll_entry* e = queue_entry_new_from_pool(tc->entry_pool);
uint8_t* buf = memory_pool_alloc(tc->data_pool);
memcpy(buf, data, len);
e->dgram = buf; e->len = (uint16_t)len;
queue_data_put(tc->write_queue, e);
```
При заполнении (порог 32) нужно ждать через `queue_waiter_wait`.
### Graceful shutdown
```c
tcp_conn_push_fin(tc); // shutdown(SHUT_WR) после отправки всех данных → on_fin_sent
tcp_conn_push_close(tc); // close сокета после отправки всех данных → on_closed
```
Можно вызывать из callback-ов (on_fin, on_read_cb и т.д.).
Повторные вызовы игнорируются (fin_local=1/closed=1 — возвращают -1).
### Уничтожение
```c
void tcp_conn_destroy(struct tcp_conn* tc);
```
Идемпотентен (`destroyed` флаг). Дренирует обе очереди, закрывает сокет, освобождает пулы через отложенный `uasync_call_soon`.
### Обработка ошибок
`on_error(tc, err, arg)` вызывается после закрытия сокета. **Реализация on_error обязана вызвать tcp_conn_destroy.** После возврата из on_error `tc` недействителен.
```c
static void on_error_cb(struct tcp_conn* tc, int err, void* arg) {
struct my_conn* c = (struct my_conn*)arg;
// почистить свои ресурсы
tcp_conn_destroy(tc); // tc после этого использовать нельзя
socket_close_wrapper(other_sock);
u_free(c);
}
```
### on_flushed (одноразовый)
```c
tcp_conn_set_flushed(tc, my_flushed_cb);
// коллбэк вызовется один раз когда write_queue + write_buf полностью опустеют, затем сбросится
```
### Приостановка чтения
```c
tcp_conn_pause_read(tc); // убирает EPOLLIN и отменяет read_waiter
```
### Ограничения и многопоточность
- **Однопоточный.** Всё работает в одном `uasync` event loop. Нельзя вызывать из другого потока.
- **FIN-сентинел в очереди.** После `push_fin` данные продолжат отправляться, сам FIN выполнится только когда все предшествующие данные отправлены.
- **Нельзя вызывать close сокета напрямую.** Только через `tcp_conn_push_close` — иначе нарушится порядок обработки очереди.
- **Можно вызывать push_fin/push_close из callback-ов** — защита от повторного вызова встроена (fin_local/closed флаги).
## 3. API
### Структуры
| Поле | Назначение |
|------|-----------|
| `struct tcp_conn` | Всё состояние TCP-соединения: сокет, очереди, пулы, коллбэки, флаги |
### Жизненный цикл
| Функция | Описание |
|---------|----------|
| `tcp_conn_create(ua, sock, entry_data_size, write_chunk_size, read_high_water, read_low_water, rcvbuf_size, on_fin, on_error, arg)` | Создать соединение: аллоцирует tc, два memory pool, два ll_queue, регистрирует сокет в uasync с EPOLLIN+EPOLLOUT. Вовращает NULL при ошибке |
| `tcp_conn_destroy(tc)` | Дренирует read_queue и write_queue, закрывает сокет, освобождает пулы через `uasync_call_soon`. Идемпотентен |
### Коллбэки (поля struct tcp_conn)
| Поле | Когда вызывается |
|------|-----------------|
| `on_fin(tc, arg)` | FIN получен от удалённой стороны (recv==0): сразу если read_queue пуста, иначе через `queue_set_empty_callback` после дренажа |
| `on_fin_sent(tc, arg)` | FIN отправлен (`shutdown(SHUT_WR)`) — после отправки всех данных перед FIN-сентинелом |
| `on_error(tc, err, arg)` | Фатальная ошибка сокета. Вызывается после close сокета. **Обязан вызвать tcp_conn_destroy.** После возврата tc недействителен |
| `on_flushed(tc, arg)` | write_queue + write_buf полностью опустели. Одноразовый — после вызова сбрасывается в NULL. Установка: `tcp_conn_set_flushed()` |
| `on_closed(tc, arg)` | Сокет закрыт через CLOSE-сентинел (`tcp_conn_push_close`) после отправки всех предшествующих данных |
### Graceful shutdown
| Функция | Описание |
|---------|----------|
| `tcp_conn_push_fin(tc)` | Помещает FIN-сентинел (`dgram=&tcp_fin_sentinel, len=0`) в write_queue. После отправки предшествующих данных: shutdown(SHUT_WR), fin_local=1, вызывает on_fin_sent. Повторный вызов → -1 |
| `tcp_conn_push_close(tc)` | Помещает CLOSE-сентинел (`dgram=NULL, len=0`) в write_queue. После отправки предшествующих данных: close сокета, удаление из uasync, closed=1, вызывает on_closed. Повторный вызов → -1 |
### Прочее
| Функция | Описание |
|---------|----------|
| `tcp_conn_set_flushed(tc, cb)` | Установить одноразовый коллбэк на полное опустошение write_queue |
| `tcp_conn_pause_read(tc)` | Принудительно выключить EPOLLIN и отменить ожидание низкого порога |
+7 -2
View File
@@ -3,7 +3,12 @@
* cancel помечает запись; её data передаётся free_callback при последующей очистке или destroy.
* Без free_callback data не освобождается. Успешный pop передаёт data вызывающему.
* index_ptr, если задан, должен жить до удаления записи; heap обновляет индекс при перестановках.
* Все операции одного heap выполняются в одном потоке, внутренней синхронизации нет. */
* Все операции одного heap выполняются в одном потоке, внутренней синхронизации нет.
* Внешние индексы 0-based; после удаления их значение больше не определяет запись и не сбрасывается автоматически.
* push/cancel_at — O(log n), cancel с поиском — O(n). Отмена ставит deleted=1, expiration=0 и поднимает запись.
* peek/pop очищают k отменённых корней за O(k log n), вызывая free_callback; успешный pop его не вызывает.
* Равные expiration не гарантируют FIFO. Размер включает ещё не очищенные отменённые записи.
* Пример: create → set_free_callback → push(expiration, data, &живой_индекс) → peek/pop или cancel_at → destroy. */
#ifndef TIMEOUT_HEAP_H
#define TIMEOUT_HEAP_H
@@ -16,7 +21,7 @@ extern "C" {
#include <stdint.h> // For uint64_t
#include <stddef.h> // For size_t
typedef uint64_t TimeoutTime; // Абсолютное время в шкале владельца (uasync использует timebase: 0.1 мс).
typedef uint64_t TimeoutTime; // Абсолютное время в шкале владельца (uasync хранит здесь миллисекунды).
typedef struct {
TimeoutTime expiration; // Sort key (smaller = earlier)
-107
View File
@@ -1,107 +0,0 @@
# timeout_heap — Min-heap для управления таймерами
## 1. Назначение
timeout_heap — легковесная реализация min-heap (двоичная куча с минимумом в корне) для хранения таймеров в порядке возрастания времени срабатывания. Используется модулем `u_async` как внутренняя структура `UASYNC::timeout_heap`.
Ключевая особенность — **ленивое удаление (lazy deletion)**: при отмене таймера элемент не удаляется из кучи, а помечается флагом `deleted = 1`, его `expiration` обнуляется, и он всплывает в корень (bubble-up). Реальное удаление происходит при `peek`/`pop` — корень с `deleted == 1` выкидывается, а данные освобождаются через пользовательский `free_callback`. Это даёт O(log n) на отмену без полного удаления из середины кучи.
## 2. Как пользоваться
Модуль используется только внутри `u_async.c`, пользователь напрямую с ним не работает. Приводится для понимания внутреннего устройства.
### Создание и уничтожение
```c
TimeoutHeap* h = timeout_heap_create(16); // начальная ёмкость 16
timeout_heap_set_free_callback(h, ua, timeout_node_free_callback);
// ... использование ...
timeout_heap_destroy(h); // уничтожает все элементы через free_callback
```
### Вставка и извлечение
```c
size_t index;
timeout_heap_push(h, expiration_ms, my_data, &index);
// index обновляется при перемещениях элемента в куче
TimeoutEntry entry;
if (timeout_heap_peek(h, &entry) == 0) {
// entry.expiration — ближайшее время срабатывания
// entry.data — пользовательские данные
}
if (timeout_heap_pop(h, &entry) == 0) {
// элемент удалён из кучи, данные нужно освободить вручную или через free_callback
}
```
### Отмена
```c
// O(n) — линейный поиск по expiration+data:
timeout_heap_cancel(h, expiration, data);
// O(log n) — удаление по индексу (для внешнего отслеживания позиции):
timeout_heap_cancel_at(h, index, data);
```
### Ленивое удаление: как это работает
1. `cancel` / `cancel_at` ставит `deleted = 1`, `expiration = 0`, вызывает `bubble_up` — элемент всплывает в корень.
2. При следующем `peek`/`pop`: если корень `deleted`, он удаляется из кучи (`remove_root`), данные освобождаются через `free_callback`. Повторяется, пока корень не окажется активным.
3. O(log n) как на cancel, так и на последующее удаление.
## 3. API
### Жизненный цикл
| Функция | Описание |
|---------|----------|
| `timeout_heap_create(capacity)` | Создать кучу с начальной ёмкостью. Расширяется автоматически при заполнении. |
| `timeout_heap_destroy(h)` | Уничтожить кучу. Все данные (включая помеченные deleted) освобождаются через `free_callback` или `free()`. |
| `timeout_heap_set_free_callback(h, user_data, cb)` | Установить callback для освобождения данных при удалении deleted-узлов. Если NULL — данные не освобождаются. |
### Основные операции
| Функция | Описание |
|---------|----------|
| `timeout_heap_push(h, exp, data, &index)` | Вставить элемент с временем срабатывания `exp`. `index_ptr` (опционально) отслеживает позицию элемента в куче — обновляется при всех перемещениях. O(log n). |
| `timeout_heap_peek(h, &entry)` | Посмотреть ближайший **неудалённый** элемент без удаления. Попутно вычищает deleted-корни через `free_callback`. O(log n) в худшем случае. |
| `timeout_heap_pop(h, &entry)` | Извлечь ближайший неудалённый элемент. Аналогично чистит deleted. O(log n). |
### Отмена
| Функция | Описание |
|---------|----------|
| `timeout_heap_cancel(h, exp, data)` | Отменить по совпадению `expiration + data`. Линейный поиск — O(n). Для массового использования предпочитать `cancel_at`. |
| `timeout_heap_cancel_at(h, index, data)` | Отменить по индексу (с проверкой data). O(log n) — ленивое удаление с bubble-up. |
### Статистика
| Функция | Описание |
|---------|----------|
| `timeout_heap_get_size(h)` | Текущий размер кучи (включая deleted-элементы). |
| `timeout_heap_get_freed_count(h)` | Количество узлов, освобождённых через `free_callback` (не реализовано в текущей версии — возвращает 0). |
### Структуры
```c
typedef struct {
TimeoutTime expiration; // время срабатывания (ключ сортировки)
void *data; // пользовательские данные
size_t *index_ptr; // указатель на внешнюю переменную с индексом (обновляется при перемещениях)
int deleted; // 0 = активен, 1 = помечен на удаление
} TimeoutEntry;
struct TimeoutHeap {
TimeoutEntry *heap; // динамический массив
size_t size; // текущее количество элементов
size_t capacity; // выделенная ёмкость
void* user_data; // аргумент для free_callback
void (*free_callback)(void*, void*); // освобождение данных deleted-узлов
};
```
Индексация в коде: внешний API использует 0-based индексы в массиве `heap[]`. Внутренние операции (`bubble_up`, `heapify_down`) оперируют 1-based индексами через макросы `PARENT(i)`, `LEFT_CHILD(i)`, `RIGHT_CHILD(i)` для упрощения арифметики кучи.
+1 -1
View File
@@ -1867,7 +1867,7 @@ err_t uasync_cancel_post(struct UASYNC* ua, struct posted_task* task) {
pthread_mutex_unlock(&ua->posted_lock);
#endif
if (found) u_free(task);
else DEBUG_ERROR(DEBUG_CATEGORY_SYS, "uasync_cancel_post: completion is not pending");
else DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "uasync_cancel_post: completion already taken or absent");
return found ? ERR_OK : ERR_FAIL;
}
+75 -36
View File
@@ -1,7 +1,8 @@
/* Цикл событий одного потока: сокеты (epoll/poll/select), таймеры и FIFO call_soon.
* create → регистрация callbacks → poll/mainloop → destroy вне callbacks после остановки владельцев.
* Таймауты задаются в timebase 0.1мс; arg принадлежит вызывающему, освобождает его callback/владелец.
* API сокетов/таймеров используется в потоке loop. Из другого потока — post/post_reserved и wakeup;
* Таймауты задаются в timebase 0.1мс, положительный абсолютный срок округляется вверх до миллисекунды.
* arg принадлежит вызывающему, освобождает его callback/владелец; отмена и destroy arg не освобождают.
* API сокетов/таймеров используется в потоке loop. Из другого потока — post/post_reserved, wakeup и stop;
* публикация не продлевает время жизни ua/arg, их освобождение нужно согласовать с остановкой producers. */
#ifndef UASYNC_H
@@ -43,12 +44,12 @@ struct timeout_node; // Forward declaration
* До первого замера valid=0; window_us растёт до секунды. При задержке замера
* вклад граничного интервала в среднее оценивается пропорционально времени. */
struct uasync_load {
double current_percent;
double average_percent;
uint64_t sample_time_us;
uint64_t interval_us;
uint64_t window_us;
int valid;
double current_percent; // CPU потока за последний фактический интервал, 0..100%
double average_percent; // Среднее за накопленное окно до секунды, 0..100%
uint64_t sample_time_us; // Время снимка в шкале get_time_us
uint64_t interval_us; // Длительность последнего интервала
uint64_t window_us; // Длительность учтённого окна среднего
int valid; // 1 = снимок готов; 0 = ещё нет замера или ошибка часов
};
struct uasync_load_interval {
@@ -73,17 +74,17 @@ struct UASYNC {
size_t timer_free_count;
size_t socket_alloc_count;
size_t socket_free_count;
// Wakeup pipe for interrupting poll
int wakeup_pipe[2]; // [0] read, [1] write
// Wakeup: eventfd на Linux, pipe на остальных POSIX, UDP-сокеты на Windows
int wakeup_pipe[2]; // [0] чтение, [1] запись; для eventfd значения совпадают
int wakeup_initialized;
// Cached pollfd array for optimized poll operations (fallback for non-Linux)
// Кеш pollfd для POSIX fallback poll, включая Linux без epoll
struct pollfd* poll_fds; // Pre-allocated pollfd array
int poll_fds_capacity; // Current capacity of poll_fds
int poll_fds_count; // Number of valid entries in poll_fds
int poll_fds_dirty; // 1 if poll_fds needs rebuild
// Epoll support (Linux only)
int epoll_fd; // epoll file descriptor (-1 if not using epoll)
int use_epoll; // 1 if using epoll, 0 if using poll
int use_epoll; // 1 = epoll; 0 = poll на POSIX или select на Windows
struct posted_task* posted_tasks_head;
struct posted_task* posted_tasks_tail;
@@ -92,8 +93,8 @@ struct UASYNC {
#else
pthread_mutex_t posted_lock;
#endif
volatile sig_atomic_t stop;
volatile sig_atomic_t running; // 0=shutdown mode (API rejected), 1=full init done
volatile sig_atomic_t stop; // Флаг выхода из mainloop; stop устанавливает, автоматически не сбрасывается
volatile sig_atomic_t running; // Статус, выставляемый владельцем через mark_*; API его не проверяет
uint64_t last_poll_exit_us;
struct uasync_load load;
struct uasync_load_interval load_history[12]; // 10 периодов + граничные интервалы при задержках
@@ -106,82 +107,120 @@ struct UASYNC {
typedef struct UASYNC uasync_t;
typedef struct UASYNC UASYNC_t;
// Атомарно читает статус владельца (NULL → 0); состояние stop и работу poll не определяет.
static inline int uasync_is_running(struct UASYNC* ua) {
if (!ua) return 0;
return __atomic_load_n(&ua->running, __ATOMIC_ACQUIRE);
}
// Instance API - основной API для работы с uasync
// Создаёт готовый к регистрации и poll экземпляр с running=0, stop=0; NULL при ошибке инициализации.
struct UASYNC* uasync_create(void);
// Уничтожение только вне callbacks, после возврата из poll/mainloop.
// Уничтожение вне callbacks после возврата из poll/mainloop и завершения всех producers; NULL допустим.
// close_fds != 0 закрывает оставшиеся зарегистрированные fd; внутренние wakeup/epoll закрываются всегда.
// Ожидающие таймеры и задачи удаляются без вызова callbacks; user_arg остаётся владельцу.
void uasync_destroy(struct UASYNC* ua, int close_fds);
// Атомарно выставляют running=1/0. Не меняют stop, не запускают/останавливают цикл и не запрещают API.
void uasync_mark_running(struct UASYNC* ua);
void uasync_mark_stopped(struct UASYNC* ua);
// текущее время (timebase 0.1ms)
// Время в единицах 0.1мс: Linux CLOCK_BOOTTIME (включает suspend), прочие POSIX CLOCK_MONOTONIC, Windows QPC.
uint64_t get_time_tb(void);
// текущее время в микросекундах (для burst-измерений)
// Время в микросекундах в той же шкале; календарные часы ОС на неё не влияют.
uint64_t get_time_us(void);
// Timeouts, timebase = 0.1 mS
// Как работать с таймаутами если таймаут может быть использован несколько раз:
// 1. заводим дескриптор таймаута и обнуляем
// 2. перед активацией таймаута проверяем дескриптор (t_id) на null
// 3. при cancel или срабатывании дескриптор обнуляем
// 3. после успешного cancel или в начале callback дескриптор обнуляем
// это обеспечит отсутствие утечек и задвоений таймаутов
// Однократный таймер: timeout_tb >= 0, callback не NULL, user_arg передаётся без копирования.
// При timeout_tb > 0 срок = ceil((now_us + timeout_tb*100)/1000) мс; при 0 готов к обработке сразу.
// Callback вызывается при обработке heap, а не синхронно из set_timeout; задержка исполнения возможна.
// name сохраняется по указателю без ограничения в 15 символов; строка должна жить до освобождения узла.
// Возвращает handle или NULL при неверных аргументах/ошибке выделения ресурсов.
void* uasync_set_timeout(struct UASYNC* ua, int timeout_tb, void* user_arg, timeout_callback_t callback, const char* name);
// Только для ещё живого таймера этого ua: ERR_OK при отмене, ERR_FAIL при неверных аргументах/отсутствии в heap.
// Освобождение узла отложено до очистки heap/destroy. После освобождения старый handle использовать нельзя.
err_t uasync_cancel_timeout(struct UASYNC* ua, void* t_id);
// Immediate execution in next mainloop (FIFO order)
// Добавляет callback(user_arg) в FIFO; handle или NULL при неверных аргументах/ошибке выделения.
// Один проход берёт исходную порцию; poll обрабатывает очередь в начале и после ожидания, перед heap.
// Поэтому callback может выполниться в текущем poll. Синхронно из call_soon он не вызывается.
void* uasync_call_soon(struct UASYNC* ua, void* user_arg, timeout_callback_t callback);
// Только для ещё живого узла call_soon этого ua: зануляет callback, освобождение — при обработке/destroy.
// ERR_OK при отмене (включая повторную до обработки узла), ERR_FAIL при NULL/чужом ua.
// После успешной отмены обнулить handle; после исполнения/освобождения обращаться к нему нельзя.
err_t uasync_call_soon_cancel(struct UASYNC* ua, void* t_id);
// Sockets - for regular file descriptors (pipe, file)
// Регистрирует fd с callbacks чтения/записи/исключения (каждый может быть NULL) и user_arg без копирования.
// name — заимствованная строка для логов, должна жить до удаления регистрации; NULL допустим.
// Возвращает handle или NULL при неверном fd, дублирующей регистрации или ошибке ресурсов/backend.
// Handle кодирует индекс без поколения, устойчив к расширению массива, но недействителен после удаления.
void* uasync_add_socket(struct UASYNC* ua, int fd, socket_callback_t read_cbk, socket_callback_t write_cbk, socket_callback_t except_cbk, const char* name, void* user_arg);
// Sockets - for socket_t (cross-platform sockets)
// То же для socket_t; callbacks получают значение sock. Возврат и владение как у uasync_add_socket.
void* uasync_add_socket_t(struct UASYNC* ua, socket_t sock, socket_t_callback_t read_cbk, socket_t_callback_t write_cbk, socket_t_callback_t except_cbk, const char* name, void* user_arg);
// Удаляет регистрацию по актуальному handle, не закрывает fd и не освобождает user_arg; ERR_OK / ERR_FAIL.
// Слот и значение handle могут быть переиспользованы новой регистрацией; старый handle нужно обнулить.
err_t uasync_remove_socket(struct UASYNC* ua, void* s_id);
// Удаляет регистрацию типа SOCK по значению sock; fd остаётся открыт. ERR_OK / ERR_FAIL.
err_t uasync_remove_socket_t(struct UASYNC* ua, socket_t sock);
// Динамическое управление мониторингом чтения/записи для сокета
// Меняет мониторинг чтения/записи по актуальному handle: enable != 0 включает, 0 отключает.
// Callback должен быть задан при регистрации. ERR_OK при успехе, ERR_FAIL при неверном handle/ошибке backend.
err_t uasync_set_socket_read(struct UASYNC* ua, void* s_id, int enable);
err_t uasync_set_socket_write(struct UASYNC* ua, void* s_id, int enable);
// Single iteration of event loop with timeout (timebase units)
// Один проход в потоке владельца: call_soon, ожидание/I/O/posted tasks, call_soon и готовые таймеры.
// timeout_tb < 0 — без пользовательского предела ожидания; 0 — без блокирующего ожидания;
// > 0 — предел системного ожидания в 0.1мс с округлением вверх до миллисекунд, сокращаемый таймерами.
// После stop ожидание неблокирующее. Время callbacks не ограничено timeout_tb; ошибка ожидания может прервать проход.
void uasync_poll(struct UASYNC* ua, int timeout_tb);
// Синхронно опустошает только call_soon, включая новые порции из callbacks; постоянный самоперезапуск мешает возврату.
void uasync_drain_immediate(struct UASYNC* ua);
// Цикл до uasync_stop (возвращается после остановки).
// Повторяет poll(ua, -1), пока stop == 0; уже установленный stop не сбрасывает и сразу возвращается.
void uasync_mainloop(struct UASYNC* ua);
// Атомарно устанавливает stop=1 и пытается разбудить ожидание; допустим из другого потока при живом ua.
// Не прерывает текущий poll/callback и оставшуюся обработку событий. running не изменяет.
void uasync_stop(struct UASYNC* ua);
// Debug statistics
// Копирует накопительные счётчики выделения/освобождения; любой выходной указатель может быть NULL.
void uasync_get_stats(struct UASYNC* ua, size_t* timer_alloc, size_t* timer_free, size_t* socket_alloc, size_t* socket_free);
// Актуализирует снимок по сроку 100 мс. Только в потоке uasync; наружу передавать копию через события.
// ERR_OK при копировании (в том числе с valid=0), ERR_FAIL при NULL ua/load.
err_t uasync_get_load(struct UASYNC* ua, struct uasync_load* load);
// Handle — кодированный индекс; сохраняется при расширении массива сокетов.
// Находит регистрацию по fd: 0 и актуальный handle, либо -1. socket_id обязателен.
// При допустимых аргументах и отсутствии регистрации пишет NULL; при неверных аргументах выход не меняет.
int uasync_lookup_socket(struct UASYNC* ua, int fd, void** socket_id);
// Print all resources (timers, sockets) for debugging
// Выводит счётчики, записи heap таймеров (включая отменённые) и активные сокеты с заданным prefix.
void uasync_print_resources(struct UASYNC* ua, const char* prefix);
// Wakeup mechanism for interrupting poll
// Пробуждение: POSIX пишет uint64_t(1) в eventfd/pipe, Windows посылает байт в UDP-сокет; 0 / -1.
// Допустим из другого потока; ua и внутренний endpoint должны оставаться живы.
int uasync_wakeup(struct UASYNC* ua);
int uasync_get_wakeup_fd(struct UASYNC* ua); // returns write fd for wakeup pipe (for signal handlers)
// Заимствованный endpoint записи или -1 при NULL/неинициализированном wakeup. Не закрывать самостоятельно.
// Linux: eventfd (тот же fd для чтения/записи; писать uint64_t), прочие POSIX: pipe, Windows: UDP-сокет.
int uasync_get_wakeup_fd(struct UASYNC* ua);
// сообщить async (из другого thread) чтобы он вызвал callback с аргументом
// Потокобезопасно публикует callback(user_arg) для потока, обслуживающего ua. ua/callback должны быть не NULL.
// При OOM задача не публикуется, ошибка только логируется; статуса возврата нет. user_arg не копируется.
// При destroy ожидающая задача удаляется без callback; user_arg во всех случаях остаётся владельцу.
void uasync_post(struct UASYNC* ua, uasync_post_callback_t callback, void* user_arg);
/* Передать заранее выделенный u_malloc task без аллокаций. uasync освобождает
* task после callback. Производитель больше не обращается к task после передачи.
* Отмена — в потоке владельца вне callbacks, после завершения производителя;
* ERR_OK означает, что task удалён из очереди и освобождён. */
/* Потокобезопасно передаёт заранее выделенный через u_malloc task без новых аллокаций; ua/task не NULL.
* До передачи задать callback и arg. Производитель после передачи к task не обращается.
* uasync освобождает task после callback, успешной отмены или при destroy без callback; arg не освобождает.
* Публикация не проверяет running/stop. Ошибка wakeup не удаляет уже поставленную задачу и не возвращается. */
void uasync_post_reserved(struct UASYNC* ua, struct posted_task* task);
// Отмена в потоке владельца вне callbacks после завершения производителя; ua/task не NULL.
// ERR_OK — task найден в очереди и освобождён; ERR_FAIL — не найден (в том числе уже взят на исполнение).
err_t uasync_cancel_post(struct UASYNC* ua, struct posted_task* task);
// синхронизация памяти (для доступности из других потоков)
// Lock/unlock posted_lock у живого ua; не ждёт исполнения задач. post уже публикует данные под этим мьютексом.
void uasync_memsync(struct UASYNC* ua);
+82 -40
View File
@@ -5,9 +5,9 @@
u_async — центральный планировщик проекта uTun. Каждый поток, которому нужна асинхронная обработка сокетов, таймеров и отложенных задач, создаёт свой экземпляр `struct UASYNC` и запускает `uasync_mainloop()` / `uasync_poll()`. Модуль объединяет:
- **Сокеты** — мониторинг fd/socket на чтение/запись/ошибки через Linux epoll (предпочтительно) с fallback на poll/select.
- **Таймеры** — однократные таймеры с точностью 0.1 мс, реализованные на базе `timeout_heap` (min-heap) и `memory_pool`.
- **Немедленное выполнение** — FIFO-очередь `uasync_call_soon` для отложенного запуска callback в ближайшей итерации цикла.
- **Межпоточную связь** — `uasync_post` позволяет другому потоку безопасно запланировать callback в главном потоке (wakeup через pipe/UDP-сокет).
- **Таймеры** — однократные таймеры на базе `timeout_heap` (min-heap) и `memory_pool`. Аргумент задаётся в единицах 0.1 мс; положительный абсолютный срок округляется вверх до миллисекунды.
- **Отложенное выполнение** — FIFO-очередь `uasync_call_soon`. Callback может выполниться в текущем `uasync_poll`, при следующем проходе обработки этой очереди.
- **Межпоточную связь** — `uasync_post` / `uasync_post_reserved` публикуют callback для потока, обслуживающего экземпляр (wakeup через eventfd/pipe/UDP-сокет).
На Linux используется epoll, на FreeBSD/Windows — poll/select. Платформенная абстракция прозрачна для вызывающего кода.
@@ -15,22 +15,24 @@ u_async — центральный планировщик проекта uTun.
### Правила
- **Один UASYNC на поток.** Нельзя создать несколько экземпляров и вызывать их из разных потоков.
- **Один обслуживающий поток на экземпляр.** Независимые экземпляры могут работать в разных потоках. Сокеты, таймеры и обработка очередей конкретного экземпляра принадлежат его потоку. Из другого потока разрешены `uasync_post`, `uasync_post_reserved`, `uasync_wakeup` и `uasync_stop` при согласованном времени жизни экземпляра.
- **Никаких sleep/usleep.** Если поток заблокирован на sleep, цикл событий стоит. Все ожидания — только через таймеры `uasync_set_timeout`.
- **Закрытие из callback.** Можно удалять сокет и освобождать его пользовательский контекст из callback. Перед следующим callback модуль повторно проверяет регистрацию, поколение и разрешённые события. Для завершения цикла вызывайте `uasync_stop`; `uasync_destroy` выполняйте после возврата из `uasync_poll` / `uasync_mainloop`, вне callbacks.
- **Указатель `user_arg`** передаётся во все callback и позволяет передать контекстную структуру (например, `UTUN_INSTANCE`).
- **Владение контекстом.** `user_arg` не копируется и автоматически не освобождается. При отмене или уничтожении ожидающей задачи её callback не вызывается; контекст освобождает владелец. Перед `destroy` завершите producers, которые могут обращаться к экземпляру или публиковать задачи.
- **Статусы `running` и `stop` независимы.** После `create` оба равны 0, API уже доступен. `mark_running` / `mark_stopped` только выставляют статус владельца; цикл и регистрацию задач не меняют. `stop` устанавливает отдельный флаг выхода из `mainloop`, который автоматически не сбрасывается.
### Типовой сценарий
```c
// 1. Создать uasync
struct UASYNC* ua = uasync_create();
if (!ua) { /* ошибка */ }
if (!ua) return;
// 2. Зарегистрировать сокет
void* sock_id = uasync_add_socket(ua, fd, my_read_cb, NULL, NULL, my_ctx);
// или для socket_t:
void* sock_id = uasync_add_socket_t(ua, sock, my_read_cb_sock, my_write_cb_sock, NULL, my_ctx);
void* sock_id = uasync_add_socket(ua, fd, my_read_cb, NULL, NULL, "my_fd", my_ctx);
// Вместо предыдущей строки для socket_t:
// void* sock_id = uasync_add_socket_t(ua, sock, my_read_cb_sock, my_write_cb_sock, NULL, "my_socket", my_ctx);
// 3. Зарегистрировать таймер (timebase = 0.1 мс, т.е. timeout_tb=100 = 10 мс)
void* t_id = uasync_set_timeout(ua, 100, my_ctx, my_timer_cb, "my_timer");
@@ -39,7 +41,7 @@ void* t_id = uasync_set_timeout(ua, 100, my_ctx, my_timer_cb, "my_timer");
uasync_cancel_timeout(ua, t_id);
t_id = NULL;
// 5. Отложенное выполнение в следующей итерации
// 5. Отложенное выполнение при обработке FIFO call_soon
void* soon_id = uasync_call_soon(ua, my_ctx, deferred_cb);
// 6. Динамически включить/отключить мониторинг записи на сокете
@@ -49,11 +51,11 @@ uasync_set_socket_write(ua, sock_id, 0); // отключить
// 7. Запустить главный цикл (блокирующий, выход через uasync_stop)
uasync_mainloop(ua);
// 8. Или один шаг с таймаутом (timebase, -1 = бесконечно)
uasync_poll(ua, 500); // ждать до 50 мс или первого события
// 8. Вместо mainloop можно вызывать отдельные шаги:
// uasync_poll(ua, 500); // системное ожидание до 50 мс, время callbacks этим не ограничено
// 9. Завершение
uasync_destroy(ua, 1); // close_fds=1 — закрыть все сокеты
uasync_destroy(ua, 1); // закрыть оставшиеся зарегистрированные fd и внутренние ресурсы
```
### Таймеры: правильный паттерн
@@ -85,7 +87,7 @@ if (reconnect_timer) {
// Из другого потока:
void notify_main(void* arg) {
UTUN_INSTANCE* u = arg;
// работаем в главном потоке — можно вызывать любые функции uasync, трогать сокеты и т.д.
// Работаем в потоке, обслуживающем u->ua: здесь можно регистрировать таймеры и сокеты.
uasync_set_timeout(u->ua, 10, u, handle_work, "work");
}
@@ -94,10 +96,21 @@ void thread_func(UTUN_INSTANCE* u) {
uasync_post(u->ua, notify_main, u);
}
// Для гарантии видимости памяти из другого потока перед uasync_post:
uasync_memsync(u->ua);
// Отдельный uasync_memsync перед post не требуется: публикация и получение задачи используют posted_lock.
```
Публикация не продлевает время жизни `ua` или `arg` и не проверяет `running` / `stop`.
Обычный `uasync_post` выделяет `posted_task`; при OOM пишет ошибку в лог и не ставит задачу, статуса возврата нет.
Для завершения worker без выделения памяти заранее выделите `posted_task` через `u_malloc`, заполните `callback` и `arg`
и один раз передайте в `uasync_post_reserved`. После передачи производитель к задаче не обращается.
Задачу освобождает uasync после исполнения, успешной отмены или при уничтожении без вызова callback; `arg` не освобождается.
Ошибка wakeup оставляет уже опубликованную задачу в очереди; обе функции публикации не возвращают эту ошибку.
Отмена через `uasync_cancel_post` выполняется в потоке владельца вне callbacks после завершения производителя.
`ERR_OK` означает, что задача найдена в ожидающей очереди и освобождена; `ERR_FAIL` — не найдена.
Порция, уже взятая на исполнение, в этой очереди не находится. `uasync_memsync` лишь захватывает и отпускает
`posted_lock`; он не ждёт исполнения задач и не заменяет согласование времени жизни контекста.
### Получение времени
```c
@@ -111,52 +124,75 @@ uint64_t now_us = get_time_us(); // микросекунды (для burst-и
| Функция | Описание |
|---------|----------|
| `uasync_create()` | Создать экземпляр: аллоцирует структуру, `socket_array`, `timeout_heap`, `memory_pool`, epoll/poll, wakeup pipe/сокет. |
| `uasync_destroy(ua, close_fds)` | Уничтожить экземпляр. При `close_fds=1` закрывает все зарегистрированные fd. Перед уничтожением выводит диагностику ресурсов и проверяет на утечки (abort при несовпадении аллокаций). |
| `uasync_stop(ua)` | Установить флаг `stop = 1` — на следующей итерации `uasync_mainloop` выйдет. |
| `uasync_mainloop(ua)` | Бесконечный цикл `while(!stop) uasync_poll(ua, -1)`. |
| `uasync_poll(ua, timeout_tb)` | Одна итерация: обработать сокеты + таймеры. `timeout_tb` = максимальное ожидание в timebase; `-1` = ждать следующего таймера (или бесконечно, если нет таймеров). |
| `uasync_create()` | Создать готовый к регистрации и опросу экземпляр с `running=0`, `stop=0`. Возвращает указатель или `NULL` при ошибке инициализации. |
| `uasync_destroy(ua, close_fds)` | Уничтожить вне callbacks после возврата из poll/mainloop и завершения producers. При `close_fds != 0` закрывает оставшиеся зарегистрированные fd; внутренние ресурсы закрывает всегда. Ожидающие задачи и таймеры удаляет без callbacks и без освобождения `arg`. Проверяет баланс внутренних аллокаций (abort при несовпадении). `NULL` допустим. |
| `uasync_mark_running(ua)` / `uasync_mark_stopped(ua)` | Атомарно выставить `running=1/0`. Не меняют `stop`, не управляют циклом и не ограничивают API. |
| `uasync_is_running(ua)` | Атомарно прочитать статус владельца; для `NULL` возвращает 0. |
| `uasync_stop(ua)` | Атомарно выставить `stop=1` и попытаться разбудить ожидание. Допустим из другого потока при живом экземпляре. Текущий poll, оставшуюся обработку событий и callbacks не прерывает; `running` не меняет. |
| `uasync_mainloop(ua)` | Цикл `while(!stop) uasync_poll(ua, -1)`. При уже установленном `stop` сразу возвращается, флаг не сбрасывает. |
| `uasync_poll(ua, timeout_tb)` | Один проход обработки событий. Отрицательное значение снимает пользовательский предел системного ожидания; 0 запрещает блокирующее ожидание; положительное задаёт предел в 0.1 мс с округлением вверх до миллисекунд, сокращаемый таймерами. Время callbacks этим пределом не ограничено. После stop ожидание неблокирующее. |
### Таймеры (timebase = 0.1 мс)
| Функция | Описание |
|---------|----------|
| `uasync_set_timeout(ua, timeout_tb, arg, cb, name)` | Запланировать однократный таймер. Возвращает дескриптор `void*` для отмены. `name` — до 15 символов, используется в логах. |
| `uasync_cancel_timeout(ua, t_id)` | Отменить таймер по дескриптору. После отмены дескриптор нужно обнулить — повторный cancel даст ошибку. |
| `uasync_set_timeout(ua, timeout_tb, arg, cb, name)` | Однократный таймер: `timeout_tb >= 0`, callback обязателен. Возвращает handle или `NULL` при неверных аргументах/ошибке ресурсов. `arg` и строка `name` сохраняются по указателю; ограничения имени в 15 символов нет. |
| `uasync_cancel_timeout(ua, t_id)` | Отменить ещё живой таймер этого экземпляра: `ERR_OK` / `ERR_FAIL`. При успехе узел помечается отменённым; освобождение отложено до очистки heap или destroy. После успеха обнулить handle. |
Для положительного `timeout_tb` абсолютный срок в миллисекундах равен
`ceil((now_us + timeout_tb * 100) / 1000)`, где `now_us` взят при регистрации.
При нулевом интервале срок равен `floor(now_us / 1000)` и таймер готов к ближайшей обработке heap.
Callback синхронно из `set_timeout` не вызывается; его исполнение может задержаться из-за ожидания или других callbacks.
Строка `name` должна оставаться доступной до освобождения узла, включая отложенную очистку после отмены.
В начале callback сохранённый handle нужно обнулить. После исполнения, освобождения или destroy старый указатель
использовать нельзя: безопасная ошибка при повторной отмене не гарантируется.
### Немедленное выполнение (FIFO)
| Функция | Описание |
|---------|----------|
| `uasync_call_soon(ua, arg, cb)` | Запланировать callback на ближайшую итерацию цикла. Используется, когда нужно отложить выполнение на «сразу после текущих событий». Узел выделяется из того же `timeout_pool`. |
| `uasync_call_soon_cancel(ua, t_id)` | Отменить — просто зануляет callback, узел будет освобождён при обработке очереди (O(1)). |
| `uasync_call_soon(ua, arg, cb)` | Добавить callback в FIFO для отложенного исполнения. Возвращает handle или `NULL` при неверных аргументах/ошибке ресурсов; узел выделяется из `timeout_pool`. |
| `uasync_call_soon_cancel(ua, t_id)` | Занулить callback ещё живого узла этого экземпляра за O(1); узел освобождается при обработке очереди/destroy. `ERR_OK` при отмене, `ERR_FAIL` при `NULL`/чужом экземпляре. После успеха обнулить handle. |
| `uasync_drain_immediate(ua)` | Синхронно обработать все порции call_soon до опустошения, включая новые callbacks. Не обрабатывает posted tasks, сокеты или heap таймеров. Постоянный самоперезапуск callback может не дать функции вернуться. |
Один проход обработки FIFO берёт исходную порцию. Обычный успешный `poll` обрабатывает FIFO в начале,
затем после ожидания и обработки I/O/posted tasks, перед проверкой heap таймеров.
Поэтому callback из обработчика сокета или из первой порции call_soon может выполниться в том же `poll`.
Ошибка ожидания может прервать проход до второй обработки FIFO. Гарантии «следующая итерация poll/mainloop» нет.
При повторной отмене call_soon до обработки узла возможен `ERR_OK`; после освобождения узла старый handle недействителен.
### Сокеты
| Функция | Описание |
|---------|----------|
| `uasync_add_socket(ua, fd, r_cb, w_cb, e_cb, arg)` | Добавить fd (pipe, file). `r_cb/w_cb/e_cb` могут быть NULL. Возвращает дескриптор `void*`. |
| `uasync_add_socket_t(ua, sock, r_cb, w_cb, e_cb, arg)` | Добавить `socket_t` (кросс-платформенный сокет). |
| `uasync_remove_socket(ua, s_id)` | Удалить сокет по дескриптору. Помечает слот неактивным, не освобождает индексную ячейку (защита от stale epoll-событий через gen). |
| `uasync_remove_socket_t(ua, sock)` | Удалить по значению `socket_t`. |
| `uasync_set_socket_read(ua, s_id, enable)` | Динамически включить/отключить мониторинг чтения (EPOLL_CTL_MOD). |
| `uasync_set_socket_write(ua, s_id, enable)` | Динамически включить/отключить мониторинг записи. |
| `uasync_lookup_socket(ua, fd, &s_id)` | Найти handle сокета по fd (кодированный индекс, устойчивый к расширению массива). |
| `uasync_add_socket(ua, fd, r_cb, w_cb, e_cb, name, arg)` | Зарегистрировать fd. Callbacks могут быть `NULL`; `name` и `arg` заимствуются. Возвращает handle либо `NULL` при неверном fd, дублирующей регистрации или ошибке ресурсов/backend. |
| `uasync_add_socket_t(ua, sock, r_cb, w_cb, e_cb, name, arg)` | То же для `socket_t`; callbacks получают значение сокета. |
| `uasync_remove_socket(ua, s_id)` | Удалить регистрацию по актуальному handle, не закрывая fd и не освобождая `arg`. Слот становится доступным для повторного использования. `ERR_OK` / `ERR_FAIL`. |
| `uasync_remove_socket_t(ua, sock)` | Удалить регистрацию типа SOCK по значению `socket_t`, не закрывая сокет. `ERR_OK` / `ERR_FAIL`. |
| `uasync_set_socket_read(ua, s_id, enable)` | По актуальному handle включить (`enable != 0`) / отключить (`0`) мониторинг чтения для заданного callback. `ERR_OK` при успехе, `ERR_FAIL` при неверном handle/ошибке backend. |
| `uasync_set_socket_write(ua, s_id, enable)` | То же для мониторинга записи. |
| `uasync_lookup_socket(ua, fd, &s_id)` | 0 и актуальный handle, либо -1. При допустимых аргументах и отсутствии регистрации записывает `NULL`; при неверных аргументах выход не меняет. |
Handle кодирует индекс без поколения и сохраняется при расширении массива. После удаления его нужно обнулить:
новая регистрация может получить то же числовое значение. Поколения защищают внутреннюю обработку событий,
но не делают безопасными вызовы API со старым handle. Строка `name` должна жить до удаления регистрации.
### Межпоточное взаимодействие
| Функция | Описание |
|---------|----------|
| `uasync_post(ua, cb, arg)` | **Потокобезопасно.** Запланировать callback в главном потоке. Выделяет `posted_task`, добавляет в связный список под мьютексом, будит главный поток через `uasync_wakeup`. |
| `uasync_memsync(ua)` | **Потокобезопасно.** Барьер памяти (lock/unlock `posted_lock`) для гарантии видимости данных, записанных из другого потока перед `uasync_post`. |
| `uasync_wakeup(ua)` | Разбудить `poll`/`epoll_wait` записью байта в wakeup pipe (POSIX) или send в UDP-сокет (Windows). Можно вызывать из обработчика сигналов. |
| `uasync_get_wakeup_fd(ua)` | Получить write-fd wakeup pipe для использования в `signalfd` или кастомных механизмах. |
| `uasync_post(ua, cb, arg)` | **Потокобезопасно.** Выделить и опубликовать задачу для потока экземпляра, попытаться разбудить его. При OOM задача не публикуется, ошибка логируется; статуса возврата нет. |
| `uasync_post_reserved(ua, task)` | **Потокобезопасно.** Передать заранее выделенный через `u_malloc` task с заполненными callback/arg без новых аллокаций. `ua` и `task` обязательны. Владение task переходит uasync. |
| `uasync_cancel_post(ua, task)` | В потоке владельца вне callbacks после завершения производителя: `ERR_OK` при удалении и освобождении ожидающего task, `ERR_FAIL`, если он не найден. `ua` и `task` обязательны. |
| `uasync_memsync(ua)` | Lock/unlock `posted_lock` живого экземпляра. Не ждёт исполнения задач; публикация через post уже использует этот мьютекс. |
| `uasync_wakeup(ua)` | 0 при успешной записи, -1 при ошибке/неинициализированном wakeup. POSIX: запись `uint64_t(1)` в eventfd/pipe; Windows: send одного байта в UDP-сокет. Экземпляр должен оставаться живым. |
| `uasync_get_wakeup_fd(ua)` | Заимствованный endpoint записи, либо -1 при `NULL`/неинициализированном wakeup. Linux: eventfd, тот же fd для чтения и записи (писать `uint64_t`); прочие POSIX: pipe; Windows: UDP-сокет. Самостоятельно не закрывать. |
### Время
| Функция | Описание |
|---------|----------|
| `get_time_tb()` | Монотонное время в timebase (0.1 мс). `clock_gettime(CLOCK_MONOTONIC)` на POSIX, `QueryPerformanceCounter` на Windows. |
| `get_time_tb()` | Время в единицах 0.1 мс: Linux `CLOCK_BOOTTIME` (включает suspend), остальные POSIX `CLOCK_MONOTONIC`, Windows `QueryPerformanceCounter`. Изменение календарных часов на шкалу не влияет. |
| `get_time_us()` | Монотонное время в микросекундах (для burst-измерений производительности). |
### Диагностика
@@ -189,9 +225,13 @@ CPU измеряется с первого `uasync_poll` или `uasync_get_load
GUI показывает округлённое секундное среднее 0–100% поверх полосы с градиентом зелёный → жёлтый → красный:
desktop — рядом с заголовком каналов, Android — рядом с версией в списке групп.
Запрос `chat_core_collect_cpu_load_trampoline` возвращает короткое событие `CHAT_EVT_CPU_LOAD` (`valid:1, average_percent:1`).
GUI опрашивает ядро каждые 333 мс, не более одного незавершённого запроса. Android опрашивает только пока экран списка групп RESUMED;
при паузе или уходе с экрана coroutine отменяется, фоновых запросов и таймеров ядра для индикатора нет.
Запрос `chat_core_collect_cpu_load_trampoline` выполняется в потоке ядра и возвращает событие `CHAT_EVT_CPU_LOAD`:
два байта `[valid][average_percent]`, где среднее округлено до целого процента. При `valid=0` GUI показывает 0%.
Desktop-таймер проверяет готовность ядра каждые 333 мс; Android отправляет запросы с паузой 333 мс,
только пока экран списка групп RESUMED. В обоих случаях допускается не более одного незавершённого запроса.
При паузе или уходе с Android-экрана coroutine опроса отменяется и новые запросы не отправляются.
Уже опубликованный запрос не отменяется: ответ снимает pending, но при скрытом экране валидный ответ не меняет индикатор.
Для индикатора не создаются таймеры ядра.
### Внутренние структуры
@@ -200,6 +240,8 @@ GUI опрашивает ядро каждые 333 мс, не более одн
- **`struct socket_array`** — массив сокетов: O(1) доступ по fd через `fd_to_index[]`, обход активных через `active_indices[]`, динамическое расширение.
- **`struct posted_task`** — задача из другого потока: callback + arg + next, защищена `posted_lock`.
Таймеры используют монотонное время: коррекция календарных часов не меняет срок срабатывания. Положительные интервалы округляются вверх до миллисекунд; нулевой интервал готов немедленно. Каждый проход обрабатывает исходную порцию `call_soon`, затем проверяет таймеры и сокеты; новые callbacks остаются на следующий проход. `uasync_drain_immediate` явно обрабатывает все порции до опустошения очереди.
Таймеры используют шкалу `get_time_us`; правила округления абсолютного срока и порядок обработки
call_soon описаны в разделах API выше. Сокеты и posted tasks обрабатываются после ожидания перед heap таймеров;
ошибка ожидания может завершить проход раньше. Установка `stop` не прерывает эти обработчики.
Для fallback `poll` массив дескрипторов резервируется при создании экземпляра и до регистрации нового сокета. Отказ памяти возвращает `NULL` из `uasync_create` / `uasync_add_socket*`; существующие регистрации сохраняются. Перестроение массива в `uasync_poll` не выделяет память. Расширение из callback сохраняет события текущего прохода, новый сокет опрашивается со следующего прохода.
+3 -2
View File
@@ -9,13 +9,14 @@
extern "C" {
#endif
/* data — первый пиксель, size — доступные байты от него; stride — шаг строки/пикселя. */
/* data — первый пиксель, size — доступные байты от него.
* row_stride — шаг строки 1..16777216 байт, pixel_stride — шаг пикселя 1..4096 байт. */
struct video_plane {
uint8_t *data;
size_t size, row_stride, pixel_stride;
};
/* src_width/height — размеры источника; rotation — 0/90/180/270 градусов.
/* src_width/height — размеры источника, каждый 1..4096; rotation — 0/90/180/270 градусов.
* При 90/270 размеры назначения переставлены. Возвращает 0 либо -1 с диагностикой;
* все границы проверяются до первой записи. */
int video_plane_rotate(const struct video_plane *src, const struct video_plane *dst,
+16 -9
View File
@@ -3,6 +3,7 @@
#include "net_emulator.h"
#include "u_async.h"
#include "mem.h"
#include "debug_config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -79,7 +80,7 @@ static int parse_ip_port(const char *str, struct sockaddr_in *addr) {
static int parse_delay_range(const char *str, delay_range_t *range) {
if (!str || !range) return -1;
char *dash = strchr(str, '-');
const char *dash = strchr(str, '-');
if (!dash) {
// Single value
range->min_ms = atoi(str);
@@ -103,10 +104,10 @@ static int parse_delay_range(const char *str, delay_range_t *range) {
static int parse_rule(const char *line, rule_t *rule) {
if (!line || !rule) return -1;
char *prob_start = strstr(line, "prob=");
const char *prob_start = strstr(line, "prob=");
if (!prob_start) return -1;
char *comma = strchr(prob_start, ',');
const char *comma = strchr(prob_start, ',');
if (!comma) return -1;
// Parse probability
@@ -117,7 +118,7 @@ static int parse_rule(const char *line, rule_t *rule) {
if (rule->probability_percent < 0 || rule->probability_percent > 100) return -1;
// Parse delay
char *delay_start = strstr(comma, "delay=");
const char *delay_start = strstr(comma, "delay=");
if (!delay_start) return -1;
char delay_str[64];
@@ -179,6 +180,7 @@ int parse_emulator_config(const char *filename, net_emulator_t *emulator) {
current_endpoint = &emulator->endpoints[emulator->endpoint_count];
memset(current_endpoint, 0, sizeof(endpoint_t));
current_endpoint->sockfd = -1;
// Parse listen address
if (parse_ip_port(endpoint_str, &current_endpoint->listen_addr) < 0) {
@@ -417,7 +419,7 @@ int net_emulator_init(net_emulator_t *emulator) {
if (create_endpoint_socket(ep) < 0) {
fprintf(stderr, "Failed to create socket for endpoint %s:%u\n",
inet_ntoa(ep->listen_addr.sin_addr), ntohs(ep->listen_addr.sin_port));
uasync_destroy(emulator->ua);
uasync_destroy(emulator->ua, 0);
emulator->ua = NULL;
return -1;
}
@@ -425,7 +427,11 @@ int net_emulator_init(net_emulator_t *emulator) {
ep->emulator = emulator;
// Add socket to u_async
uasync_add_socket(emulator->ua, ep->sockfd, socket_callback, NULL, NULL, "emulator", ep);
if (!uasync_add_socket(emulator->ua, ep->sockfd, socket_callback, NULL, NULL, "emulator", ep)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "net_emulator_init: socket registration failed fd=%d endpoint=%d", ep->sockfd, i);
net_emulator_cleanup(emulator);
return -1;
}
printf("[INIT] Listening on %s:%u\n",
inet_ntoa(ep->listen_addr.sin_addr), ntohs(ep->listen_addr.sin_port));
}
@@ -439,7 +445,7 @@ void net_emulator_run(net_emulator_t *emulator) {
if (!emulator || !emulator->running || !emulator->ua) return;
printf("[EMULATOR] Starting main loop. Press Ctrl+C to stop.\n");
uasync_mainloop(emulator->ua); // This never returns
uasync_mainloop(emulator->ua); // Возвращается после uasync_stop.
}
// Clean up emulator resources
@@ -455,7 +461,7 @@ void net_emulator_cleanup(net_emulator_t *emulator) {
}
if (emulator->ua) {
uasync_destroy(emulator->ua);
uasync_destroy(emulator->ua, 0);
emulator->ua = NULL;
}
@@ -466,6 +472,7 @@ void net_emulator_cleanup(net_emulator_t *emulator) {
int main(int argc, char *argv[]) {
setbuf(stdout, NULL);
setbuf(stderr, NULL);
debug_config_init();
if (argc != 2) {
fprintf(stderr, "Usage: %s <config_file>\n", argv[0]);
@@ -500,4 +507,4 @@ int main(int argc, char *argv[]) {
// Cleanup (never reached)
net_emulator_cleanup(&g_emulator);
return 0;
}
}
+30 -22
View File
@@ -1,4 +1,8 @@
// net_emulator.h - Network emulator for testing utun with delay, loss, reordering
/* IPv4 UDP-эмулятор одного потока uasync: правила случайной задержки и остаточная вероятность потерь.
* Обнулить состояние → parse_emulator_config → init → run → cleanup после возврата из run.
* Конфиг: [IPv4:port], строки prob=N,delay=min-max (мс); до 100 endpoint, до 10 правил на endpoint.
* Для каждого пакета выбирается одно правило по накопленной вероятности; остаток до 100% означает drop.
* Разные задержки могут менять порядок пакетов; отдельного механизма перестановки по reorder_probability нет. */
#ifndef NET_EMULATOR_H
#define NET_EMULATOR_H
@@ -22,8 +26,8 @@ typedef struct net_emulator_s net_emulator_t;
// Delay range (milliseconds)
typedef struct {
int min_ms;
int max_ms;
int min_ms; // Минимальная задержка >=0
int max_ms; // Верхняя граница включительно; max >= min, значение <= INT_MAX/10 для timebase
} delay_range_t;
// Rule: probability and delay
@@ -35,16 +39,15 @@ typedef struct {
// Network endpoint configuration
typedef struct {
struct sockaddr_in listen_addr; // Address to listen on
struct sockaddr_in target_addr; // Address to forward to (port - 10000)
struct sockaddr_in target_addr; // Тот же IP; порт listen-10000 при listen>=10000, иначе исходный порт
rule_t rules[MAX_RULES_PER_ENDPOINT];
int rule_count;
int total_probability; // Sum of probabilities (should be <= 100)
int total_probability; // Сумма 0..100%; вероятность потери — 100-total_probability
// For reordering simulation
int reorder_probability; // Probability to reorder with next packet (0-100)
int reorder_probability; // Не используется и не читается из конфигурации
// Socket descriptor
int sockfd;
int sockfd; // Созданный endpoint-сокет; -1 до создания/после cleanup
net_emulator_t* emulator; // Parent emulator instance
} endpoint_t;
@@ -52,34 +55,39 @@ typedef struct {
struct net_emulator_s {
endpoint_t endpoints[MAX_ENDPOINTS];
int endpoint_count;
int running;
uasync_t* ua; // uasync instance
int running; // init завершён; сам флаг не останавливает mainloop, нужен uasync_stop(ua)
uasync_t* ua; // Принадлежит эмулятору, создаётся init и уничтожается cleanup
};
/**
* @brief Parse configuration file
* @param filename Configuration file path
* @param emulator Emulator state to fill
* @return 0 on success, -1 on error
* @brief Читает endpoint и правила; перед первым вызовом обнулить emulator, живой экземпляр не перенастраивать.
* @param filename путь к конфигу; строки с ';' и пустые пропускаются
* @param emulator состояние, в которое добавляются endpoint; прежние записи не очищаются
* @return 0 после разбора; -1 при NULL, ошибке открытия, неверном endpoint, превышении лимитов/суммы вероятностей
* Ошибочное правило логируется и пропускается без -1; прочие нераспознанные строки игнорируются.
* При -1 уже разобранная часть сохраняется. Диапазон задержек должен помещаться в INT_MAX/10 мс.
*/
int parse_emulator_config(const char *filename, net_emulator_t *emulator);
/**
* @brief Initialize network emulator
* @param emulator Emulator state
* @return 0 on success, -1 on error
* @brief Создаёт собственный uasync и неблокирующие UDP-сокеты настроенных endpoint.
* @param emulator состояние после parse, без живого ua; endpoint/rule counts должны помещаться в массивы
* @return 0 — сокеты созданы и зарегистрированы, -1 — NULL/ошибка ресурсов; после ошибки вызвать cleanup
* При ручном заполнении endpoint до init установить sockfd=-1. Ошибка init может оставить созданные сокеты до cleanup.
*/
int net_emulator_init(net_emulator_t *emulator);
/**
* @brief Run network emulator main loop
* @param emulator Emulator state
* @brief Запускает блокирующий mainloop, возвращается после uasync_stop(emulator->ua).
* @param emulator успешно инициализированное состояние; NULL/нет ua/running=0 ничего не делает
* Остановка и cleanup выполняются при согласованном времени жизни состояния, cleanup — вне callbacks.
*/
void net_emulator_run(net_emulator_t *emulator);
/**
* @brief Clean up emulator resources
* @param emulator Emulator state
* @brief Закрывает созданные endpoint-сокеты и уничтожает собственный ua, сбрасывает running.
* @param emulator состояние после init (в том числе неудачного); NULL и повторный cleanup допустимы
* Перед cleanup завершить отложенные отправки: уничтожение таймеров uasync само не освобождает их packet-аргументы.
*/
void net_emulator_cleanup(net_emulator_t *emulator);
@@ -87,4 +95,4 @@ void net_emulator_cleanup(net_emulator_t *emulator);
}
#endif
#endif /* NET_EMULATOR_H */
#endif /* NET_EMULATOR_H */
+9 -3
View File
@@ -2,7 +2,7 @@ bin_PROGRAMS = utun
noinst_LIBRARIES = libutun.a
EXTRA_DIST = lwip_tcp/COPYING dnsmasq/COPYING dnsmasq/COPYING-v3 routing_layer/address_profile.h routing_layer/route_ping_doc.md \
stream/stream_playback_audio.h stream/stream_playback_audio.cpp
stream/stream_playback_audio.h stream/stream_playback_audio.cpp config_ini.h
stream_core_files = \
stream/stream.h \
@@ -79,6 +79,8 @@ utun_CORE_SOURCES = \
transport_layer/stun_nat.h \
transport_layer/etcp_keepalive.c \
transport_layer/etcp_bbr.c \
transport_layer/etcp_tx.c \
transport_layer/etcp_tx.h \
transport_layer/etcp_loadbalancer.c \
transport_layer/etcp_debug.c \
transport_layer/etcp_dump.c \
@@ -99,7 +101,6 @@ utun_CORE_SOURCES = \
transport_layer/pkt_normalizer.c \
transport_layer/packet_dump.c \
transport_layer/etcp_api.c \
transport_layer/etcp_connect.c \
transport_layer/node_conn_direct.c \
transport_layer/socket_monitor.c \
transport_layer/auto_socket.c \
@@ -184,6 +185,8 @@ utun_CORE_SOURCES = \
chat/chat_sync.c \
chat/chat_admin.c \
chat/chat_join.c \
chat/chat_join_port.c \
chat/chat_join_port.h \
chat/member_sync.c \
chat/merkle_sync.c \
chat/merkle_tree.c \
@@ -266,6 +269,8 @@ libutun_a_SOURCES = \
transport_layer/stun_nat.h \
transport_layer/etcp_keepalive.c \
transport_layer/etcp_bbr.c \
transport_layer/etcp_tx.c \
transport_layer/etcp_tx.h \
transport_layer/etcp_loadbalancer.c \
transport_layer/etcp_debug.c \
transport_layer/etcp_dump.c \
@@ -286,7 +291,6 @@ libutun_a_SOURCES = \
transport_layer/pkt_normalizer.c \
transport_layer/packet_dump.c \
transport_layer/etcp_api.c \
transport_layer/etcp_connect.c \
transport_layer/node_conn_direct.c \
transport_layer/socket_monitor.c \
transport_layer/auto_socket.c \
@@ -371,6 +375,8 @@ libutun_a_SOURCES = \
chat/chat_sync.c \
chat/chat_admin.c \
chat/chat_join.c \
chat/chat_join_port.c \
chat/chat_join_port.h \
chat/member_sync.c \
chat/merkle_sync.c \
chat/invite_link.c \
+16 -13
View File
@@ -46,10 +46,13 @@ int broadcast_init(struct TOPO_GROUP* group) {
ctx->messages = queue_new(group->instance->ua, 64, BMSG_UUID_OFFSET, BROADCAST_UUID_SIZE, "broadcast_msg");
if (!ctx->messages) { u_free(ctx); DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_init: queue_new failed"); return -1; }
group->broadcast = ctx;
ctx->cleanup_timer = uasync_set_timeout(group->instance->ua, BROADCAST_CLEANUP_INTERVAL_TB, ctx, broadcast_cleanup_timer_cb, "broadcast_cleanup_timer");
if (!ctx->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_init: cleanup_timer failed grp=%016llx", (unsigned long long)group->group_id);
if (!ctx->cleanup_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_init: cleanup_timer failed grp=%016llx", (unsigned long long)group->group_id);
queue_free(ctx->messages); u_free(ctx);
return -1;
}
group->broadcast = ctx;
DEBUG_INFO(DEBUG_CATEGORY_BGP, "broadcast_init ok grp=%016llx", (unsigned long long)group->group_id);
return 0;
@@ -158,6 +161,7 @@ static void broadcast_fire_cbks(struct broadcast_ctx* ctx, const uint8_t* uuid,
static void broadcast_cleanup_timer_cb(void* arg) {
struct broadcast_ctx* ctx = (struct broadcast_ctx*)arg;
if (!ctx || !ctx->group) return;
ctx->cleanup_timer = NULL;
#ifdef UTUN_HAVE_STANDBY
if (standby_get_sleep_tb() > 0) {
ctx->cleanup_wait = standby_wait(ctx, broadcast_cleanup_timer_cb);
@@ -168,20 +172,19 @@ static void broadcast_cleanup_timer_cb(void* arg) {
uint64_t now_tb = get_time_tb();
int removed = 0;
while (1) {
struct ll_entry* e = queue_data_get(ctx->messages);
if (!e) break;
while (ctx->messages->head) {
struct ll_entry* e = ctx->messages->head;
struct broadcast_msg* msg = (struct broadcast_msg*)e->data;
if (now_tb - msg->local_time_tb > BROADCAST_TTL_TB) {
queue_entry_free(e); removed++;
} else {
queue_data_put_first(ctx->messages, e);
break;
}
if (now_tb - msg->local_time_tb <= BROADCAST_TTL_TB) break;
queue_entry_free(queue_data_get(ctx->messages));
removed++;
}
if (removed > 0) DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "broadcast_cleanup: removed %d stale msgs grp=%016llx", removed, (unsigned long long)ctx->group->group_id);
DEBUG_DEBUG(DEBUG_CATEGORY_BGP, "broadcast_cleanup: grp=%016llx removed=%d cached=%d",
(unsigned long long)ctx->group->group_id, removed, queue_entry_count(ctx->messages));
ctx->cleanup_timer = uasync_set_timeout(ctx->group->instance->ua, BROADCAST_CLEANUP_INTERVAL_TB, ctx, broadcast_cleanup_timer_cb, "broadcast_cleanup_timer");
if (!ctx->cleanup_timer)
DEBUG_ERROR(DEBUG_CATEGORY_BGP, "broadcast_cleanup: timer rearm failed grp=%016llx", (unsigned long long)ctx->group->group_id);
}
void broadcast_recv(struct TOPO_GROUP* group, struct ETCP_CONN* from_conn, const uint8_t* payload, size_t payload_len) {
+18 -5
View File
@@ -2,7 +2,9 @@
* Контекст и кеш увиденных UUID принадлежат группе; TTL — срок хранения UUID, не счётчик сетевых хопов.
* init_instance регистрирует общий ETCP-диспетчер, init(group) создаёт состояние отдельной группы.
* Все вызовы/callbacks — uasync-поток; recv получает заимствованные uuid/data только на время callback.
* send копирует данные и возвращает 0 / -1; 0 не подтверждает доставку каждому участнику. */
* send копирует данные и возвращает 0 / -1; 0 не подтверждает доставку каждому участнику.
* Получение нового UUID вызывает подписчиков; локальный send их не вызывает. Кеш общий для send/recv группы.
* Внутри recv-callback нельзя менять список подписчиков или уничтожать группу/контекст broadcast. */
#ifndef BROADCAST_H
#define BROADCAST_H
@@ -19,25 +21,36 @@ struct UTUN_INSTANCE;
#define BROADCAST_MAX_DATA 1600
#define BROADCAST_UUID_SIZE 16
#define BROADCAST_CLEANUP_INTERVAL_TB 600000 // 1 минута
#define BROADCAST_TTL_TB 1200000 // 2 минуты
#define BROADCAST_TTL_TB 1200000 // 2 минуты; удаление при возрасте > TTL на периодической очистке
// uuid — 16 байт, data — data_len байт; заимствованы только на время вызова. arg — контекст из add_cbk.
typedef void (*broadcast_recv_fn)(const uint8_t* uuid, const uint8_t* data, uint16_t data_len, void* arg);
struct broadcast_cbk_entry {
broadcast_recv_fn fn;
void* arg;
struct broadcast_cbk_entry* next;
broadcast_recv_fn fn; // Функция подписчика
void* arg; // Заимствованный контекст, модуль его не освобождает
struct broadcast_cbk_entry* next; // Следующая регистрация (новые добавляются в начало)
};
struct broadcast_ctx;
// Создаёт кеш и таймер живой group с instance/ua; 0 — успех, -1 — NULL group, повторный init или ошибка ресурсов.
int broadcast_init(struct TOPO_GROUP* group);
// Отменяет таймер/standby-ожидание, освобождает кеш и регистрации, но не arg подписчиков; NULL/нет контекста допустимы.
void broadcast_destroy(struct TOPO_GROUP* group);
// data не NULL даже при data_len=0; data_len <= BROADCAST_MAX_DATA, группа должна быть инициализирована.
// 0 — данные сохранены в кеше и рассылка предпринята; -1 — аргументы/отсутствие контекста/OOM записи кеша.
// Ошибки выделения/отправки отдельным соседям не меняют 0; собственные recv-callbacks не вызываются.
int broadcast_send(struct TOPO_GROUP* group, const uint8_t* data, uint16_t data_len);
// Добавляет fn(arg) в начало; дубликаты разрешены и вызываются отдельно. При NULL/нет контекста/OOM не добавляет.
// arg должен жить до удаления регистрации или destroy; список не менять из recv-callback.
void broadcast_add_cbk(struct TOPO_GROUP* group, broadcast_recv_fn fn, void* arg);
// Удаляет первое совпадение fn+arg (последнее добавленное); отсутствие совпадения ничего не меняет.
void broadcast_remove_cbk(struct TOPO_GROUP* group, broadcast_recv_fn fn, void* arg);
// Регистрирует диспетчер ETCP_ID_BROADCAST для instance; возвращает результат etcp_bind или -1 при NULL.
int broadcast_init_instance(struct UTUN_INSTANCE* instance);
// Снимает общий диспетчер, но не уничтожает контексты отдельных групп; NULL допустим.
void broadcast_destroy_instance(struct UTUN_INSTANCE* instance);
#ifdef __cplusplus
+10 -7
View File
@@ -4,7 +4,7 @@
// медиа (CALL_MEDIA) → HANGUP/ENDED. Звонок из CHAT-группы или DM — модуль
// оперирует (group_id, peer_node_id), маршрутизация через etcp_router.
//
// Сигналинг идёт с ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT, медиа — без подписи.
// Сигналинг, аудио и видео требуют ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT между конечными узлами.
// Реестр сессий: ll_queue с hash-индексом по call_id (8 байт).
//
// Thread-модель: всё в uasync-потоке (single-threaded). Освобождение сессии
@@ -815,9 +815,9 @@ static void call_etcp_recv(struct ETCP_CONN* conn, struct ll_entry* entry) {
const uint8_t* body = d + ROUTER_SVC_PAYLOAD_OFF;
size_t blen = entry->len - ROUTER_SVC_PAYLOAD_OFF;
if (subcmd != CALL_SUBCMD_MEDIA &&
(d[ROUTER_SVC_FLAGS_OFF] & (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) != (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "unauthenticated call signaling command=%u", subcmd);
if ((d[ROUTER_SVC_FLAGS_OFF] & (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) != (ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED)) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "unprotected call packet command=%u peer=%016llx flags=%02x",
subcmd, (unsigned long long)from, d[ROUTER_SVC_FLAGS_OFF]);
queue_dgram_free(entry); queue_entry_free(entry); return;
}
@@ -916,12 +916,15 @@ int call_init(struct UTUN_INSTANCE* inst) {
}
ctx->initialized = 1;
if (etcp_router_bind(inst, ETCP_RT_ID_CALL_AUDIO, call_etcp_recv) || call_video_transport_bind(inst)) {
if (etcp_router_require_crypto(inst, ETCP_RT_ID_CALL, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)
|| etcp_router_bind(inst, ETCP_RT_ID_CALL_AUDIO, call_etcp_recv)
|| etcp_router_require_crypto(inst, ETCP_RT_ID_CALL_AUDIO, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)
|| call_video_transport_bind(inst)) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "call media channel bind failed");
call_destroy(inst); return -1;
}
call_publish_phone_status(inst);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized (svc=0x%02x)", CALL_ID, ETCP_RT_ID_CALL);
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: initialized (svc=0x%02x audio/video E2E=AES-128-CCM+Ed25519 required)", CALL_ID, ETCP_RT_ID_CALL);
return 0;
}
@@ -1012,7 +1015,7 @@ static void call_tx_flush(struct call_session* s) {
uint32_t wait_ms = (uint32_t)(get_time_tb() / 10) - packet.ts_ms;
++s->tx_wait_frames; s->tx_wait_total_ms += wait_ms;
if (wait_ms > s->tx_wait_max_ms) s->tx_wait_max_ms = wait_ms;
if (etcp_route_send(ctx->inst, s->group_id, s->peer_node_id, e, 1, 0) == 0) {
if (etcp_route_send(ctx->inst, s->group_id, s->peer_node_id, e, 1, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT) == 0) {
s->c_tx_frames++;
if (packet.capture_pts_us != INT64_MIN) call_video_audio_sent(s, packet.capture_pts_us + 20000);
} else DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: media enqueue failed id=%016llx", CALL_ID, (unsigned long long)s->call_id);
+30 -15
View File
@@ -1,11 +1,14 @@
// call.h — P2P аудио-звонок (сигналинг + медиа), per-instance
// call — управление P2P аудио/видеозвонком одного узла.
// Начать вызов через start, ответить через accept/decline, завершить через hangup.
// Состояния и результаты приходят событиями CHAT_EVT_CALL_* из chat_event.h.
// Аудио подключается через call_audio.h, видеотрек — через call_video_transport.h или call_video_port.h.
//
// Модуль оперирует (group_id, peer_node_id): звонок можно инициировать из
// CHAT-группы или из DM (group_id — группа маршрутизации, как в dm_core).
// Сигналинг идёт через etcp_router (ROUTE_CRYPTO_SIGN|ENCRYPT), соединение
// Сигналинг, аудио и видео идут через etcp_router с обязательными ROUTE_CRYPTO_SIGN|ENCRYPT; соединение
// оптимизируется через параллельные пробы conn_mgr (DIRECT/REVERSE/INDIRECT).
//
// Все функции — в uasync-потоке. GUI/headless вызывают через trampoline
// Все функции, кроме явно отмеченных, — в uasync-потоке. GUI/headless вызывают через trampoline
// (gui_bridge_post_uasync_fn / uasync_post).
#ifndef CALL_H
#define CALL_H
@@ -20,26 +23,32 @@ struct UTUN_INSTANCE;
extern "C" {
#endif
/* ── жизненный цикл ── */
// ── жизненный цикл ──
int call_init(struct UTUN_INSTANCE* inst); /* из utun_instance.c после dm_core_init */
void call_destroy(struct UTUN_INSTANCE* inst); /* перед dm_core_destroy */
/* ── сигналинг (uasync-поток) ── */
// ── сигналинг (uasync-поток) ──
/* Caller: ch_id — группа либо "dm:<conv_id>" (BGP-группа выбирается при запуске).
* out_call_id — случайный uint64, генерит вызывающий. */
void call_start (struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id, uint64_t out_call_id);
/* Callee: ответить/отклонить. hangup — любая сторона в любой момент. */
// Callee: ответить/отклонить. hangup — любая сторона в любой момент.
void call_accept (struct UTUN_INSTANCE* inst, uint64_t call_id);
void call_decline(struct UTUN_INSTANCE* inst, uint64_t call_id);
void call_hangup (struct UTUN_INSTANCE* inst, uint64_t call_id);
/* Проверка занятости обычным звонком; только uasync, включая входящий/исходящий. */
// Проверка занятости обычным звонком; только uasync, включая входящий/исходящий.
int call_busy(struct UTUN_INSTANCE* inst);
/* Продолжить исходящий вызов после предупреждения о телефонной занятости. Только uasync. */
// Продолжить исходящий вызов после предупреждения о телефонной занятости. Только uasync.
void call_continue(struct UTUN_INSTANCE* inst, uint64_t call_id);
/* Независимые настройки входящих вызовов; запрет выбранного типа приоритетнее проверки занятости. */
// Независимые настройки входящих вызовов; запрет выбранного типа приоритетнее проверки занятости.
enum call_policy {
CALL_POLICY_PUBLISH_PHONE = 1, CALL_POLICY_WARN_PHONE = 2, CALL_POLICY_REJECT_PHONE = 4,
CALL_POLICY_BLOCK_CALLS = 8, CALL_POLICY_BLOCK_RADIO = 16
@@ -47,14 +56,15 @@ enum call_policy {
/* Живое состояние ОС: 0=неизвестно, 1=свободен, 2=занят; policy — маска call_policy.
* Только uasync. Изменение статуса публикуется в BGP, если включён PUBLISH_PHONE. */
void call_set_phone_state(struct UTUN_INSTANCE* inst, int state, unsigned policy);
/* Флаги BGP PHONE_KNOWN/BUSY с учётом звонка приложения и разрешения публикации.
* Desktop без звонка свободен; у mobile состояние ОС может быть неизвестно. Только uasync. */
uint8_t call_phone_status(struct UTUN_INSTANCE* inst);
/* Read-only онлайн-проверка (GUI может звать из любого потока). */
// Read-only онлайн-проверка (GUI может звать из любого потока).
int call_is_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t peer_node_id);
/* ── медиа (релей Opus-кадров, uasync-поток) ── */
// ── медиа (релей Opus-кадров, uasync-поток) ──
/* Отправить закодированный Opus-кадр пиру (оборачивает в CALL_MEDIA).
* Только когда звонок ACTIVE. seq/ts выставляются автоматически. */
@@ -62,9 +72,10 @@ int call_send_media(struct UTUN_INSTANCE* inst, uint64_t call_id, const uint8_t*
const struct call_capture_time* timing);
/* Коллбэк приёма медиа: вызывается в uasync-потоке на каждый Opus-кадр пира.
* Используется headless-аудио-сокетом (и в фазе 3 — декодером GUI). */
* Используется headless-аудиосокетом; opus заимствован до возврата callback. */
typedef void (*call_media_recv_fn)(struct UTUN_INSTANCE* inst, uint64_t call_id,
const uint8_t* opus, int len, void* arg);
void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, void* arg);
/* ── аудио-движок (опционально, GUI-сборки) ──
@@ -73,7 +84,7 @@ void call_set_media_recv_cb(struct UTUN_INSTANCE* inst, call_media_recv_fn fn, v
* и get_stats для события CALL_STATS. Демон без аудио (headless) — ops не ставит. */
struct call_audio_ops {
/* uasync-поток: принятый Opus-кадр пира (декод + jitter-буфер). */
// uasync-поток: принятый Opus-кадр пира (декод + jitter-буфер).
void (*on_media)(struct UTUN_INSTANCE* inst, uint64_t call_id,
uint16_t seq, uint32_t ts_ms, const uint8_t* opus, int len, const struct call_capture_time* timing);
/* uasync-поток: текущие метрики джиттер-буфера (buffer_ms, tempo_x100,
@@ -83,7 +94,7 @@ struct call_audio_ops {
};
void call_set_audio_ops(struct UTUN_INSTANCE* inst, const struct call_audio_ops* ops);
/* Отправить Opus-кадр из ДРУГОГО потока (GUI-аудио): post в uasync через trampoline. */
// Отправить Opus-кадр из ДРУГОГО потока (GUI-аудио): post в uasync через trampoline.
struct call_media_arg {
struct UTUN_INSTANCE* inst;
uint64_t call_id;
@@ -93,7 +104,7 @@ struct call_media_arg {
};
void call_send_media_trampoline(void* arg);
/* ── trampoline-структуры для gui_bridge_post_uasync_fn / uasync_post ── */
// ── trampoline-структуры для gui_bridge_post_uasync_fn / uasync_post ──
struct call_start_arg {
struct UTUN_INSTANCE* inst;
@@ -108,9 +119,13 @@ struct call_id_arg {
};
void call_start_trampoline (void* arg);
void call_accept_trampoline (void* arg);
void call_decline_trampoline(void* arg);
void call_hangup_trampoline (void* arg);
void call_continue_trampoline(void* arg);
#ifdef __cplusplus
+8 -3
View File
@@ -198,8 +198,9 @@ static void ca_parse_frames(struct call_audio_client* cli) {
CH_ID, flen, CA_PCM_FRAME_BYTES);
} else {
opus_codec_encoder_t* enc = ca_encoder(ca, call_id);
int16_t pcm[CA_FRAME_SAMPLES]; memcpy(pcm, data, sizeof(pcm)); // wire header 11 байт не выровнен для int16_t
uint8_t opus[256];
int olen = enc ? opus_codec_encode(enc, (const int16_t*)data, CA_FRAME_SAMPLES, opus, (int)sizeof(opus)) : -1;
int olen = enc ? opus_codec_encode(enc, pcm, CA_FRAME_SAMPLES, opus, (int)sizeof(opus)) : -1;
if (olen <= 0) {
ca->c_enc_dropped++;
DEBUG_WARN(DEBUG_CATEGORY_CALL, "%s: encode failed (olen=%d) call_id=%016llx",
@@ -346,7 +347,10 @@ int call_headless_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst,
call_set_media_recv_cb(inst, ca_media_recv, ca);
ca->cleanup_timer = uasync_set_timeout(ua, 5000, ca, ca_cleanup, "ca_cleanup");
if (!ca->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: cleanup timer scheduling failed", CH_ID);
if (!ca->cleanup_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_CALL, "%s: cleanup timer scheduling failed", CH_ID);
call_headless_destroy(inst); return -1;
}
ca->running = 1;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket listening on %s:%u", CH_ID, bind_ip, bound_port);
@@ -361,7 +365,7 @@ uint16_t call_headless_get_port(struct UTUN_INSTANCE* inst) {
void call_headless_destroy(struct UTUN_INSTANCE* inst) {
struct call_audio* ca = ca_of(inst);
if (!ca || !ca->running) return;
if (!ca) return;
ca->running = 0;
if (ca->cleanup_timer) { uasync_cancel_timeout(ca->ua, ca->cleanup_timer); ca->cleanup_timer = NULL; }
call_set_media_recv_cb(inst, NULL, NULL);
@@ -379,4 +383,5 @@ void call_headless_destroy(struct UTUN_INSTANCE* inst) {
ca->clients = NULL; ca->client_count = 0;
DEBUG_INFO(DEBUG_CATEGORY_CALL, "%s: audio socket destroyed (enc=%u dec=%u enc_drop=%u dec_drop=%u)",
CH_ID, ca->c_enc_frames, ca->c_dec_frames, ca->c_enc_dropped, ca->c_dec_dropped);
inst->call_audio = NULL; u_free(ca);
}
+5 -3
View File
@@ -1,8 +1,10 @@
// call_proto.h — wire-формат и константы P2P аудио-звонка (svc_id ETCP_RT_ID_CALL 0x36)
// call_proto.h — wire-формат сигналинга (ETCP_RT_ID_CALL 0x36) и аудио (ETCP_RT_ID_CALL_AUDIO 0x3F).
// CALL_MEDIA несёт только CALL_TRACK_AUDIO; видео использует call_video.h и отдельные каналы
// ETCP_RT_ID_CALL_VIDEO_CONTROL 0x3D / ETCP_RT_ID_CALL_VIDEO 0x3E (call_video_transport.h).
//
// Приём/отправка через etcp_router. На проводе первый байт payload = subcmd.
// Сигналинг идёт с ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT, медиа — без подписи
// (уже внутри шифрованного канала etcp_router).
// Сигналинг и медиа требуют ROUTE_CRYPTO_SIGN|ROUTE_CRYPTO_ENCRYPT: шифрование тела
// и подпись заголовка/шифртекста между конечными узлами, независимо от маршрута.
#ifndef CALL_PROTO_H
#define CALL_PROTO_H
+51 -13
View File
@@ -1,8 +1,8 @@
/* Один платформенный видеозвонок процесса. Attach/detach — uasync; остальные API
* потокобезопасны. Detach отменяет post до уничтожения ядра. Worker не хранит inst.
* Один TX mailbox (до восьми кадров, каждый <=2MiB) + восемь RX кадров <=4MiB;
* владение pull.data передаётся через u_free. TX не ждёт накопления пачки.
* Никаких callbacks в UI: status/pull читает worker. Кодирование не блокирует uasync. */
/* call_video_port — обмен готовыми видеокадрами между платформенными workers и ядром звонка.
* Attach подключает ядро; для активного call_id: open → send/pull/status → close. Перед отправкой настроить encoder через format.
* Attach/detach — uasync; остальные API потокобезопасны. До detach остановить пользователей порта.
* Send копирует кадры; pull передаёт буфер вызывающему, освобождение — u_free(). Команды исполняются в uasync.
* В процессе один порт: TX до восьми кадров по 2 МиБ, RX до восьми кадров и 4 МиБ суммарно. */
#ifndef CALL_VIDEO_PORT_H
#define CALL_VIDEO_PORT_H
#include "call_video.h"
@@ -11,40 +11,78 @@ struct UTUN_INSTANCE;
extern "C" {
#endif
struct call_video_port_status {
struct call_video_caps peer;
uint64_t key_serial, rate_generation, rate_request, configured_generation;
uint32_t rate_bps;
uint32_t minimum_bps, maximum_bps;
int have_peer, error;
struct call_video_caps peer; // принятые возможности кодеков пира
uint64_t key_serial, rate_generation, rate_request, configured_generation; // счётчик запросов KEY, ID команды битрейта и настроенное поколение
uint32_t rate_bps; // запрошенный кодеру битрейт, бит/с; результат передать через applied
uint32_t minimum_bps, maximum_bps; // действующий диапазон регулятора, бит/с
int have_peer, error; // возможности пира получены / настройка завершилась ошибкой
int can_encode; // Новые raw входы допустимы; готовый TX всё равно сохраняется и повторяется.
int tx_idle; // Mailbox свободен; новый формат не обгонит кадры прежнего поколения в mailbox.
uint64_t decoded_generation;
int decoded_accepted;
uint64_t decoded_generation; // поколение, для которого удалённый decoder сообщил результат
int decoded_accepted; // 1 — удалённый decoder принял поколение, 0 — отказ
unsigned rx_frames; // Полные encoded кадры порта; pending decoder/GUI учитывает RX worker.
};
// ── Привязка к ядру ──
// Подключить единственное ядро к порту; 0/-1.
int call_video_port_attach(struct UTUN_INSTANCE *inst);
// Отменить ожидающий post и освободить очереди; выполнять до уничтожения ядра.
void call_video_port_detach(struct UTUN_INSTANCE *inst);
// ── Открытие и закрытие видеотрека ──
// Скопировать локальные capabilities и запланировать настройку видеотрека; 0/-1, не подтверждение готовности пира.
int call_video_port_open(uint64_t id, const struct call_video_caps *caps);
// Закрыть текущий id и очистить его очереди; чужой id игнорируется.
void call_video_port_close(uint64_t id);
// ── Состояние и уведомления ──
// Скопировать текущее состояние; 0 — снимок получен, -1 — нет активного id/ядра или out.
int call_video_port_status(uint64_t id, struct call_video_port_status *out);
/* Неблокирующий накопительный счётчик полных принятых RX кадров текущего звонка.
* Pull не уменьшает его. Для общего запаса вычитаются показанные/отклонённые кадры.
* 0 для закрытого/другого звонка; новый open после остановки readers начинает с нуля. */
uint64_t call_video_port_received(uint64_t id);
/* Уведомление RX без обращения к port API из callback; вызывается под mutex порта.
* 1 — установлен, 0 — звонок ещё не открыт/уже закрыт. NULL снимает callback до освобождения arg. */
int call_video_port_set_wake(uint64_t id, void (*wake)(void *), void *arg);
// ── Настройка кодера и исходящие кадры ──
// Передать фактический формат и битрейт открытого encoder; формат копируется. 0 — команда принята, -1 — отказ.
int call_video_port_format(uint64_t id, const struct call_video_format *format, uint32_t bps);
// Подтвердить команду status.rate_generation/rate_request: bps — применённый битрейт, 0 — отказ encoder. 0/-1.
int call_video_port_applied(uint64_t id, uint64_t generation, uint64_t request, uint32_t bps);
#define CALL_VIDEO_PORT_TX_FRAMES 8u
#define CALL_VIDEO_PORT_TX_BYTES (CALL_VIDEO_PORT_TX_FRAMES * CALL_VIDEO_MAX_FRAME)
/* Атомарно копирует готовую пачку одного звонка. 1 — принята целиком, 0 — mailbox занят,
* -1 — ошибка; на 0/-1 ни один кадр не принят. count: 1..CALL_VIDEO_PORT_TX_FRAMES. */
* -1 — ошибка; на 0/-1 ни один кадр не принят. count: 1..CALL_VIDEO_PORT_TX_FRAMES.
* Входные кадры всегда остаются у вызывающего. При 0 повторить ту же пачку после освобождения порта. */
int call_video_port_send(const struct call_video_frame *frames, unsigned count);
// ── Входящие кадры ──
// Забрать старейший полный RX-кадр: 1 — получен, out->data освободить через u_free; 0 — кадра нет, out не меняется.
int call_video_port_pull(uint64_t id, struct call_video_frame *out);
// ── Управление качеством и восстановлением ──
// Запланировать запрос нового KEY удалённому encoder после потери опорных кадров.
void call_video_port_request_key(uint64_t id);
// Задать диапазон битрейта в бит/с: 300000..4000000, кратно 1000; 0 — команда принята, -1 — отказ.
int call_video_port_range(uint64_t id, uint32_t minimum_bps, uint32_t maximum_bps);
// Сообщить результат запуска decoder поколения generation: accepted=0/1; 0 — команда принята, -1 — отказ.
int call_video_port_decoded(uint64_t id, uint64_t generation, int accepted);
#ifdef __cplusplus
}
#endif
+14 -1
View File
@@ -262,7 +262,9 @@ static void flush(struct call_video_transport *v) {
f->data = head->data + f->offset;
size_t length;
if (call_video_fragment_encode(f, wire, sizeof(wire), &length)
|| etcp_router_conn_send(conn, wire, length, 0)) { discard_tx(v, "router_send"); return; }
|| etcp_router_conn_send(conn, wire, length, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)) {
discard_tx(v, "router_send"); return;
}
f->offset += f->length;
if (f->offset == f->size) {
if (f->pts_us % 1000000 < 34000)
@@ -483,6 +485,13 @@ static void receive(struct ETCP_CONN *conn, struct ll_entry *entry) {
}
const uint8_t *data = entry->dgram + ROUTER_SVC_PAYLOAD_OFF;
size_t length = entry->len - ROUTER_SVC_PAYLOAD_OFF;
if (!length) goto done; // Локальное уведомление роутера о закрытии, не медиапакет.
if ((entry->dgram[ROUTER_SVC_FLAGS_OFF] & (ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED))
!= (ROUTER_FLAG_ENCRYPTED | ROUTER_FLAG_SIGNED)) {
DEBUG_WARN(DEBUG_CATEGORY_CALL, "unprotected call video packet svc=%u flags=%02x bytes=%zu",
entry->dgram[0], entry->dgram[ROUTER_SVC_FLAGS_OFF], length);
goto done;
}
uint64_t group, peer, id = 0;
memcpy(&group, entry->dgram + ROUTER_SVC_GROUP_OFF, 8); memcpy(&peer, entry->dgram + ROUTER_SVC_SRC_OFF, 8);
int control = entry->dgram[0] == ETCP_RT_ID_CALL_VIDEO_CONTROL;
@@ -558,6 +567,10 @@ int call_video_transport_bind(struct UTUN_INSTANCE *inst) {
if (etcp_router_bind(inst, ETCP_RT_ID_CALL_VIDEO, receive)) {
etcp_router_unbind(inst, ETCP_RT_ID_CALL_VIDEO_CONTROL); return -1;
}
if (etcp_router_require_crypto(inst, ETCP_RT_ID_CALL_VIDEO_CONTROL, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)
|| etcp_router_require_crypto(inst, ETCP_RT_ID_CALL_VIDEO, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)) {
etcp_router_unbind(inst, ETCP_RT_ID_CALL_VIDEO); etcp_router_unbind(inst, ETCP_RT_ID_CALL_VIDEO_CONTROL); return -1;
}
etcp_router_set_receive_ready(inst, ETCP_RT_ID_CALL_VIDEO, ready); return 0;
}
void call_video_transport_unbind(struct UTUN_INSTANCE *inst) {
+66 -18
View File
@@ -23,11 +23,13 @@ enum { AK_REQUEST = 1, AK_REQUEST_ACK, AK_GRANT, AK_DENY, AK_RESULT_ACK };
struct ak_file {
struct UTUN_INSTANCE* inst;
char channel[32], source[1024], cipher[1024], plain[1024], temporary[1040], type[32];
char channel[32], source[1024], cipher[1024], plain[1024], type[32];
struct file_io_temp output; // Единственный собственный temp; стабильный адрес до completion.
uint8_t id[16], key[32], message[32], hash[32], reply[32];
struct attachment_info info;
uint64_t author, request;
int encrypt, error;
int plain_prepared, published; // Только реально подготовленный snapshot и выполненный rename дают право очистки.
};
/* Все ошибки SQLite содержат конкретную операцию. */
@@ -94,10 +96,8 @@ invalid:
/* Разделённые деревья исключают раздачу plaintext и удаление его обходом orphan media. */
static int ak_paths(struct ak_file* job) {
const char* slash = strrchr(CC(job->inst)->db_path, '/');
char base[512], hex[33]; ak_hex(job->id, hex);
if (slash) snprintf(base, sizeof(base), "%.*s", (int)(slash - CC(job->inst)->db_path), CC(job->inst)->db_path);
else snprintf(base, sizeof(base), ".");
if (media_index_get_base(CC(job->inst)->db, base)) return -1;
int a = snprintf(job->cipher, sizeof(job->cipher), "%s/media/%s/%s.uenc", base, job->channel, hex);
int b = snprintf(job->plain, sizeof(job->plain), "%s/decrypted/%s/%s.file", base, job->channel, hex);
if (a < 0 || b < 0 || (size_t)a >= sizeof(job->cipher) || (size_t)b >= sizeof(job->plain)) {
@@ -121,12 +121,18 @@ static int ak_parent(const char* path) {
static void ak_file_work(void* arg) {
struct ak_file* job = arg; job->error = -1;
if (ak_parent(job->plain) || ak_parent(job->cipher)) return;
snprintf(job->temporary, sizeof(job->temporary), "%s.part", job->encrypt ? job->cipher : job->plain);
if (job->encrypt) {
if (ma_copy_file(job->source, job->plain) || encrypted_file_sync(job->plain) || encrypted_file_sync_directory(job->plain) ||
encrypted_file_write(job->plain, job->temporary, job->key, job->author, job->id, &job->info, job->type)) return;
} else if (encrypted_file_read(job->cipher, job->temporary, job->key, job->author, job->id, job->hash, &job->info, job->type)) return;
if (encrypted_file_publish(job->temporary, job->encrypt ? job->cipher : job->plain)) return;
if (ma_copy_file(job->source, job->plain)) return;
job->plain_prepared = 1;
if (file_io_sync_file(job->plain) || file_io_sync_directory(job->plain) ||
encrypted_file_write(job->plain, job->cipher, &job->output, job->key, job->author, job->id, &job->info, job->type)) return;
} else if (encrypted_file_read(job->cipher, job->plain, &job->output, job->key, job->author, job->id,
job->hash, &job->info, job->type)) return;
int error = file_io_temp_publish(&job->output, job->encrypt ? job->plain : job->cipher, job->encrypt ? job->cipher : job->plain);
job->published = !job->output.temporary[0]; // Rename уже выполнен и при последующем отказе directory sync.
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "attachment keys: output publication encrypt=%d installed=%d error=%d",
job->encrypt, job->published, error);
if (error) return;
job->error = 0;
}
@@ -174,11 +180,12 @@ static void ak_file_done(void* arg, int error) {
if (job->encrypt) chat_event_submitted(inst, job->request, 0);
}
if (job->encrypt && remove(job->source) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: prepared source cleanup failed");
if ((!success || !valid) && job->temporary[0] && remove(job->temporary) && errno != ENOENT)
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary cleanup failed");
file_io_temp_discard(&job->output);
if (!valid) {
if (remove(job->plain) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel plaintext cleanup failed");
if (job->encrypt && remove(job->cipher) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel ciphertext cleanup failed");
if (((job->encrypt && job->plain_prepared) || (!job->encrypt && job->published)) && remove(job->plain) && errno != ENOENT)
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel plaintext cleanup failed");
if (job->encrypt && job->published && remove(job->cipher) && errno != ENOENT)
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: deleted channel ciphertext cleanup failed");
}
if (valid) {
if (success && !job->encrypt) chat_media_cache_enforce(inst);
@@ -191,7 +198,7 @@ static void ak_file_done(void* arg, int error) {
int chat_attachment_send(struct UTUN_INSTANCE* inst, const char* channel, const char* source,
const struct attachment_info* info, const char* type, const uint8_t reply[32], uint64_t request) {
if (!inst || !CC(inst) || CC(inst)->attachment_closing || !inst->media_async || !ak_channel(channel) ||
!source || strlen(source) >= 1024 || !type || strlen(type) >= 32 || attachment_validate(info)) {
!source || strlen(source) >= 1024 || encrypted_file_validate_type(type) || attachment_validate(info)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: invalid send arguments/state"); return -1;
}
struct ak_file* job = u_calloc(1, sizeof(*job));
@@ -544,9 +551,52 @@ static void ak_tick(void* arg) {
if (!CC(inst)->attachment_timer) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: timer allocation failed");
}
struct ak_temp_cleanup { const char* root; const char* extension; unsigned removed; };
/* Только <channel>/<UUID>.<uenc|file>.tmp.<UUID>; чужие имена и активные owners сохраняются. */
static void ak_remove_temp(void* arg, const char* path, int64_t size) {
(void)size;
struct ak_temp_cleanup* cleanup = arg;
size_t prefix = strlen(cleanup->root);
if (strncmp(path, cleanup->root, prefix) || path[prefix] != '/') return;
const char* relative = path + prefix + 1;
const char* slash = strchr(relative, '/');
if (!slash || slash - relative >= 20) return;
char channel[20]; memcpy(channel, relative, (size_t)(slash - relative)); channel[slash - relative] = 0;
if (!ak_channel(channel)) return;
const char* name = slash + 1;
const char* hex = "0123456789abcdef";
if (strlen(name) != 74 || strspn(name, hex) != 32 || memcmp(name + 32, cleanup->extension, 5) ||
memcmp(name + 37, ".tmp.", 5) || strspn(name + 42, hex) != 32) return;
int removed = file_io_remove_orphan(path);
if (removed > 0) cleanup->removed++;
else if (removed < 0) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery failed path=%s", path);
}
/* И при crash до публикации, и при прерванной расшифровке temp не является готовым вложением. */
static void ak_recover_temps(struct UTUN_INSTANCE* inst) {
char base[MEDIA_BASE_MAX];
if (media_index_get_base(CC(inst)->db, base)) return;
const char* trees[] = {"media", "decrypted"};
for (unsigned i = 0; i < 2; i++) {
char root[MEDIA_BASE_MAX + 16]; snprintf(root, sizeof(root), "%s/%s", base, trees[i]);
struct file_io_info info;
int status = file_io_inspect(root, 0, &info);
if (status == 1) continue;
if (status < 0) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: cannot inspect temporary tree path=%s", root); continue; }
struct ak_temp_cleanup cleanup = {.root = root, .extension = i ? ".file" : ".uenc"};
if (ma_dir_walk(root, ak_remove_temp, &cleanup))
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery incomplete path=%s removed=%u", root, cleanup.removed);
else DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "attachment keys: temporary recovery path=%s removed=%u", root, cleanup.removed);
}
}
/* После crash/отмены убрать только собственные подготовки, не имеющие сообщения.
* Вызывается до запуска workers либо после их полного завершения. */
static void ak_recover_preparations(struct UTUN_INSTANCE* inst) {
ak_recover_temps(inst);
char base[MEDIA_BASE_MAX];
if (media_index_get_base(CC(inst)->db, base)) return;
char** channels = NULL; int count = 0, columns = 0;
if (sqlite3_get_table(CC(inst)->db, "SELECT channel_id FROM channels", &channels, &count, &columns, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: recovery channel scan failed: %s", sqlite3_errmsg(CC(inst)->db)); return;
@@ -566,16 +616,14 @@ static void ak_recover_preparations(struct UTUN_INSTANCE* inst) {
for (int row = 0; row < rows; row++) {
for (int field = 0; field < fields; field++) {
const char* path = paths[(row + 1) * fields + field];
char temporary[1040]; snprintf(temporary, sizeof(temporary), "%s.part", path);
if ((remove(path) && errno != ENOENT) || (remove(temporary) && errno != ENOENT)) {
if (remove(path) && errno != ENOENT) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: unfinished file removal failed path=%s: %s", path, strerror(errno)); failed = 1;
}
}
}
sqlite3_free_table(paths);
if (rows && !failed) {
const char* slash = strrchr(CC(inst)->db_path, '/');
unsigned prefix = slash ? (unsigned)(slash - CC(inst)->db_path) : 1;
unsigned prefix = (unsigned)strlen(base);
char cleanup[768];
snprintf(cleanup, sizeof(cleanup), "DELETE FROM media_files WHERE chat_id='%s' AND location IN"
"(SELECT substr(a.cipher_path,%u) FROM attachment_keys a WHERE %s)", channel, prefix + 2, condition);
+44 -12
View File
@@ -1,8 +1,8 @@
/* Зашифрованные групповые вложения и ручная выдача ключей.
* Все API кроме identity — uasync-поток. Ключи, запросы и outbox локальны.
* init после media_async; quiesce до его destroy, destroy после workers.
* Отдельный router-сервис требует E2E-шифрование и подпись каждого пакета.
* Очереди переживают restart; для доставки нужен живой маршрут обеих сторон. */
/* chat_attachment — вложения канала с шифрованием и ручной выдачей ключа автором.
* send готовит контейнер и публикует сообщение; downloaded/want запускают получение ключа и расшифровку.
* Автор отвечает на запрос через decide. Ключи и очередь доставки сохраняются в локальной БД.
* Все вызовы, кроме identity, — uasync. init после media_async; quiesce до остановки workers, destroy после них.
* Обмен ключами подписан и зашифрован; для доставки нужен живой маршрут обеих сторон. */
#ifndef CHAT_ATTACHMENT_H
#define CHAT_ATTACHMENT_H
#include "../media_async/attachment.h"
@@ -11,25 +11,52 @@
extern "C" {
#endif
struct UTUN_INSTANCE;
// ── Жизненный цикл модуля ──
// Подключить таблицы, router-сервис и обработку сохранённых задач; 0/-1.
int chat_attachment_init(struct UTUN_INSTANCE* inst);
// Запретить новые подготовки и остановить таймер; запущенные workers завершаются отдельно.
void chat_attachment_quiesce(struct UTUN_INSTANCE* inst);
// После завершения workers убрать незавершённые подготовки и отключить router-сервис.
void chat_attachment_destroy(struct UTUN_INSTANCE* inst);
/* source — собственный подготовленный файл приложения, передаётся во владение. */
// ── Отправка и учёт вложений ──
/* Асинхронно отправить подготовленный файл; channel — десятичный ID, reply — 32 байта ID ответа либо нули.
* Аргументы копируются. 0 — задача принята, source передан задаче и будет удалён даже при ошибке подготовки.
* -1 — отказ запуска, source остаётся у вызывающего. Ненулевой request возвращается в CHAT_EVT_MSG_SUBMITTED. */
/* type проверяется encrypted_file_validate_type до выделения задачи, генерации UUID/ключа и записи в БД. */
int chat_attachment_send(struct UTUN_INSTANCE* inst, const char* channel, const char* source,
const struct attachment_info* info, const char* type, const uint8_t reply[32], uint64_t request);
/* UUID из подписанного имени контейнера; принимает канонический base64-prefix тела медиа. */
// Прочитать UUID из канонического base64-префикса тела медиа; 0/-1. Подпись проверяет вызывающий.
int chat_attachment_identity(const char* body, size_t length, uint8_t id[16]);
/* Связать заранее сохранённый авторский ключ с подписанным сообщением до публикации. */
// Связать авторский ключ с ID сообщения и SHA256 контейнера до публикации; 0/-1. Массивы копируются в БД.
int chat_attachment_bind(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t id[16],
const uint8_t message[32], const uint8_t hash[32]);
/* Учесть проверенное db_sync сообщение; обычные сообщения игнорируются. */
// Учесть проверенное db_sync сообщение и сохранить метаданные контейнера; 0/-1. Обычные сообщения игнорируются.
int chat_attachment_observe(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32],
uint64_t author, const char* json, size_t length);
/* После проверенной загрузки: сохранить состояние, запросить ключ либо расшифровать. */
// ── Получение файла и выдача доступа ──
// После проверенной загрузки: сохранить состояние, запросить ключ либо расшифровать.
void chat_attachment_downloaded(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]);
/* Ручная загрузка на storage-узле также означает запрос пользовательского доступа. */
// Ручная загрузка на storage-узле также означает запрос пользовательского доступа.
void chat_attachment_want(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32]);
/* Сохранить решение автора по ожидающему запросу peer: approve!=0 — выдать ключ, 0 — отказать.
* 0 — решение и исходящий ответ записаны в БД, -1 — ошибка/нет прав или ожидающего запроса; доставка идёт позже. */
int chat_attachment_decide(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t message[32], uint64_t peer, int approve);
// ── Команда интерфейса ──
struct chat_attachment_decision {
struct UTUN_INSTANCE* inst;
char channel[32];
@@ -37,9 +64,14 @@ struct chat_attachment_decision {
uint64_t peer;
int approve;
};
// arg — u_malloc/u_calloc chat_attachment_decision, освобождается здесь; результат — CHAT_EVT_ATTACHMENT_KEYS_CHANGED.
void chat_attachment_decision_trampoline(void* arg);
/* Канал удаляется локально: отменить очереди, стереть ключи и частные копии. */
// ── Локальное удаление канала ──
// Канал удаляется локально: отменить очереди, стереть ключи и частные копии.
void chat_attachment_delete_channel(struct UTUN_INSTANCE* inst, const char* channel);
#ifdef __cplusplus
}
#endif
+33 -38
View File
@@ -60,30 +60,33 @@ static int channel_sign_join(const uint8_t* ch_x25519, const uint8_t* ch_ed25519
/* ─── подготовка инфраструктуры канала (db_sync instance) ─── */
void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !ch_id || !ch_id[0]) return;
if (chat_feedback_ensure_channel(inst, ch_id) < 0) return;
if (si_find(inst, ch_id)) return;
if (topo_node_sqlite_member_local_init(cc->db, ch_id) < 0) return;
char tbl_msg[80]; msg_table_name(ch_id, tbl_msg, sizeof(tbl_msg));
int chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (!cc || !cc->initialized || !ch_id || !ch_id[0] || !inst->topo_groups) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel preparation without active chat/topology"); return -1;
}
if (chat_feedback_ensure_channel(inst, ch_id) < 0 || topo_node_sqlite_member_local_init(cc->db, ch_id) < 0) return -1;
uint64_t gid = strtoull(ch_id, NULL, 10);
if (gid != 0 && inst->topo_groups && !topo_groups_find(inst->topo_groups, gid)) {
struct TOPO_GROUP* g = topo_groups_create_group(inst->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, ch_id);
if (g) member_sync_subscribe_group(inst, g);
struct TOPO_GROUP* group = topo_groups_find(inst->topo_groups, gid);
int created_group = !group;
if (!group) group = topo_groups_create_group(inst->topo_groups, gid, TOPO_GROUP_TYPE_CHAT, ch_id);
if (!group || member_sync_subscribe_group(inst, group) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel group preparation failed ch=%s", ch_id);
if (group && created_group) topo_groups_remove_group(inst->topo_groups, gid);
return -1;
}
struct DB_SYNC_INSTANCE* si = db_sync_instance_add(inst, tbl_msg, gid);
if (si) {
si_register(inst, si, ch_id);
db_sync_set_insert_cb(si, on_msg_inserted, inst);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: channel ready ch=%s tbl=%s group=0x%016llx",
CC_ID, ch_id, tbl_msg, (unsigned long long)gid);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: db_sync_instance_add failed for ch=%s",
CC_ID, ch_id);
struct DB_SYNC_INSTANCE* si = si_find(inst, ch_id);
int created_sync = !si;
char table[80]; msg_table_name(ch_id, table, sizeof(table));
if (!si) si = db_sync_instance_add(inst, table, gid);
if (!si) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel sync allocation failed ch=%s", ch_id);
if (created_group) topo_groups_remove_group(inst->topo_groups, gid);
return -1;
}
if (created_sync) { si_register(inst, si, ch_id); db_sync_set_insert_cb(si, on_msg_inserted, inst); }
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "channel prepared ch=%s new_group=%d new_sync=%d", ch_id, created_group, created_sync);
return 0;
}
/* ─── создание канала ─── */
@@ -319,20 +322,6 @@ void chat_core_apply_group_autoconnect(struct UTUN_INSTANCE* inst) {
/* ─── локальное удаление канала ─── */
static const char* cc_ui_state_get(struct chat_core_ctx* cc, const char* key) {
static char val[512];
val[0] = '\0';
sqlite3_stmt* st = NULL;
if (sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* v = (const char*)sqlite3_column_text(st, 0);
if (v) snprintf(val, sizeof(val), "%s", v);
}
sqlite3_finalize(st);
}
return val;
}
void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
struct chat_core_ctx* cc = CC(inst);
@@ -341,6 +330,7 @@ void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
return;
}
if (!ch_id || !ch_id[0]) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel — empty ch_id", CC_ID); return; }
if (chat_media_defer_channel_delete(inst, ch_id)) return;
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel BEGIN ch=%s", CC_ID, ch_id);
uint64_t gid = strtoull(ch_id, NULL, 10);
@@ -368,10 +358,14 @@ void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
chat_sync_on_channel_deleted(inst, ch_id);
/* 6. удалить медиафайлы канала на диске + строки media_files */
media_index_delete_channel(cc->db, ch_id, cc_ui_state_get(cc, "media_base"));
char media_base[MEDIA_BASE_MAX];
int media_error = media_index_get_base(cc->db, media_base) || media_index_delete_channel(cc->db, ch_id, media_base);
if (media_error) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "channel=%s removed with incomplete media cleanup; retry required", ch_id);
/* 7. удалить таблицы и метаданные канала */
topo_node_sqlite_channel_delete(cc->db, ch_id);
if (topo_node_sqlite_channel_delete(cc->db, ch_id)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "channel metadata deletion failed ch=%s", ch_id); return;
}
/* 8. уведомить GUI */
{
@@ -380,7 +374,8 @@ void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id) {
chat_event_post(inst, CHAT_EVT_CHANNEL_DELETED, evt, 1 + cl);
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel DONE ch=%s gid=0x%016llx", CC_ID, ch_id, (unsigned long long)gid);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: delete_channel DONE ch=%s gid=0x%016llx media_cleanup=%s",
CC_ID, ch_id, (unsigned long long)gid, media_error ? "incomplete" : "complete");
}
void chat_core_delete_channel_trampoline(void* arg) {
+19 -9
View File
@@ -107,6 +107,11 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
if (!cc->shared_db) sqlite3_close(cc->db);
inst->chat_core = NULL; u_free(cc); return -1;
}
if (strlen(db_path) >= sizeof(cc->db_path) || media_index_init(cc->db)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "chat media schema/path initialization failed");
if (!cc->shared_db) sqlite3_close(cc->db);
inst->chat_core = NULL; u_free(cc); return -1;
}
cc->inst = inst;
cc->my_node_id = inst->node_id;
@@ -141,9 +146,6 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
/* register adm_tags change callback → CHAT_EVT_MEMBER_UPDATED */
chat_member_init(inst);
/* create media_files table */
media_index_init(cc->db);
db_exec(cc,
"CREATE TABLE IF NOT EXISTS local_identity ("
" id INTEGER PRIMARY KEY CHECK (id = 1),"
@@ -172,13 +174,20 @@ int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) {
);
/* store media_base for block_req handler */
{
char mbase[512];
char mbase[MEDIA_BASE_MAX];
const char* ls = strrchr(db_path, '/');
if (ls) snprintf(mbase, sizeof(mbase), "%.*s", (int)(ls - db_path), db_path);
else snprintf(mbase, sizeof(mbase), "%s", db_path);
char sql[1024]; snprintf(sql, sizeof(sql),
"INSERT OR REPLACE INTO ui_state(key,value) VALUES('media_base','%s')", mbase);
sqlite3_exec(cc->db, sql, NULL, NULL, NULL);
if (ls) snprintf(mbase, sizeof(mbase), "%.*s", ls == db_path ? 1 : (int)(ls - db_path), db_path);
else snprintf(mbase, sizeof(mbase), ".");
sqlite3_stmt* stored = NULL;
int rc = sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key='media_base'", -1, &stored, NULL);
if (rc == SQLITE_OK) rc = sqlite3_step(stored);
int base_error = rc == SQLITE_ROW ? media_index_get_base(cc->db, mbase)
: rc == SQLITE_DONE ? media_index_set_base(cc->db, mbase) : -1;
int finish = stored ? sqlite3_finalize(stored) : SQLITE_OK;
if (base_error || finish != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "chat media_base initialization failed rc=%d: %s", rc, sqlite3_errmsg(cc->db));
chat_core_destroy(inst); return -1;
}
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "%s: media_base=%s", CC_ID, mbase);
}
@@ -245,6 +254,7 @@ void chat_core_destroy(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized) return;
cc->initialized = 0;
chat_member_destroy(inst);
chat_attachment_destroy(inst);
member_sync_destroy(inst); /* Also releases callback lists after partial startup. */
+85 -50
View File
@@ -2,6 +2,8 @@
* chat_core — API каналов, сообщений, профиля и настроек одного узла.
* chat_service_start/stop управляют всем чат-сервисом поверх работающего ядра.
* Данные хранятся в общей SQLite; события для интерфейса доставляет chat_event.
* Порядок: utun_core_start → chat_service_start → команды чата → chat_service_stop.
* Сообщения отправляются через submit_message или submit_media_message; результат GUI — CHAT_EVT_MSG_SUBMITTED.
*
* Все DB-операции и сетевые функции выполняются в uasync-потоке.
* Все функции per-instance: первым аргументом struct UTUN_INSTANCE*.
@@ -19,10 +21,11 @@
struct sqlite3;
struct UTUN_INSTANCE;
/* ── Жизненный цикл ── */
// ── Жизненный цикл ──
/* Запустить чат поверх core_start; UTUN не требуется. Повтор допустим; 0/-1. */
// Запустить чат поверх core_start; UTUN не требуется. Повтор допустим; 0/-1.
int chat_service_start(struct UTUN_INSTANCE* inst);
/* Остановить чат, сохранив ядро, UTUN и БД. Повтор допустим; вызывать вне callbacks чата.
* Отменяет доставку и ждёт native media workers; длительная задача может задержать stop. */
void chat_service_stop(struct UTUN_INSTANCE* inst);
@@ -30,7 +33,8 @@ void chat_service_stop(struct UTUN_INSTANCE* inst);
/* Внутренняя часть запуска: открыть/подключить БД и загрузить каналы. 0/-1.
* Для запуска всего чат-сервиса использовать chat_service_start(). */
int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path);
/* Освободить chat_core после остановки остальных частей чата; общая БД остаётся у ядра. */
// Освободить chat_core после остановки остальных частей чата; общая БД остаётся у ядра.
void chat_core_destroy(struct UTUN_INSTANCE* inst);
/* Backfill: регистрирует уже скачанные медиафайлы (лежат на диске, но отсутствуют
@@ -44,14 +48,16 @@ void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst);
/* Регистрирует триггер: после первого SYNC_DONE (+ дебаунс 2с) анонсирует локальные
* блоки суперузлам и запускает chat_media_autodownload_backfill. Один раз за запуск чата. */
void chat_media_startup_backfill_init(struct UTUN_INSTANCE* inst);
void chat_media_startup_backfill_destroy(struct UTUN_INSTANCE* inst);
/* Заимствованная БД текущего chat_core; NULL до init/после destroy. */
// Заимствованная БД текущего chat_core; NULL до init/после destroy.
struct sqlite3* chat_core_get_db(struct UTUN_INSTANCE* inst);
/* 1 — chat_core инициализирован, 0 — нет; не проверяет доступность сети. */
// 1 — chat_core инициализирован, 0 — нет; не проверяет доступность сети.
int chat_core_is_initialized(struct UTUN_INSTANCE* inst);
/* ── Отправка сообщения (GUI → uasync) ── */
// ── Отправка сообщения (GUI → uasync) ──
struct chat_msg_submit {
struct UTUN_INSTANCE* inst;
@@ -72,31 +78,38 @@ struct chat_msg_submit {
uint64_t request_id; /* ненулевой ID: trampoline/media отправляет CHAT_EVT_MSG_SUBMITTED */
};
/* Подписать, сохранить и разослать сообщение. req/data остаются у вызывающего. 0/-1. */
// Подписать, сохранить и разослать сообщение. req/data остаются у вызывающего. 0/-1.
int chat_core_submit_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
/* Отправка медиа-сообщения (вложение): src->dest (copy) или видео (transcode). */
/* Подготовить файл в worker, затем сохранить и разослать сообщение; результат — CHAT_EVT_MSG_SUBMITTED при request_id!=0.
* req и data копируются до возврата и остаются у вызывающего; файлы должны жить до завершения подготовки.
* media_copy копирует src в dest, media_video запускает транскодирование. Ошибка также приходит событием. */
void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req);
/* Ручная загрузка вложения; для зашифрованного файла также запрашивает ключ автора. */
// Ручная загрузка вложения; для зашифрованного файла также запрашивает ключ автора.
void chat_core_attachment_download(struct UTUN_INSTANCE* inst, const char* channel_id, int64_t msg_id);
/* ── DB-операции (оставлены для интроспекции) ── */
// ── DB-операции (оставлены для интроспекции) ──
/* Число локальных сообщений; 0 также при отсутствии БД или ошибке запроса. */
// Число локальных сообщений; 0 также при отсутствии БД или ошибке запроса.
uint32_t chat_core_count(struct UTUN_INSTANCE* inst, const char* ch_id);
/* Хеш записи по позиции с нуля в порядке (timestamp, node_id); hash_out — 32 байта.
* 0 = запрос выполнен (нет записи — нулевой хеш), -1 = ошибка. */
int chat_core_chain_hash_at(struct UTUN_INSTANCE* inst, const char* ch_id, uint32_t pos, uint8_t* hash_out);
/* Список ID: count:u16, затем [длина:u8, строка]; buf_size >= 2. 0/-1.
* Списки ограничены буфером; out_len — записанные байты, числа в порядке байтов хоста. */
int chat_core_list_channels(struct UTUN_INSTANCE* inst, uint8_t* buf, size_t buf_size, size_t* out_len);
/* Список участников: count:u16, затем node_id:u64; buf_size >= 2. 0/-1. */
// Список участников: count:u16, затем node_id:u64; buf_size >= 2. 0/-1.
int chat_core_list_peers(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t* buf, size_t buf_size, size_t* out_len);
/* Ключи X25519/Ed25519 (по 32 байта) и список адресов из БД; buf_size >= 65.
* out_len — размер результата; 0 = прочитано, -1 = нет узла или ошибка. */
int chat_core_load_nodeinfo(struct UTUN_INSTANCE* inst, uint64_t node_id, uint8_t* buf, size_t buf_size, size_t* out_len);
/* ── Создание канала (GUI → uasync) ── */
// ── Создание канала (GUI → uasync) ──
struct chat_channel_create {
struct UTUN_INSTANCE* inst;
@@ -112,16 +125,21 @@ struct chat_channel_create {
struct chat_create_auto_arg { struct UTUN_INSTANCE* inst; char name[128]; };
/* Подготовить локальные таблицы, группу и синхронизацию канала. Членства на других узлах не даёт. */
void chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id);
/* Создать канал по готовым ключам и метаданным req; req остаётся у вызывающего. */
/* Подготовить таблицы, группу и синхронизацию; 0/-1. Ошибка удаляет новые runtime-ресурсы,
* проверенные данные БД сохраняются. Членства на других узлах не даёт. */
int chat_core_ensure_channel_ready(struct UTUN_INSTANCE* inst, const char* ch_id);
// Создать канал по готовым ключам и метаданным req; req остаётся у вызывающего.
void chat_core_create_channel(struct UTUN_INSTANCE* inst, struct chat_channel_create* req);
void chat_core_create_channel_trampoline(void* arg);
/* Сгенерировать ключи и создать канал, добавив себя владельцем. */
// Сгенерировать ключи и создать канал, добавив себя владельцем.
void chat_core_create_channel_auto(struct UTUN_INSTANCE* inst, const char* name);
void chat_core_create_channel_auto_trampoline(void* arg);
/* ── Локальное удаление канала (GUI → uasync) ── */
// ── Локальное удаление канала (GUI → uasync) ──
struct chat_delete_channel_req {
struct UTUN_INSTANCE* inst;
@@ -130,31 +148,39 @@ struct chat_delete_channel_req {
/* Удалить канал с этого устройства: отмена медиа-загрузок, удаление группы
* (соединения/синк), db_sync инстанса, таблиц msg_ и peers_, медиафайлов и
* merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют. */
* merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют.
* Исходящие video/register задачи сначала завершаются; удаление продолжит последний completion.
* Новые исходящие медиа этого канала в это время отклоняются. Событие CHANNEL_DELETED приходит после удаления метаданных;
* ошибка файловой очистки логируется отдельно, записи неочищенных файлов сохраняются для повтора. */
void chat_core_delete_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_delete_channel_trampoline(void* arg);
/* ── Утилиты ── */
// ── Утилиты ──
/* Задать локальный ID в контексте чата; ключи и идентичность ядра не меняет. */
// Задать локальный ID в контексте чата; ключи и идентичность ядра не меняет.
void chat_core_set_my_node_id(struct UTUN_INSTANCE* inst, uint64_t node_id);
/* Сохранить имя узла и обновить собственные member-записи каналов. */
// Сохранить имя узла и обновить собственные member-записи каналов.
void chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name);
struct update_my_name_arg { struct UTUN_INSTANCE* inst; char name[128]; };
void chat_core_update_my_name_trampoline(void* arg);
/* Обновить адреса topo_node и собственные member-записи каналов. */
// Обновить адреса topo_node и собственные member-записи каналов.
void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst);
/* При изменении своего userinfo в каналах увеличить update_ts, подписать и запустить sync.
* Адреса обновляются отдельно через topo_node; подпись member-блока их не содержит. */
void chat_core_update_my_member(struct UTUN_INSTANCE* inst);
/* Сохранение key-value в ui_state (вызывается из uasync-потока) */
// Сохранение key-value в ui_state (вызывается из uasync-потока)
void chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value);
/* Per-channel «play sound on new message» (ui_state: ch_<id>_sound_on_message, default 1=on).
* set — сохраняет и рассылает пирам через chat_sync (троттлинг db_sync на сервере). */
int chat_core_get_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_set_sound_on_message(struct UTUN_INSTANCE* inst, const char* ch_id, int on);
struct chat_sound_arg { struct UTUN_INSTANCE* inst; char ch_id[64]; int on; };
@@ -163,18 +189,20 @@ void chat_core_set_sound_on_message_trampoline(void* arg);
struct save_ui_state_arg { struct UTUN_INSTANCE* inst; char data[1024]; };
void chat_core_save_ui_state_trampoline(void* arg);
/* Установка chat-настройки (name=value) через chat_setting API (GUI → uasync) */
// Установка chat-настройки (name=value) через chat_setting API (GUI → uasync)
void chat_core_set_setting(struct UTUN_INSTANCE* inst, const char* name, const char* value);
struct chat_setting_arg { struct UTUN_INSTANCE* inst; char data[256]; };
void chat_core_set_setting_trampoline(void* arg);
/* Запуск подключения к узлам выбранного канала (GUI → uasync) */
// Запуск подключения к узлам выбранного канала (GUI → uasync)
void chat_core_connect_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
void chat_core_connect_channel_trampoline(void* arg);
/* Однократная попытка подключения к конкретному узлу канала (GUI → uasync) */
// Однократная попытка подключения к конкретному узлу канала (GUI → uasync)
void chat_core_connect_node(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
void chat_core_connect_node_trampoline(void* arg);
struct chat_connect_node_req {
@@ -183,10 +211,12 @@ struct chat_connect_node_req {
uint64_t node_id;
};
/* Трамплин для gui_bridge_post_uasync (GUI → uasync) */
/* Передать в uasync блок chat_msg_submit, выделенный u_calloc/u_malloc; trampoline освобождает его.
* data отдельно не освобождается: удобно разместить содержимое в хвосте того же блока.
* Непустой media_src выбирает подготовку вложения, иначе — обычную отправку. Результат — по request_id. */
void chat_core_submit_trampoline(void* arg);
/* Трамплин для ручного запуска загрузки медиа (JNI → uasync) */
// Трамплин для ручного запуска загрузки медиа (JNI → uasync)
struct attachment_dl_req {
struct UTUN_INSTANCE* inst;
char channel_id[64];
@@ -194,14 +224,15 @@ struct attachment_dl_req {
};
void chat_core_attachment_download_trampoline(void* arg);
/* Обновление local_attrs для записи (вызывается из uasync-потока) */
// Обновление local_attrs для записи (вызывается из uasync-потока)
int chat_core_update_local_attrs(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t ts,
const uint8_t* author_sig,
const char* local_attrs_json);
/* Пометить голосовое сообщение как проигранное (добавляет "pl":1 в local_attrs) */
// Пометить голосовое сообщение как проигранное (добавляет "pl":1 в local_attrs)
void chat_core_mark_voice_played(struct UTUN_INSTANCE* inst, const char* channel_id, uint64_t ts,
uint64_t author_node_id);
void chat_core_mark_voice_played_trampoline(void* arg);
struct voice_played_req {
@@ -211,26 +242,25 @@ struct voice_played_req {
uint64_t author_node_id;
};
/* Сбор статуса (NTP + connections) и отправка в GUI. arg = struct UTUN_INSTANCE* (inst) */
// Сбор статуса (NTP + connections) и отправка в GUI. arg = struct UTUN_INSTANCE* (inst)
void chat_core_collect_status_trampoline(void* arg);
/* Лёгкий запрос нагрузки uasync для GUI. arg = inst, ответ CHAT_EVT_CPU_LOAD. */
// Лёгкий запрос нагрузки uasync для GUI. arg = inst, ответ CHAT_EVT_CPU_LOAD.
void chat_core_collect_cpu_load_trampoline(void* arg);
/* Сбор списка ETCP-соединений и отправка в GUI (binary packet). arg = inst */
// Сбор списка ETCP-соединений и отправка в GUI (binary packet). arg = inst
void chat_core_collect_conn_list_trampoline(void* arg);
struct chat_node_arg { struct UTUN_INSTANCE* inst; uint64_t node_id; };
/* Сбор полных метрик одного соединения (arg = u_malloc struct chat_node_arg) */
// Сбор полных метрик одного соединения (arg = u_malloc struct chat_node_arg)
void chat_core_collect_conn_metrics_trampoline(void* arg);
/* Сбор live-снапшота для панели деталей участника (arg = u_malloc struct chat_node_arg) */
void chat_core_collect_member_detail_trampoline(void* arg);
/* Диагностика карточки участника: запрос/codec находятся в chat_member.h. */
/* ── Headless CLI API ── */
// ── Headless CLI API ──
/* Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}] */
// Список каналов с метаданными в JSON: [{id,name,owner_id,owner_name,peers,msgs,online}]
int chat_core_get_channels_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len);
/* Сообщения канала в JSON: [{id,ts,author_id,author_name,content_type,data,local_attrs}].
@@ -242,20 +272,24 @@ int chat_core_get_messages_json(struct UTUN_INSTANCE* inst, const char* ch_id, i
* connected означает наличие транспорта в реестре, а не его UP или готовность группы. */
int chat_core_get_members_json(struct UTUN_INSTANCE* inst, const char* ch_id, char* buf, size_t buf_size, size_t* out_len);
/* Имя узла из таблицы nodes */
// Имя узла из таблицы nodes
int chat_core_get_node_name(struct UTUN_INSTANCE* inst, uint64_t node_id, char* out, size_t sz);
/* Ссылка на описание avatars/<sha256>.json внутри дерева своего UID/канала. server=0 удаляет ссылку. */
// Ссылка на описание avatars/<sha256>.json внутри дерева своего UID/канала. server=0 удаляет ссылку.
int chat_core_set_my_avatar(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t server, const char* file);
int chat_core_name_userinfo(const char* name, char* out, size_t capacity);
int chat_core_get_my_profile(struct UTUN_INSTANCE* inst, const char* channel, char* out, size_t capacity,
uint8_t signature[64], uint64_t* timestamp);
/* Одно подписанное обновление нескольких собственных строковых опций. NULL value удаляет ключ.
* Остальные поля сохраняются; имя/аватар меняются своими API. Ошибка не меняет БД/Merkle.
* Вызов только в uasync-потоке, вне внешней SQLite-транзакции. */
struct chat_profile_option { const char* key; const char* value; };
int chat_core_set_my_profile_options(struct UTUN_INSTANCE* inst, const char* channel,
const struct chat_profile_option* options, size_t count);
struct chat_avatar_req {
struct UTUN_INSTANCE* inst;
uint64_t id, group, server;
@@ -263,33 +297,33 @@ struct chat_avatar_req {
};
void chat_core_set_my_avatar_trampoline(void* arg);
/* ── Редактирование adm_tags (владелец канала) ── */
// ── Редактирование adm_tags (владелец канала) ──
#define CHAT_MEMBER_TAGS_MAX_KEYS 16
#define CHAT_MEMBER_TAGS_KEY_SZ 64
struct chat_member_tags;
/* Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет. */
// Загрузить текущие adm_tags мембера в память. Возвращает NULL если мембера нет.
struct chat_member_tags* chat_member_tags_load(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
/* Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit. */
// Установить ключ. val=NULL → удалить ключ. ver — особый, автоинкремент при commit.
int chat_member_tags_set(struct chat_member_tags* t, const char* key, const char* val);
/* Получить значение ключа. Возвращает NULL если ключ не найден. */
// Получить значение ключа. Возвращает NULL если ключ не найден.
const char* chat_member_tags_get(struct chat_member_tags* t, const char* key);
/* Заменить все теги данными из JSON-строки (плоский, строковые значения). */
// Заменить все теги данными из JSON-строки (плоский, строковые значения).
int chat_member_tags_replace(struct chat_member_tags* t, const char* json);
/* Сохранить: сериализовать в JSON, инкрементировать ver, подписать канальным Ed25519,
* записать через member_sync_put() → синхронизируется всем. */
int chat_member_tags_commit(struct UTUN_INSTANCE* inst, struct chat_member_tags* t);
/* Освободить без сохранения. */
// Освободить без сохранения.
void chat_member_tags_free(struct chat_member_tags* t);
/* Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память). */
// Трамплин для gui_bridge: arg = struct chat_member_tags_req* (доверенная память).
struct chat_member_tags_req {
struct UTUN_INSTANCE* inst;
char ch_id[64];
@@ -298,10 +332,11 @@ struct chat_member_tags_req {
};
void chat_member_tags_commit_trampoline(void* arg);
/* ── Передача прав админа (владелец → другому узлу) ── */
// ── Передача прав админа (владелец → другому узлу) ──
/* Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router). */
// Передать оба канальных privkey выбранному узлу (encrypted+signed через etcp_router).
void chat_core_transfer_admin(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
void chat_core_transfer_admin_trampoline(void* arg);
struct chat_transfer_admin_req {
+4
View File
@@ -79,6 +79,7 @@ struct ms_props_cbk; /* member_sync.c */
struct media_index_result;
struct ms_apply_cbk; /* member_sync.c */
struct group_avatar;
struct media_submit_ctx;
struct chat_core_ctx {
struct UTUN_INSTANCE* inst;
@@ -92,6 +93,7 @@ struct chat_core_ctx {
struct ms_apply_cbk* apply_cbks; /* was member_sync.c g_apply_cbks */
struct group_avatar* group_avatar;
uint64_t media_cache_bytes; /* was chat_msg.c g_media_cache_bytes */
struct media_submit_ctx* media_submissions; /* исходящие задачи до последнего callback; удаление канала ждёт их */
void* media_backfill_timer;/* was chat_msg.c g_media_startup_backfill_timer */
int media_backfill_ran; /* was chat_msg.c g_media_startup_backfill_ran */
struct UTUN_INSTANCE* media_backfill_inst; /* was chat_msg.c g_media_startup_backfill_inst */
@@ -125,6 +127,8 @@ void on_msg_inserted(struct DB_SYNC_INSTANCE* si, const uint8_t message_id[32],
int chat_msg_parse_media_body(const char* body, struct media_index_result* result);
/* Учесть также расшифрованные производные файлы и применить общую квоту кеша. */
void chat_media_cache_enforce(struct UTUN_INSTANCE* inst);
/* Пометить текущие исходящие задачи канала; 1 — удаление продолжит последний completion, 0 — задач нет. */
int chat_media_defer_channel_delete(struct UTUN_INSTANCE* inst, const char* channel);
/* ── применить настройку group_autoconnect ко всем CHAT-группам (chat_channel.c) ── */
+1 -1
View File
@@ -40,7 +40,7 @@ void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data,
[20]="CONN_LIST", [21]="CONN_METRICS",
[22]="MEMBER_DETAIL", [23]="MEMBER_LIST",
[24]="MEMBER_UPDATED", [25]="MEMBER_REMOVED",
[26]="BGP_INFO", [27]="CONNECTING_NODES",
[27]="CONNECTING_NODES",
[28]="ADMIN_KEY_RECEIVED", [29]="INVITE_LINK_READY",
[30]="INVITE_CANDIDATES", [31]="CHANNEL_DELETED",
[32]="DM_MSG_RECEIVED", [33]="DM_CONV_UPDATED",
+18 -7
View File
@@ -1,8 +1,10 @@
/*
* chat_event.h — система нотификаций о событиях чата
* chat_event — доставка событий ядра чата интерфейсу или headless-клиенту.
*
* Производители (chat_core, chat_sync, ...) вызывают chat_event_post().
* Потребители (chatgui, headless, будущий HTTP API) регистрируют один обработчик.
* На экземпляр регистрируется один обработчик через chat_event_set_handler().
* post вызывает его сразу, в том же uasync-потоке; очереди событий здесь нет.
* data заимствован только до возврата обработчика. Для передачи в другой поток нужна копия.
*
* Без обработчика — данные логируются через DEBUG_DEBUG + log_dump.
* Все вызовы — из uasync-потока.
@@ -20,9 +22,9 @@ struct UTUN_INSTANCE;
extern "C" {
#endif
/* Типы событий (совпадают с GUI_EVT_* в gui_bridge.h для прямой совместимости) */
// Типы событий (совпадают с GUI_EVT_* в gui_bridge.h для прямой совместимости)
#define CHAT_EVT_MSG_RECEIVED 1 /* [ch_id_len:1][ch_id:var][author_node_id:8][initial_sync:1] */
#define CHAT_EVT_CONNECT_RESULT 2 /* [node_id:8][result:4][channel_id:8] */
#define CHAT_EVT_CONNECT_RESULT 2 /* [node_id:8][result:4][channel_id:8][request_id:8], native */
#define CHAT_EVT_NEW_PEER 3 /* [node_id:8] */
#define CHAT_EVT_CHANNEL_UPDATED 4 /* [ch_id_len:1][ch_id:var] */
#define CHAT_EVT_MEMBERS_CHANGED 5 /* [ch_id_len:1][ch_id:var] */
@@ -42,11 +44,10 @@ extern "C" {
#define CHAT_EVT_LOCAL_SOCKETS 19 /* JSON: [{"si":id,"nm":"...","ad":"...","po":port,"pt":"TCP|UDP","st":0|1|2,"lk":n},...] */
#define CHAT_EVT_CONN_LIST 20 /* [count:2][entry:46B]* — список ETCP-соединений */
#define CHAT_EVT_CONN_METRICS 21 /* текст — полный дамп метрик выбранного соединения */
#define CHAT_EVT_MEMBER_DETAIL 22 /* [node_id:8][flags:1][own_tcp_active:2][links...][socks...] — live-снапшот для панели деталей участника */
#define CHAT_EVT_MEMBER_DETAIL 22 /* Адресованный ответ диагностики; wire описан в chat_member.h. */
#define CHAT_EVT_MEMBER_LIST 23 /* [ch_id_len:1][ch_id:var][count:2][member:CHAT_MEMBER_DISPLAY_SIZE]* */
#define CHAT_EVT_MEMBER_UPDATED 24 /* [ch_id_len:1][ch_id:var][count:1][member:CHAT_MEMBER_DISPLAY_SIZE] */
#define CHAT_EVT_MEMBER_REMOVED 25 /* [ch_id_len:1][ch_id:var][node_id:8] — мембер удалён из канала */
#define CHAT_EVT_BGP_INFO 26 /* текст: "rtt:XXms hop:X via bgp: [XXXX] name1 [XXXX] name2" */
#define CHAT_EVT_CONNECTING_NODES 27 /* [ch_id_len:1][ch_id:var][count:2][node_id:8]* */
#define CHAT_EVT_ADMIN_KEY_RECEIVED 28 /* [ch_id_len:1][ch_id:var][from_node_id:8] — получен канальный ключ (стал совладельцем) */
#define CHAT_EVT_INVITE_LINK_READY 29 /* [ch_id_len:1][ch_id:var][request_id:8][link_len:2][link:var] — пустой link = ошибка */
@@ -90,12 +91,22 @@ extern "C" {
#define CHAT_EVT_RADIO_CALL 65 /* [id:8LE][group:8LE][peer:8LE][state:1][reason:1][incoming:1], radio_call.h */
#define CHAT_EVT_CALL_PHONE_BUSY 66 /* [id:8LE][reserved:1]; вызывающий выбирает продолжить либо завершить */
// ── Подключение получателя событий ──
typedef void (*chat_event_handler_fn)(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
// Заменить обработчик; NULL отключает доставку. Предыдущий обработчик не вызывается.
void chat_event_set_handler(struct UTUN_INSTANCE* inst, chat_event_handler_fn handler);
// ── Отправка событий из ядра ──
// Синхронно передать type и len байт data; память остаётся у отправителя. Формат зависит от CHAT_EVT_*.
void chat_event_post(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len);
// Сообщить этап подготовки вложения: state=0 начало, 1 готово, 2 ошибка; target — ID канала или DM.
void chat_event_attachment(struct UTUN_INSTANCE* inst, int is_dm, const char* target, const uint8_t id[16], int state);
/* request_id=0 отключает подтверждение; вызывается один раз после сохранения либо ошибки подготовки. */
// request_id=0 отключает подтверждение; вызывается один раз после сохранения либо ошибки подготовки.
void chat_event_submitted(struct UTUN_INSTANCE* inst, uint64_t request_id, int success);
#ifdef __cplusplus
+14 -15
View File
@@ -509,7 +509,7 @@ static void hc_invite_ready(void* arg, int result, const char* link) {
}
DEBUG_INFO(DEBUG_CATEGORY_HEADLESS, "headless: invite confirmed ch=%llu node=%016llx addrs=%d",
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
char resp[1280];
char resp[INVITE_LINK_MAX + 256];
snprintf(resp, sizeof(resp),
"{\"link\":\"%s\",\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d}",
link, (unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
@@ -571,7 +571,7 @@ static void hc_handle_invite_nodes(struct headless_client* cli, int id, const ch
static void hc_handle_connect(struct headless_client* cli, int id, const char* json) {
struct headless_control* hc = cli->hc;
char link[1024];
char link[INVITE_LINK_MAX];
if (json_get_str(json, "link", link, sizeof(link)) < 0) { send_response(cli, id, NULL, "missing 'link' param"); return; }
struct InviteData d; char err[256];
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) { send_response(cli, id, NULL, err); return; }
@@ -580,17 +580,8 @@ static void hc_handle_connect(struct headless_client* cli, int id, const char* j
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: connect ch=%llu node=0x%016llx addrs=%d",
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount);
uint8_t addrs_buf[2048];
int addrs_len = invite_serialize_addrs(&d, addrs_buf, sizeof(addrs_buf));
if (addrs_len <= 0) { send_response(cli, id, NULL, "serialize addrs failed"); return; }
{ char chstr[64]; snprintf(chstr, sizeof(chstr), "%llu", (unsigned long long)d.channelId);
chat_core_ensure_channel_ready(hc->inst, chstr); }
chat_core_set_my_node_id(hc->inst, hc->inst->node_id);
chat_sync_connect_from_invite(hc->inst, d.channelId, d.nodeId, d.pubkey,
addrs_buf, d.addrCount, addrs_len, d.password_len ? d.password : NULL, d.join_key);
int result = chat_sync_join_link(hc->inst, 0, link, strlen(link));
if (result) { send_response(cli, id, NULL, "join could not start (see log)"); return; }
char resp[512]; snprintf(resp, sizeof(resp),
"{\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d,\"connecting\":true}",
@@ -647,8 +638,16 @@ static void hc_handle_invite_to(struct headless_client* cli, int id, const char*
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: invite_to ch=%s node=0x%016llx addr=%s:%d proto=%d",
ch, (unsigned long long)target_node_id, ip, port, proto);
chat_sync_invite_to_channel_with_addrs(hc->inst, ch, target_node_id,
pubkey_bin, addrs_data, 1, addrs_len);
struct InviteData target = { .channelId = strtoull(ch, NULL, 10), .join_key = 1, .addrCount = 1 };
memcpy(target.pubkey, pubkey_bin, 32); target.addrs[0].family = addrs_data[1];
target.addrs[0].proto = (uint8_t)proto; target.addrs[0].port = (uint16_t)port;
memcpy(target.addrs[0].address, addrs_data + 4, target.addrs[0].family == 4 ? 4 : 16);
char target_link[INVITE_LINK_MAX];
if (sc_derive_node_id_from_pubkey(pubkey_bin) != target_node_id ||
invite_link_encode(&target, NULL, target_link, sizeof(target_link)) < 0 ||
chat_sync_offer_link(hc->inst, 0, target.channelId, target_link, strlen(target_link))) {
send_response(cli, id, NULL, "invitation could not start"); return;
}
char resp[256]; snprintf(resp, sizeof(resp),
"{\"channel_id\":\"%s\",\"node_id\":\"0x%016llx\",\"inviting\":true}",
+4 -2
View File
@@ -63,8 +63,10 @@
* - Recovery восстанавливает сессии существующих участников и не использует
* invite-ключ как замену локальной таблицы участников.
*
* J <-> C: ETCP_RT_ID_CHAT_SYNC, заголовок [service:1][group_id:8][type:1], payload
* описан в chat_sync.h. A <-> C: ETCP_RT_ID_JOIN через etcp_router с подписью
* J <-> C: ETCP_ID_CHAT_SYNC, заголовок [service:1][group_id:8][type:1], payload
* описан в chat_sync.h. Для INFO_REQ/RESP, REQUEST, READY и ERROR payload начинается
* с attempt_id:8BE; C сохраняет его в waiter и возвращает в ответах. Это ID попытки,
* не join_key и не request_id интерфейса. A <-> C: ETCP_RT_ID_JOIN через etcp_router с подписью
* и шифрованием; subcommands и payload описаны ниже.
*
* Модуль отвечает только за маршрутизируемую (etcp_router) часть:
+127
View File
@@ -0,0 +1,127 @@
#include "chat_join_port.h"
#include "chat_sync.h"
#include "invite_link.h"
#include "../utun_instance.h"
#include "../../lib/mem.h"
#include "../../lib/debug_config.h"
#include <pthread.h>
#include <string.h>
struct join_command {
struct posted_task post; /* первый член: uasync освобождает всю команду */
struct join_command* next;
struct chat_join_port* port;
uint64_t request, channel, peer, offer;
size_t len;
int completed;
char link[];
};
struct chat_join_port {
pthread_mutex_t lock;
unsigned refs;
uint64_t active_request;
struct UTUN_INSTANCE* inst; /* только callback в uasync обращается к instance */
struct UASYNC* ua; /* жив до close; producer публикует под lock */
struct join_command* commands;
};
struct chat_join_port* chat_join_port_create(struct UTUN_INSTANCE* inst) {
struct chat_join_port* p = u_calloc(1, sizeof(*p));
if (!p) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "join port allocation failed"); return NULL; }
if (pthread_mutex_init(&p->lock, NULL)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "join port mutex initialization failed"); u_free(p); return NULL;
}
p->refs = 1; p->inst = inst; p->ua = inst->ua; return p;
}
void chat_join_port_retain(struct chat_join_port* p) {
if (!p) return;
pthread_mutex_lock(&p->lock); p->refs++; pthread_mutex_unlock(&p->lock);
}
void chat_join_port_release(struct chat_join_port* p) {
if (!p) return;
pthread_mutex_lock(&p->lock); unsigned refs = --p->refs; pthread_mutex_unlock(&p->lock);
if (!refs) { pthread_mutex_destroy(&p->lock); u_free(p); }
}
static void execute(void* arg) {
struct join_command* command = arg;
struct chat_join_port* p = command->port;
pthread_mutex_lock(&p->lock);
struct join_command** link = &p->commands;
while (*link && *link != command) link = &(*link)->next;
if (*link) *link = command->next;
struct UTUN_INSTANCE* inst = p->inst;
if (!command->completed && inst && !p->active_request) p->active_request = command->request;
pthread_mutex_unlock(&p->lock);
if (!command->completed && inst) {
int result = command->offer ? chat_sync_offer_link(inst, command->request, command->offer, command->link, command->len)
: chat_sync_join_link(inst, command->request, command->link, command->len);
if (result) {
chat_join_port_complete(p, command->request);
chat_sync_join_result(inst, command->request, command->channel, command->peer, result);
}
}
chat_join_port_release(p); /* память command освободит uasync */
}
static int submit(struct chat_join_port* p, uint64_t request, const char* link, size_t len, uint64_t offer) {
if (!p || !request || !link || !len || len >= INVITE_LINK_MAX) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "join command invalid or port unavailable"); return CHAT_CONNECT_INVALID;
}
struct InviteData invite;
if (invite_link_decode(link, len, &invite, NULL, 0)) return CHAT_CONNECT_INVALID;
struct join_command* command = u_calloc(1, sizeof(*command) + len);
if (!command) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "join command allocation failed"); return CHAT_CONNECT_ERROR; }
command->post.callback = execute; command->post.arg = command; command->port = p;
command->offer = offer; command->request = request; command->channel = offer ? offer : invite.channelId; command->peer = invite.nodeId;
command->len = len; memcpy(command->link, link, len);
pthread_mutex_lock(&p->lock);
if (!p->inst) {
pthread_mutex_unlock(&p->lock); u_free(command);
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "join command rejected: port closed"); return CHAT_CONNECT_CANCELLED;
}
if (p->active_request == request) {
pthread_mutex_unlock(&p->lock); u_free(command);
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "join request already active"); return CHAT_CONNECT_BUSY;
}
for (struct join_command* c = p->commands; c; c = c->next) if (c->request == request) {
pthread_mutex_unlock(&p->lock); u_free(command);
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "duplicate pending join request"); return CHAT_CONNECT_BUSY;
}
p->refs++; command->next = p->commands; p->commands = command;
uasync_post_reserved(p->ua, &command->post);
pthread_mutex_unlock(&p->lock);
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "join command queued request=%llu", (unsigned long long)request);
return 0;
}
void chat_join_port_close(struct chat_join_port* p) {
if (!p) return;
pthread_mutex_lock(&p->lock);
struct UTUN_INSTANCE* inst = p->inst;
struct UASYNC* ua = p->ua;
struct join_command* commands = p->commands;
p->commands = NULL; p->inst = NULL; p->ua = NULL;
pthread_mutex_unlock(&p->lock);
while (commands) {
struct join_command* c = commands; commands = c->next;
c->completed = 1;
uint64_t request = c->request, channel = c->channel, peer = c->peer;
/* Уже извлечённый batch исполнит пустой callback и снимет свою ссылку. */
if (uasync_cancel_post(ua, &c->post) == ERR_OK) chat_join_port_release(p);
chat_sync_join_result(inst, request, channel, peer, CHAT_CONNECT_CANCELLED);
}
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "join port closed");
}
void chat_join_port_complete(struct chat_join_port* p, uint64_t request) {
if (!p) return;
pthread_mutex_lock(&p->lock);
if (p->active_request == request) p->active_request = 0;
pthread_mutex_unlock(&p->lock);
}
int chat_join_port_submit(struct chat_join_port* p, uint64_t request, const char* link, size_t len) {
return submit(p, request, link, len, 0);
}
int chat_join_port_offer(struct chat_join_port* p, uint64_t request, uint64_t channel, const char* link, size_t len) {
if (!channel) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "offer missing channel"); return CHAT_CONNECT_INVALID; }
return submit(p, request, link, len, channel);
}
+32
View File
@@ -0,0 +1,32 @@
/* Потокобезопасный вход команд JOIN для GUI/JNI. Контекст удерживается фронтендом
* от bind до unbind; close в uasync отменяет команды и запрещает дальнейшие submit.
* После close порт живёт до release последних ссылок, не содержит живого instance. */
#ifndef CHAT_JOIN_PORT_H
#define CHAT_JOIN_PORT_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct UTUN_INSTANCE;
struct chat_join_port;
/* Создать порт в uasync; одна ссылка принадлежит chat_sync, NULL при ошибке. */
struct chat_join_port* chat_join_port_create(struct UTUN_INSTANCE* inst);
/* Получить отдельную ссылку на порт текущего чата; только uasync, NULL до init. */
struct chat_join_port* chat_sync_join_port(struct UTUN_INSTANCE* inst);
/* Управление отдельной ссылкой. retain требует уже удерживаемой ссылки. */
void chat_join_port_retain(struct chat_join_port* port);
void chat_join_port_release(struct chat_join_port* port);
/* uasync: снять корреляцию завершённой активной команды до уведомления. */
void chat_join_port_complete(struct chat_join_port* port, uint64_t request);
/* uasync: закрыть приём и завершить ожидающие команды CANCELLED; повтор допустим. */
void chat_join_port_close(struct chat_join_port* port);
/* Любой поток: скопировать ссылку и принять команду. 0 — будет один результат с request;
* <0 — команда не принята, результата не будет. request != 0; link заимствован до возврата. */
int chat_join_port_submit(struct chat_join_port* port, uint64_t request, const char* link, size_t len);
/* Те же гарантии для приглашения адресата ссылки в собственный channel. */
int chat_join_port_offer(struct chat_join_port* port, uint64_t request, uint64_t channel, const char* link, size_t len);
#ifdef __cplusplus
}
#endif
#endif
+80 -1
View File
@@ -4,6 +4,13 @@
#include "chat_setting.h"
#include "../utun_instance.h"
#include "../local_transport/protocol.h"
#include "../routing_layer/topo_group.h"
#include "../routing_layer/topo_group_connect.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../routing_layer/sock_match.h"
#include "../transport_layer/etcp.h"
#include "../transport_layer/etcp_connections.h"
#include "../transport_layer/node_conn_direct.h"
#include "../../lib/debug_config.h"
#include "../../lib/mem.h"
#include <string.h>
@@ -76,9 +83,81 @@ static void activity(struct UTUN_INSTANCE* inst, int active, void* arg) {
DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "failed to apply discovery activity node=%016llx", (unsigned long long)inst->node_id);
}
/* Только принимающие рабочие endpoints этого IP; скрытые и SOCKS/probe-сокеты не публикуются. */
static int route_sockets(void* arg, const struct sockaddr_storage* address, uint32_t index,
struct local_route_socket* out, size_t capacity) {
struct UTUN_INSTANCE* inst = arg;
size_t count = 0;
for (struct ETCP_SOCKET* sock = inst->etcp_sockets; sock; sock = sock->next) {
if (sock->type == CFG_SERVER_TYPE_PRIVATE || !sock_matches_lan(sock, address, index)) continue;
uint16_t port = ss_get_port(&sock->local_addr);
if (!port || !sock->generation) {
DEBUG_WARN(DEBUG_CATEGORY_DISCOVERY, "LAN socket omitted name=%s id=%u port=%u generation=%016llx",
sock->name, sock->sock_id, port, (unsigned long long)sock->generation); continue;
}
if (count == capacity) {
DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "too many LAN sockets interface=%u capacity=%zu", index, capacity); return -1;
}
out[count++] = (struct local_route_socket){ .socket_id = sock->sock_id, .generation = sock->generation,
.port = port, .protocol = sock->is_tcp ? LOCAL_ROUTE_TCP | (sock->reality_enabled ? LOCAL_ROUTE_REALITY : 0) : LOCAL_ROUTE_UDP };
}
return (int)count;
}
/* Ключи не покидают ядро; wire magic/version/type отделяют эту подпись от других протоколов. */
static int route_sign(void* arg, const uint8_t* data, size_t len, uint8_t signature[64]) {
struct UTUN_INSTANCE* inst = arg;
if (inst->node_id != sc_derive_node_id_from_pubkey(inst->my_keys.public_key) ||
sc_ed25519_sign(inst->my_ed25519_privkey, data, len, signature) != SC_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "LAN endpoint signing failed node=%016llx", (unsigned long long)inst->node_id); return -1;
}
return 0;
}
/* Отсутствие ключа не мешает открытому discovery, но не даёт адрес для NCD. */
static int route_verify(void* arg, uint64_t peer, const uint8_t* data, size_t len, const uint8_t signature[64]) {
struct UTUN_INSTANCE* inst = arg;
uint8_t key[32];
struct TOPO_NODE* node = topo_node_registry_find(inst->topo_groups, peer);
if (node && node->timestamp) memcpy(key, node->ed25519_public_key, sizeof(key));
else if (!inst->topo_sqlite_db || topo_node_sqlite_get_ed25519_pubkey(inst->topo_sqlite_db, peer, key)) {
DEBUG_DEBUG(DEBUG_CATEGORY_DISCOVERY, "LAN route ignored peer=%016llx reason=unknown signing key", (unsigned long long)peer); return 1;
}
if (sc_ed25519_verify(key, data, len, signature) != SC_OK) {
DEBUG_WARN(DEBUG_CATEGORY_DISCOVERY, "LAN route signature rejected peer=%016llx", (unsigned long long)peer); return -1;
}
return 0;
}
/* Общие адреса узла нельзя заменять, пока доступен прямой транспорт или любой BGP-путь. */
static int route_offline(void* arg, uint64_t peer) {
struct UTUN_INSTANCE* inst = arg;
struct ETCP_CONN* conn = inst->connections ? instance_find_conn(inst, peer) : NULL;
if (conn && conn->links_up && !conn->close_requested && conn->state != 2) return 0;
for (struct ll_entry* e = inst->topo_groups && inst->topo_groups->group_list ? inst->topo_groups->group_list->head : NULL;
e; e = e->next) {
if (topo_node_best_path(topo_node_find_by_id((struct TOPO_GROUP*)e, peer))) return 0;
}
return 1;
}
/* Обновление помогает уже удерживаемому NCD; право создавать соединение остаётся у сервисов. */
static void routes_changed(void* arg, uint64_t peer) {
struct UTUN_INSTANCE* inst = arg;
if (inst->connections && node_conn_direct_update_local(inst, peer) < 0)
DEBUG_ERROR(DEBUG_CATEGORY_DISCOVERY, "LAN address application failed peer=%016llx", (unsigned long long)peer);
for (struct ll_entry* e = inst->topo_groups && inst->topo_groups->group_list ? inst->topo_groups->group_list->head : NULL;
e; e = e->next) {
struct TOPO_GROUP* group = (struct TOPO_GROUP*)e;
if (group->group_type == TOPO_GROUP_TYPE_CHAT && !group->stopping) topo_group_connect_changed(group);
}
}
int chat_local_start(struct UTUN_INSTANCE* inst) {
if (inst->local_transport || inst->client_type == CLIENT_TYPE_SERVER) return 0;
struct local_transport_callbacks callbacks = { peers_changed, received, delivered, activity_changed };
struct local_transport_callbacks callbacks = { .peers_changed = peers_changed, .received = received, .delivery = delivered,
.activity_changed = activity_changed, .route_sockets = route_sockets, .route_sign = route_sign,
.route_verify = route_verify, .route_offline = route_offline, .routes_changed = routes_changed };
inst->local_transport = local_transport_create(inst->ua, inst->node_id, inst->name, &callbacks, inst);
if (!inst->local_transport) return -1;
if (chat_local_apply_settings(inst) < 0) { chat_local_stop(inst); return -1; }
+3 -1
View File
@@ -1,4 +1,6 @@
/* Адаптер локального транспорта: только доставка готовых invite-ссылок, без запуска JOIN. */
/* Адаптер LAN discovery: готовые invite-ссылки и подписанные адреса известных узлов, без запуска JOIN.
* Адреса подтверждаются nonce-ответом только при отсутствии прямого UP и BGP-путей во всех группах.
* Ключи берутся из registry/SQLite и не передаются в открытых объявлениях; NODEINFO не изменяется. */
#ifndef CHAT_LOCAL_H
#define CHAT_LOCAL_H
+115 -195
View File
@@ -22,7 +22,7 @@
#include <stdlib.h>
/* ─── live-онлайн: узел присутствует в BGP/topo_group канала ─── */
/* ─── live-онлайн: тот же живой путь, которым пользуется маршрутизатор ─── */
int chat_core_member_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id) {
struct chat_core_ctx* cc = CC(inst);
@@ -32,7 +32,7 @@ int chat_core_member_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint6
uint64_t group_id = strtoull(ch_id, NULL, 10);
struct TOPO_GROUP* g = topo_groups_find(cc->inst->topo_groups, group_id);
if (!g || g->group_type != TOPO_GROUP_TYPE_CHAT) return 0;
return topo_node_find_by_id(g, node_id) != NULL;
return topo_node_best_path(topo_node_find_by_id(g, node_id)) != NULL;
}
/* Рация — живая подписка на конкретную CHAT-группу; у self нет записи в group->nodes. */
@@ -87,13 +87,11 @@ static uint8_t parse_adm_tags_flags(const char* adm_tags) {
/* ─── сериализация (struct → wire) ─── */
void chat_core_serialize_member(const struct chat_member_display* src, uint8_t* dst) {
uint64_t nid = src->node_id;
uint16_t rtt = src->rtt;
memcpy(dst, &nid, 8);
mf_put64(dst, src->node_id);
dst[8] = src->online;
dst[9] = src->is_self;
dst[10] = src->flags;
memcpy(dst + 11, &rtt, 2);
dst[11] = (uint8_t)src->rtt; dst[12] = (uint8_t)(src->rtt >> 8);
dst[13] = src->nat_type;
dst[14] = src->storage;
/* Поле на проводе всегда завершается NUL и не содержит хвост памяти исходной строки. */
@@ -102,17 +100,20 @@ void chat_core_serialize_member(const struct chat_member_display* src, uint8_t*
mf_put64(dst + 79, src->avatar_server);
memcpy(dst + 87, src->avatar_file, 70);
dst[157] = src->storage_role;
dst[158] = src->verified; dst[159] = src->node_type;
dst[160] = src->conn_presence; dst[161] = src->conn_up;
dst[162] = (uint8_t)src->intermediaries; dst[163] = (uint8_t)(src->intermediaries >> 8);
DEBUG_TRACE(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: serialized node=%016llx name_bytes=%zu zero_padded=1",
(unsigned long long)src->node_id, strlen((const char*)dst + 15));
}
int chat_core_deserialize_member(const uint8_t* src, struct chat_member_display* dst) {
if (!src || !dst) return -1;
memcpy(&dst->node_id, src, 8);
dst->node_id = mf_get64(src);
dst->online = src[8];
dst->is_self = src[9];
dst->flags = src[10];
memcpy(&dst->rtt, src + 11, 2);
dst->rtt = (uint16_t)src[11] | ((uint16_t)src[12] << 8);
dst->nat_type = src[13];
dst->storage = src[14];
memcpy(dst->name, src + 15, 64);
@@ -120,6 +121,9 @@ int chat_core_deserialize_member(const uint8_t* src, struct chat_member_display*
dst->avatar_server = mf_get64(src + 79);
memcpy(dst->avatar_file, src + 87, 70); dst->avatar_file[69] = 0;
dst->storage_role = src[157];
dst->verified = src[158]; dst->node_type = src[159];
dst->conn_presence = src[160]; dst->conn_up = src[161];
dst->intermediaries = (uint16_t)src[162] | ((uint16_t)src[163] << 8);
return 0;
}
@@ -144,214 +148,122 @@ static int member_storage_role(const char* tags, sqlite3_stmt* st, int source, i
tags && json_flat_get(tags, "storage", role, sizeof(role)) == 0 && !strcmp(role, "yes");
}
/* ─── запрос списка мемберов ─── */
/* Один SELECT и одна функция заполнения для списка и одиночного обновления. */
static void member_from_row(struct UTUN_INSTANCE* inst, const char* ch_id, sqlite3_stmt* st, struct chat_member_display* m) {
memset(m, 0, sizeof(*m));
m->node_id = (uint64_t)sqlite3_column_int64(st, 0);
const char* node_name = (const char*)sqlite3_column_text(st, 1);
const char* local_nick = (const char*)sqlite3_column_text(st, 2);
const char* tags = (const char*)sqlite3_column_text(st, 3);
m->storage = (uint8_t)sqlite3_column_int(st, 4);
m->storage_role = member_storage_role(tags, st, 7, 8, sqlite3_column_int(st, 5));
m->is_self = m->node_id == inst->node_id;
m->flags = parse_adm_tags_flags(tags) | member_radio_flag(inst, ch_id, m->node_id);
if (sqlite3_column_int(st, 9)) m->flags |= CHAT_MEMBER_FLAG_RADIO_MUTED;
if (sqlite3_column_int(st, 5)) m->flags |= CHAT_MEMBER_FLAG_DELETED;
member_avatar(m, (const char*)sqlite3_column_text(st, 6));
char verified[16];
m->verified = tags && json_flat_get(tags, "verified", verified, sizeof(verified)) == 0 && !strcmp(verified, "yes");
m->node_type = (uint8_t)sqlite3_column_int(st, 10);
m->nat_type = (uint8_t)sqlite3_column_int(st, 11);
m->rtt = UINT16_MAX;
struct TOPO_GROUP* group = inst->topo_groups ? topo_groups_find(inst->topo_groups, strtoull(ch_id, NULL, 10)) : NULL;
struct TOPO_GROUP_NODE* node = group ? topo_node_find_by_id(group, m->node_id) : NULL;
if (node) {
m->conn_presence = node->conn_presence; m->conn_up = node->conn_up;
struct TOPO_NODEPATH* path = topo_node_best_path(node);
if (path) {
m->rtt = topo_get_chain_rtt(node);
m->intermediaries = path->hop_count > 0 ? path->hop_count - 1 : 0;
m->online = 1;
}
}
if (m->is_self) m->online = 1;
if (m->flags & CHAT_MEMBER_FLAG_DELETED) m->online = 0;
if (!m->online) m->flags &= ~(CHAT_MEMBER_FLAG_PHONE_KNOWN | CHAT_MEMBER_FLAG_PHONE_BUSY);
const char* name = local_nick && local_nick[0] ? local_nick : node_name;
if (name && name[0]) snprintf(m->name, sizeof(m->name), "%s", name);
else snprintf(m->name, sizeof(m->name), "0x%04X", (unsigned)(m->node_id & 0xFFFF));
}
int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t** out, int* count) {
static int member_query(struct UTUN_INSTANCE* inst, const char* ch_id, int single, uint64_t node_id,
uint8_t** out, int* count) {
struct chat_core_ctx* cc = CC(inst);
if (!out || !count) return -1;
*out = NULL; *count = 0;
if (!cc || !cc->initialized || !ch_id || !cc->db || !cc->inst) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: get_list — not initialized");
return -1;
if (!cc || !cc->initialized || !ch_id || !ch_id[0] || !cc->db || !cc->inst) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: invalid instance/channel"); return -1;
}
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
uint64_t my_id = cc->my_node_id;
/* 1. запрашиваем всех мемберов */
sqlite3_stmt* st = NULL;
char sql[512];
char table[80]; peers_table_name(ch_id, table, sizeof(table));
char sql[1536];
snprintf(sql, sizeof(sql),
"SELECT p.node_id, COALESCE(n.name,''),"
" COALESCE(p.local_nick,''), COALESCE(p.adm_tags,''),"
" COALESCE(p.storage,0), COALESCE(p.deleted,0), COALESCE(p.userinfo,''),p.source,p.adm_tags_sig,p.radio_muted"
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
" ORDER BY p.node_id ASC", peers_tbl);
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: get_list — query failed ch=%s err=%s",
ch_id, sqlite3_errmsg(cc->db));
return -1;
"SELECT p.node_id,COALESCE(n.name,''),COALESCE(p.local_nick,''),COALESCE(p.adm_tags,''),"
"p.storage,p.deleted,p.userinfo,p.source,p.adm_tags_sig,p.radio_muted,p.node_type,"
"(SELECT CASE WHEN COUNT(*)=0 THEN 255 WHEN MAX(addr_type=1) THEN 1 WHEN MAX(addr_type=5) THEN 5"
" WHEN MAX(addr_type=2) THEN 2 WHEN MAX(addr_type=6) THEN 6 WHEN MAX(addr_type=4) THEN 4"
" WHEN MAX(addr_type=3) THEN 3 ELSE 0 END FROM node_addresses WHERE node_id=p.node_id)"
" FROM \"%s\" p LEFT JOIN nodes n ON n.node_id=p.node_id %s ORDER BY p.node_id ASC",
table, single ? "WHERE p.node_id=?" : "");
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL);
if (rc == SQLITE_OK && single) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (rc != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: query ch=%s rc=%d: %s", ch_id, rc, sqlite3_errmsg(cc->db));
sqlite3_finalize(st); return -1;
}
struct chat_member_display* members = NULL;
int cap = 0, cnt = 0;
while (sqlite3_step(st) == SQLITE_ROW) {
if (cnt >= cap) {
cap = cap ? cap * 2 : 32;
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
if (cnt == cap) {
cap = cap ? cap * 2 : (single ? 1 : 32);
struct chat_member_display* tmp = u_realloc(members, (size_t)cap * sizeof(*members));
if (!tmp) { u_free(members); sqlite3_finalize(st); return -1; }
if (!tmp) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: allocation ch=%s count=%d", ch_id, cnt);
u_free(members); sqlite3_finalize(st); return -1;
}
members = tmp;
}
struct chat_member_display* m = &members[cnt];
m->node_id = (uint64_t)sqlite3_column_int64(st, 0);
member_avatar(m, (const char*)sqlite3_column_text(st, 6));
m->online = (uint8_t)chat_core_member_online(inst, ch_id, m->node_id);
const char* node_name = (const char*)sqlite3_column_text(st, 1);
const char* local_nick = (const char*)sqlite3_column_text(st, 2);
const char* adm_tags = (const char*)sqlite3_column_text(st, 3);
m->storage = (uint8_t)sqlite3_column_int(st, 4);
m->storage_role = member_storage_role(adm_tags, st, 7, 8, sqlite3_column_int(st, 5));
m->is_self = (uint8_t)(m->node_id == my_id ? 1 : 0);
m->flags = parse_adm_tags_flags(adm_tags) | member_radio_flag(inst, ch_id, m->node_id);
if (sqlite3_column_int(st, 9)) m->flags |= CHAT_MEMBER_FLAG_RADIO_MUTED;
if (sqlite3_column_int(st, 5)) m->flags |= CHAT_MEMBER_FLAG_DELETED;
m->rtt = 0xFFFF;
m->nat_type = 0xFF;
/* display name: local_nick если задан, иначе node name, иначе short hex */
const char* display = (local_nick && local_nick[0]) ? local_nick :
(node_name && node_name[0]) ? node_name : NULL;
if (display) {
strncpy(m->name, display, sizeof(m->name) - 1);
m->name[sizeof(m->name) - 1] = '\0';
} else {
snprintf(m->name, sizeof(m->name), "0x%04X", (unsigned)(m->node_id & 0xFFFF));
}
cnt++;
member_from_row(inst, ch_id, st, &members[cnt++]);
}
sqlite3_finalize(st);
if (cnt == 0) {
*out = NULL; *count = 0;
return 0;
if (rc != SQLITE_DONE) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: read ch=%s rc=%d: %s", ch_id, rc, sqlite3_errmsg(cc->db));
u_free(members); return -1;
}
/* 2. RTT и NAT type из node_addresses (batch) */
char* id_list = u_malloc((size_t)cnt * 21 + 4);
if (!id_list) { u_free(members); return -1; }
id_list[0] = '\0';
for (int i = 0; i < cnt; i++) {
if (i > 0) strcat(id_list, ",");
char nbuf[24]; snprintf(nbuf, sizeof(nbuf), "%llu", (unsigned long long)members[i].node_id);
strcat(id_list, nbuf);
}
snprintf(sql, sizeof(sql),
"SELECT node_id, MIN(CASE WHEN rtt > 0 THEN rtt END),"
" CASE WHEN MAX(addr_type=1) THEN 1"
" WHEN MAX(addr_type=5) THEN 5"
" WHEN MAX(addr_type=2) THEN 2"
" WHEN MAX(addr_type=6) THEN 6"
" WHEN MAX(addr_type=4) THEN 4"
" WHEN MAX(addr_type=3) THEN 3"
" ELSE 0 END"
" FROM node_addresses WHERE node_id IN (%s) GROUP BY node_id", id_list);
u_free(id_list);
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
while (sqlite3_step(st) == SQLITE_ROW) {
uint64_t nid = (uint64_t)sqlite3_column_int64(st, 0);
int rtt_val = sqlite3_column_type(st, 1) == SQLITE_NULL ? 0xFFFF : sqlite3_column_int(st, 1);
int at = sqlite3_column_int(st, 2);
for (int i = 0; i < cnt; i++) {
if (members[i].node_id == nid) {
members[i].rtt = (uint16_t)(rtt_val > 0xFFFF ? 0xFFFF : rtt_val);
members[i].nat_type = (uint8_t)at;
break;
}
}
}
sqlite3_finalize(st);
}
/* 3. сортируем: онлайн первыми, затем по node_id (стабильно) */
for (int i = 1; i < cnt; i++) {
struct chat_member_display key = members[i];
int j = i - 1;
while (j >= 0 && ((members[j].online < key.online)
|| (members[j].online == key.online && members[j].node_id > key.node_id))) {
members[j + 1] = members[j];
j--;
while (j >= 0 && (members[j].online < key.online ||
(members[j].online == key.online && members[j].node_id > key.node_id))) {
members[j + 1] = members[j]; j--;
}
members[j + 1] = key;
}
/* 4. сериализуем в wire-формат */
size_t wire_sz = (size_t)cnt * CHAT_MEMBER_DISPLAY_SIZE;
uint8_t* wire = u_malloc(wire_sz);
if (!wire) { u_free(members); return -1; }
for (int i = 0; i < cnt; i++)
chat_core_serialize_member(&members[i], wire + i * CHAT_MEMBER_DISPLAY_SIZE);
u_free(members);
*out = wire;
*count = cnt;
if (cnt) {
*out = u_malloc((size_t)cnt * CHAT_MEMBER_DISPLAY_SIZE);
if (!*out) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: wire allocation ch=%s count=%d", ch_id, cnt);
u_free(members); return -1;
}
for (int i = 0; i < cnt; i++) chat_core_serialize_member(&members[i], *out + i * CHAT_MEMBER_DISPLAY_SIZE);
}
u_free(members); *count = cnt;
return 0;
}
/* ─── один мембер ─── */
int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t** out, int* count) {
if (!out || !count) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: null output"); return -1; }
return member_query(inst, ch_id, 0, 0, out, count);
}
int chat_core_get_single_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id, uint8_t* out) {
struct chat_core_ctx* cc = CC(inst);
if (!out) return -1;
if (!cc || !cc->initialized || !ch_id || !cc->db || !cc->inst) return -1;
char peers_tbl[80]; peers_table_name(ch_id, peers_tbl, sizeof(peers_tbl));
uint64_t my_id = cc->my_node_id;
sqlite3_stmt* st = NULL;
char sql[384];
snprintf(sql, sizeof(sql),
"SELECT COALESCE(n.name,''),"
" COALESCE(p.local_nick,''), COALESCE(p.adm_tags,''),"
" COALESCE(p.storage,0), COALESCE(p.deleted,0), COALESCE(p.userinfo,''),p.source,p.adm_tags_sig,p.radio_muted"
" FROM \"%s\" p LEFT JOIN nodes n ON p.node_id=n.node_id"
" WHERE p.node_id=?", peers_tbl);
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) != SQLITE_OK) return -1;
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) != SQLITE_ROW) { sqlite3_finalize(st); return -1; }
struct chat_member_display m;
m.node_id = node_id;
member_avatar(&m, (const char*)sqlite3_column_text(st, 5));
m.online = (uint8_t)chat_core_member_online(inst, ch_id, node_id);
const char* node_name = (const char*)sqlite3_column_text(st, 0);
const char* local_nick = (const char*)sqlite3_column_text(st, 1);
const char* adm_tags = (const char*)sqlite3_column_text(st, 2);
m.storage = (uint8_t)sqlite3_column_int(st, 3);
m.storage_role = member_storage_role(adm_tags, st, 6, 7, sqlite3_column_int(st, 4));
m.is_self = (uint8_t)(node_id == my_id ? 1 : 0);
m.flags = parse_adm_tags_flags(adm_tags) | member_radio_flag(inst, ch_id, node_id);
if (sqlite3_column_int(st, 8)) m.flags |= CHAT_MEMBER_FLAG_RADIO_MUTED;
if (sqlite3_column_int(st, 4)) m.flags |= CHAT_MEMBER_FLAG_DELETED;
m.rtt = 0xFFFF;
m.nat_type = 0xFF;
const char* display = (local_nick && local_nick[0]) ? local_nick :
(node_name && node_name[0]) ? node_name : NULL;
if (display) {
strncpy(m.name, display, sizeof(m.name) - 1);
m.name[sizeof(m.name) - 1] = '\0';
} else {
snprintf(m.name, sizeof(m.name), "0x%04X", (unsigned)(node_id & 0xFFFF));
if (!out) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: null single output"); return -1; }
uint8_t* wire = NULL; int count = 0;
if (member_query(inst, ch_id, 1, node_id, &wire, &count) < 0) return -1;
if (count != 1) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member snapshot: absent ch=%s node=%016llx", ch_id, (unsigned long long)node_id);
u_free(wire); return -1;
}
sqlite3_finalize(st);
/* RTT / NAT */
snprintf(sql, sizeof(sql),
"SELECT MIN(CASE WHEN rtt > 0 THEN rtt END),"
" CASE WHEN MAX(addr_type=1) THEN 1"
" WHEN MAX(addr_type=5) THEN 5"
" WHEN MAX(addr_type=2) THEN 2"
" WHEN MAX(addr_type=6) THEN 6"
" WHEN MAX(addr_type=4) THEN 4"
" WHEN MAX(addr_type=3) THEN 3"
" ELSE 0 END"
" FROM node_addresses WHERE node_id=?");
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
int rtt_val = sqlite3_column_type(st, 0) == SQLITE_NULL ? 0xFFFF : sqlite3_column_int(st, 0);
m.rtt = (uint16_t)(rtt_val > 0xFFFF ? 0xFFFF : rtt_val);
m.nat_type = (uint8_t)sqlite3_column_int(st, 1);
}
sqlite3_finalize(st);
}
chat_core_serialize_member(&m, out);
memcpy(out, wire, CHAT_MEMBER_DISPLAY_SIZE); u_free(wire);
return 0;
}
@@ -438,11 +350,7 @@ void chat_core_request_member_rtt_trampoline(void* arg) {
if (!evt) { u_free(members); return; }
/* шлём мемберов по одному (RTT мог измениться у каждого) */
uint16_t rtt_nodata = 0xFFFF;
for (int i = 0; i < count; i++) {
uint16_t rtt_val;
memcpy(&rtt_val, members + i * CHAT_MEMBER_DISPLAY_SIZE + 11, 2);
if (rtt_val == rtt_nodata) continue;
evt[0] = (uint8_t)ch_len;
memcpy(evt + 1, ch_id, ch_len);
evt[1 + ch_len] = 1;
@@ -490,7 +398,19 @@ static void on_member_props_changed(uint64_t node_id, const char* adm_tags, cons
}
}
static void on_member_route_changed(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* node, void* arg) {
if (!group || !node || group->group_type != TOPO_GROUP_TYPE_CHAT || !group->channel_id[0]) return;
on_member_props_changed(node->node_id, NULL, group->channel_id, arg);
}
void chat_member_init(struct UTUN_INSTANCE* inst) {
member_sync_add_props_cbk(inst, on_member_props_changed, inst);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: initialized (BGP node events → MEMBER_UPDATED)");
utun_add_nodeinfo_cbk(inst, on_member_route_changed, inst);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: initialized (group properties/routes -> complete MEMBER_UPDATED)");
}
void chat_member_destroy(struct UTUN_INSTANCE* inst) {
utun_remove_nodeinfo_cbk(inst, on_member_route_changed, inst);
member_sync_remove_props_cbk(inst, on_member_props_changed, inst);
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "chat_member: properties/routes subscriptions removed");
}
+67 -6
View File
@@ -3,21 +3,26 @@
*
* Используется chatgui (desktop), headless CLI, Android.
* Live-снимки для всех потребителей формирует chat_core_get_member_list() в uasync-потоке.
* Android дополняет ими свой JSON-список с расширенными свойствами участников.
* Снимок содержит свойства участника и состояние маршрута именно его CHAT-группы.
*
* Формат на проводе (ручная сериализация, фиксированный размер 158 байт):
* Формат на проводе (ручная сериализация, фиксированный размер 164 байта):
* [0-7] node_id uint64_t LE
* [8] online uint8_t
* [9] is_self uint8_t
* [10] flags uint8_t 0x01=supernode 0x02=admin 0x04=moder 0x08=deleted 0x10=radio 0x20=local radio mute
* 0x40=phone status known, 0x80=busy on a call (live BGP hint)
* [11-12] rtt uint16_t LE (ms, 0xFFFF=нет данных)
* [11-12] rtt uint16_t LE (0,1 мс выбранного живого пути, 0xFFFF=нет данных)
* [13] nat_type uint8_t ADDR_TYPE_*
* [14] storage uint8_t
* [15-78] name char[64] local_nick если задан, иначе node name. \0-padded
* [79-86] avatar_server uint64_t LE; 0 = нет аватарки
* [87-156] avatar_file char[70], sha256.json с NUL
* [157] storage_role uint8_t, подписанная роль, независимо от сетевого приоритета storage
* [158] verified uint8_t, административный тег verified=yes
* [159] node_type uint8_t, вычисленный тип узла из peers
* [160] conn_presence uint8_t, известные виды подключения NCONN_*
* [161] conn_up uint8_t, работающие виды подключения NCONN_*
* [162-163] intermediaries uint16_t LE, число посредников выбранного пути
*/
#ifndef CHAT_MEMBER_H
@@ -26,6 +31,8 @@
#include <stdint.h>
#include <stddef.h>
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
@@ -39,7 +46,7 @@ extern "C" {
#define CHAT_MEMBER_FLAG_PHONE_KNOWN 0x40 /* опубликовано состояние телефонной занятости */
#define CHAT_MEMBER_FLAG_PHONE_BUSY 0x80 /* занят звонком; цвет трубки в интерфейсе */
#define CHAT_MEMBER_DISPLAY_SIZE 158
#define CHAT_MEMBER_DISPLAY_SIZE 164
/* Получить всех мемберов канала. out = u_malloc-массив, count = количество. */
int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uint8_t** out, int* count);
@@ -47,7 +54,7 @@ int chat_core_get_member_list(struct UTUN_INSTANCE* inst, const char* ch_id, uin
/* Получить одного мембера по node_id. out = буфер размером CHAT_MEMBER_DISPLAY_SIZE. */
int chat_core_get_single_member(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id, uint8_t* out);
/* Live-онлайн: узел присутствует в BGP/topo_group канала (self всегда онлайн).
/* Live-онлайн: маршрутизатор располагает живым путём в группе канала (self всегда онлайн).
Не читает nodes.online — статус не хранится в БД. */
int chat_core_member_online(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t node_id);
@@ -64,6 +71,11 @@ struct chat_member_display {
uint64_t avatar_server;
char avatar_file[70];
uint8_t storage_role;
uint8_t verified; /* административное подтверждение участника */
uint8_t node_type; /* классификация узла, не состояние маршрута */
uint8_t conn_presence; /* известные виды подключения NCONN_* */
uint8_t conn_up; /* работающие виды подключения NCONN_* */
uint16_t intermediaries; /* число посредников выбранного живого пути */
};
void chat_core_serialize_member(const struct chat_member_display* src, uint8_t* dst);
@@ -83,8 +95,57 @@ int chat_core_toggle_member_radio_mute(struct UTUN_INSTANCE* inst, const char* c
struct chat_member_radio_mute_req { struct UTUN_INSTANCE* inst; char ch_id[64]; uint64_t node_id; };
void chat_core_toggle_member_radio_mute_trampoline(void* arg);
/* Инициализация: регистрирует node_props_changed callback для рассылки MEMBER_UPDATED. */
/* Подписки свойств/маршрутов → MEMBER_UPDATED; вызываются в uasync при init/destroy чата. */
void chat_member_init(struct UTUN_INSTANCE* inst);
void chat_member_destroy(struct UTUN_INSTANCE* inst);
/* Дополнительная диагностика карточки; online/RTT берутся только из chat_member_display.
* Запрос выделяет GUI через u_calloc и передаёт в uasync; trampoline освобождает его.
* Ответ CHAT_EVT_MEMBER_DETAIL всегда повторяет request_id/group_id/node_id, включая ошибки. */
struct chat_member_detail_req {
struct UTUN_INSTANCE* inst; /* ядро, в uasync которого выполняется сбор */
uint64_t group_id; /* конкретная CHAT-группа */
uint64_t node_id; /* выбранный участник */
uint64_t request_id; /* ненулевой ID запроса, не повторяется при смене выбора */
};
/* Wire LE: request:8, group:8, node:8, result:1, transport_flags:1, own_tcp:2,
* hop_count:1, link_count:1, sock_count:1; далее hops:8*, links:64*, socks:48*.
* transport_flags: bit0=conn_present, bit1=links_up, bit2=initialized; не доступность в группе.
* hops — список выбранного пути от назначения к первому посреднику; у offline/self пустой.
* Ошибка содержит только заголовок.
* link: tcp:1,state:1,status:1,ready:1,server:1,local_family:1,local_addr:16,local_port:2,
* remote_family:1,remote_addr:16,remote_port:2,socket_name:20,reality:1.
* sock: id:4,port:2,links_up:2,links_down:2,ifname:38. Строки завершаются NUL. */
#define CHAT_MEMBER_DETAIL_HEADER_SIZE 31
#define CHAT_MEMBER_DETAIL_MAX_HOPS 16
#define CHAT_MEMBER_DETAIL_MAX_LINKS 16
#define CHAT_MEMBER_DETAIL_MAX_SOCKS 16
#define CHAT_MEMBER_DETAIL_LINK_SIZE 64
#define CHAT_MEMBER_DETAIL_SOCK_SIZE 48
#define CHAT_MEMBER_DETAIL_MAX_SIZE (CHAT_MEMBER_DETAIL_HEADER_SIZE + 8 * CHAT_MEMBER_DETAIL_MAX_HOPS + \
CHAT_MEMBER_DETAIL_LINK_SIZE * CHAT_MEMBER_DETAIL_MAX_LINKS + CHAT_MEMBER_DETAIL_SOCK_SIZE * CHAT_MEMBER_DETAIL_MAX_SOCKS)
enum chat_member_detail_result {
CHAT_MEMBER_DETAIL_OK = 0,
CHAT_MEMBER_DETAIL_UNAVAILABLE = 1, /* чат/группа остановлены */
CHAT_MEMBER_DETAIL_NOT_MEMBER = 2, /* запись участника отсутствует */
CHAT_MEMBER_DETAIL_INVALID = 3, /* неверные аргументы запроса */
CHAT_MEMBER_DETAIL_ERROR = 4 /* ошибка чтения данных участника */
};
/* Проверенный view заимствует массивы из входного пакета; живёт не дольше data. */
struct chat_member_detail_view {
uint64_t request_id, group_id, node_id; /* адресат ответа */
uint8_t result, transport_flags; /* результат и состояние физического транспорта */
uint16_t own_tcp_active; /* TCP-соединения self */
uint8_t hop_count, link_count, sock_count; /* длины массивов */
const uint8_t *hops, *links, *socks; /* неизменяемые wire-массивы */
};
/* Проверить весь пакет до применения: 0=успех, -1=ошибка с логом; out при ошибке не меняется. */
int chat_core_parse_member_detail(const uint8_t* data, size_t len, struct chat_member_detail_view* out);
void chat_core_collect_member_detail_trampoline(void* arg);
#ifdef __cplusplus
}
+127 -99
View File
@@ -23,6 +23,7 @@
#include "../../lib/mem.h"
#include "../../lib/u_async.h"
#include "../../lib/platform_compat.h"
#include "../../lib/file_io.h"
#include <openssl/sha.h>
#include <stdlib.h>
@@ -156,6 +157,9 @@ void chat_core_submit_trampoline(void* arg) {
/* ─── Медиа-сообщения: async регистрация в media_files + отправка ─── */
struct media_submit_ctx {
struct media_submit_ctx* next;
struct media_destination* destination; // Общая с загрузчиком резервация пути до завершения workers.
int delete_pending;
struct UTUN_INSTANCE* inst;
char channel_id[64];
char content_type[32];
@@ -172,8 +176,34 @@ struct media_submit_ctx {
/* Завершение владеющей асинхронной отправки, включая все ошибки подготовки/регистрации. */
static void media_submit_finish(struct media_submit_ctx* ctx, int success) {
struct UTUN_INSTANCE* inst = ctx->inst;
struct chat_core_ctx* cc = CC(inst);
char channel[64]; snprintf(channel, sizeof(channel), "%s", ctx->channel_id);
/* Синхронный обработчик события может запросить удаление; ctx пока остаётся в списке владельца. */
chat_event_submitted(ctx->inst, ctx->request_id, success);
int delete_pending = ctx->delete_pending;
struct media_submit_ctx** slot = &cc->media_submissions;
while (*slot && *slot != ctx) slot = &(*slot)->next;
if (*slot) *slot = ctx->next;
else DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media submission missing from owner ch=%s", channel);
media_delivery_release(inst, ctx->destination);
u_free(ctx);
if (delete_pending) {
for (struct media_submit_ctx* pending = cc->media_submissions; pending; pending = pending->next)
if (!strcmp(pending->channel_id, channel)) return;
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media submissions drained; continuing channel deletion ch=%s", channel);
chat_core_delete_channel(inst, channel);
}
}
int chat_media_defer_channel_delete(struct UTUN_INSTANCE* inst, const char* channel) {
struct chat_core_ctx* cc = CC(inst); int pending = 0;
for (struct media_submit_ctx* ctx = cc->media_submissions; ctx; ctx = ctx->next) {
if (strcmp(ctx->channel_id, channel)) continue;
ctx->delete_pending = 1; pending++;
}
if (pending) DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "channel deletion waiting for outgoing media ch=%s tasks=%d", channel, pending);
return pending != 0;
}
static void on_media_registered(void* arg, int err, const struct media_index_result* result) {
@@ -186,6 +216,10 @@ static void on_media_registered(void* arg, int err, const struct media_index_res
media_submit_finish(mctx, 0);
return;
}
if (mctx->delete_pending) {
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "prepared media retained for pending deletion ch=%s", mctx->channel_id);
media_submit_finish(mctx, 0); return;
}
struct DB_SYNC_INSTANCE* si = si_find(mctx->inst, mctx->channel_id);
if (!si) {
@@ -325,14 +359,8 @@ static void on_video_prepared(void* arg, int err, const struct video_info* vi) {
CC_ID, vi->width, vi->height, (long long)vi->duration_ms);
}
char media_base[1024];
{
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash)
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
}
char media_base[MEDIA_BASE_MAX];
if (media_index_get_base(cc->db, media_base)) { media_submit_finish(mctx, 0); return; }
media_index_register_async(
mctx->inst->media_async, mctx->inst->ua, cc->db,
@@ -350,6 +378,23 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
if (req) chat_event_submitted(inst, req->request_id, 0);
return;
}
char media_base[MEDIA_BASE_MAX], location[MEDIA_PATH_MAX], destination[MEDIA_PATH_MAX];
if (media_index_get_base(cc->db, media_base) || media_index_location(req->media_dest, media_base, location)
|| media_index_path(media_base, location, destination)) {
chat_event_submitted(inst, req->request_id, 0); return;
}
for (struct media_submit_ctx* pending = cc->media_submissions; pending; pending = pending->next) {
if ((!strcmp(pending->channel_id, req->channel_id) && pending->delete_pending)
|| !strcmp(pending->media_dest, destination)) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media submit rejected: channel deleting/destination busy ch=%s path=%s",
req->channel_id, req->media_dest);
chat_event_submitted(inst, req->request_id, 0); return;
}
}
if (!si_find(inst, req->channel_id)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media submit without channel ch=%s", req->channel_id);
chat_event_submitted(inst, req->request_id, 0); return;
}
char reply_hex[65];
if (msg_reply_hex(cc, req->channel_id, req->reply_to, reply_hex) < 0) {
chat_event_submitted(inst, req->request_id, 0); return;
@@ -358,7 +403,11 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
/* лимит одного медиафайла применяется и к локальным отправкам */
uint64_t unit = inst->config->global.chatserver_storage_unit_size;
if (unit > 0) {
int64_t sz = (int64_t)ma_file_size(req->media_src);
int64_t sz = ma_file_size(req->media_src);
if (sz < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media submit source size unavailable path=%s", req->media_src);
chat_event_submitted(inst, req->request_id, 0); return;
}
if (sz > 0 && (uint64_t)sz > unit) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: media too large for local send (%lld > %llu bytes), rejected ch=%s",
CC_ID, (long long)sz, (unsigned long long)unit, req->channel_id);
@@ -377,28 +426,25 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
return;
}
/* build media_base from db_path: dirname(db_path) e.g. /path/to/data */
char media_base[1024];
{
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash)
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
}
/* copy fields for async callback (req is freed by trampoline immediately) */
struct media_submit_ctx* mctx = u_calloc(1, sizeof(*mctx));
if (!mctx) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media context allocation failed", CC_ID);
chat_event_submitted(inst, req->request_id, 0); return;
}
mctx->destination = media_delivery_reserve(inst, destination);
if (!mctx->destination) { u_free(mctx); chat_event_submitted(inst, req->request_id, 0); return; }
if (media_index_track_preparation(cc->db, req->channel_id, destination, media_base)) {
media_delivery_release(inst, mctx->destination); u_free(mctx);
chat_event_submitted(inst, req->request_id, 0); return;
}
mctx->inst = inst;
mctx->request_id = req->request_id;
mctx->next = cc->media_submissions; cc->media_submissions = mctx;
memcpy(mctx->reply_hex, reply_hex, strlen(reply_hex) + 1);
snprintf(mctx->channel_id, sizeof(mctx->channel_id), "%s", req->channel_id);
snprintf(mctx->content_type, sizeof(mctx->content_type), "%s", req->content_type);
snprintf(mctx->media_dest, sizeof(mctx->media_dest), "%s", req->media_dest);
snprintf(mctx->media_dest, sizeof(mctx->media_dest), "%s", destination);
mctx->timestamp = req->timestamp;
mctx->duration_ms = req->duration_ms;
mctx->width = req->width;
@@ -409,7 +455,7 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
}
if (req->media_video) {
if (video_transcode_start(inst->media_async, inst->ua, req->media_src, req->media_dest, on_video_prepared, mctx) < 0)
if (video_transcode_start(inst->media_async, inst->ua, req->media_src, destination, on_video_prepared, mctx) < 0)
on_video_prepared(mctx, -1, NULL);
return;
}
@@ -418,7 +464,7 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_
inst->media_async, inst->ua, cc->db,
cc->my_node_id, inst->my_ed25519_privkey,
req->channel_id,
req->media_src, req->media_dest, req->media_copy ? 1 : 0,
req->media_src, destination, req->media_copy ? 1 : 0,
media_base,
on_media_registered, mctx);
}
@@ -871,16 +917,10 @@ static int md_start_download(struct UTUN_INSTANCE* inst,
char final_name[256];
media_final_name(&result, content_type, base_filename, final_name, sizeof(final_name));
char dest[1024], media_base[512];
{
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash) {
snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
} else {
snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
}
}
snprintf(dest, sizeof(dest), "%s/media/%s/%s", media_base, ch_id, final_name);
char dest[MEDIA_PATH_MAX], media_base[MEDIA_BASE_MAX], relative[MEDIA_PATH_MAX];
int length = snprintf(relative, sizeof(relative), "media/%s/%s", ch_id, final_name);
if (length < 0 || length >= (int)sizeof(relative) || media_index_get_base(cc->db, media_base)
|| media_index_path(media_base, relative, dest)) { media_index_result_free(&result); return -1; }
struct md_done_ctx* ctx = u_calloc(1, sizeof(*ctx));
if (!ctx) { media_index_result_free(&result); return -1; }
@@ -897,9 +937,13 @@ static int md_start_download(struct UTUN_INSTANCE* inst,
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: download start ch=%s ct=%s file=%s blocks=%d size=%lld dest=%s",
CC_ID, ch_id, content_type ? content_type : "?", final_name, nb, (long long)fsize, dest);
uint64_t gid = strtoull(ch_id, NULL, 10);
media_download_start(inst, gid, &result, dest, media_base, author_node_id, md_download_done_cb, ctx, md_download_progress_cb, ctx);
int started = media_download_start(inst, gid, &result, dest, media_base, author_node_id,
md_download_done_cb, ctx, md_download_progress_cb, ctx);
media_index_result_free(&result);
return 0;
if (started < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media download rejected ch=%s path=%s", ch_id, dest); u_free(ctx);
}
return started;
}
static int md_auto_download(struct UTUN_INSTANCE* inst,
@@ -1097,10 +1141,8 @@ void chat_media_backfill(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db) return;
char media_base[512];
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
char media_base[MEDIA_BASE_MAX];
if (media_index_get_base(cc->db, media_base)) return;
int total = 0;
sqlite3_stmt* cs = NULL;
@@ -1153,43 +1195,15 @@ void chat_media_backfill(struct UTUN_INSTANCE* inst) {
char path[1536];
snprintf(path, sizeof(path), "%s/media/%s/%s", media_base, ch_id, final_name);
int fsz = ma_file_size(path);
if (fsz <= 0 || (int64_t)fsz != result.file_size) {
int64_t fsz = ma_file_size(path);
if (fsz <= 0 || fsz != result.file_size) {
u_free(body); media_index_result_free(&result);
continue;
}
int registered = 0;
for (int n = 0; n < result.num_blocks; n++) {
int exists = 0;
sqlite3_stmt* ex = NULL;
if (sqlite3_prepare_v2(cc->db, "SELECT 1 FROM media_files WHERE block_id=? LIMIT 1", -1, &ex, NULL) == SQLITE_OK) {
sqlite3_bind_blob(ex, 1, result.block_ids + n * 16, 16, SQLITE_STATIC);
exists = (sqlite3_step(ex) == SQLITE_ROW);
sqlite3_finalize(ex);
}
if (exists) continue;
char rel[512];
size_t bl = strlen(media_base);
if (strncmp(path, media_base, bl) == 0 && path[bl] == '/') snprintf(rel, sizeof(rel), "%s", path + bl + 1);
else snprintf(rel, sizeof(rel), "%s", path);
int64_t chunk_size = (n == result.num_blocks - 1) ? result.file_size - (int64_t)n * result.block_size : result.block_size;
int64_t offset = (int64_t)n * result.block_size;
if (media_index_register_downloaded(cc->db, result.media_id, result.block_ids + n * 16,
result.content_hash, ch_id, rel, cc->my_node_id,
result.file_size, chunk_size, n, offset) == 0) {
registered++;
}
}
if (registered > 0) {
total += registered;
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: backfill registered %d blocks ch=%s file=%s",
CC_ID, registered, ch_id, final_name);
}
int registered = media_index_commit(cc->db, &result, cc->my_node_id, NULL, ch_id, path, media_base);
if (!registered) total += result.num_blocks;
else DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media backfill registration failed ch=%s file=%s", ch_id, path);
u_free(body);
media_index_result_free(&result);
@@ -1290,7 +1304,7 @@ static void bf_collect(struct chat_core_ctx* cc, const char* media_base, const c
char full_path[1536];
snprintf(full_path, sizeof(full_path), "%s/media/%s/%s", media_base, ch_id, final_name);
int64_t actual = (int64_t)ma_file_size(full_path);
int64_t actual = ma_file_size(full_path);
if (actual < 0) actual = 0;
if (l->count == l->cap) {
@@ -1331,10 +1345,8 @@ void chat_media_autodownload_backfill(struct UTUN_INSTANCE* inst) {
uint64_t now_ms = (uint64_t)ntp_time_get_seconds(inst) * 1000ULL;
uint64_t cutoff_ms = now_ms - (uint64_t)days * 86400ULL * 1000ULL;
char media_base[512];
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
char media_base[MEDIA_BASE_MAX];
if (media_index_get_base(cc->db, media_base)) return;
struct bf_list list = {0};
bf_collect(cc, media_base, NULL, &list);
@@ -1401,10 +1413,8 @@ void chat_media_autodownload_backfill_channel(struct UTUN_INSTANCE* inst, const
uint64_t now_ms = (uint64_t)ntp_time_get_seconds(inst) * 1000ULL;
uint64_t cutoff_ms = now_ms - (uint64_t)days * 86400ULL * 1000ULL;
char media_base[512];
const char* last_slash = strrchr(cc->db_path, '/');
if (last_slash) snprintf(media_base, sizeof(media_base), "%.*s", (int)(last_slash - cc->db_path), cc->db_path);
else snprintf(media_base, sizeof(media_base), "%s", cc->db_path);
char media_base[MEDIA_BASE_MAX];
if (media_index_get_base(cc->db, media_base)) return;
struct bf_list list = {0};
bf_collect(cc, media_base, ch_id, &list);
@@ -1444,12 +1454,6 @@ struct media_msg_file {
typedef void (*media_msg_file_cb)(const struct media_msg_file* f, void* arg);
static void chat_media_base(struct chat_core_ctx* cc, char* out, size_t out_sz) {
const char* ls = strrchr(cc->db_path, '/');
if (ls) snprintf(out, out_sz, "%.*s", (int)(ls - cc->db_path), cc->db_path);
else snprintf(out, out_sz, "%s", cc->db_path);
}
/* Итератор по всем медиасообщениям всех каналов (без фильтра по node_id/времени). */
static void chat_media_foreach_media_message(struct chat_core_ctx* cc, const char* media_base, media_msg_file_cb cb, void* arg) {
if (!cc->db || !cb) return;
@@ -1588,13 +1592,14 @@ static void cache_rel_collect_cb(const struct media_msg_file* f, void* arg) {
}
static void cache_file_collect_cb(const struct media_msg_file* f, void* arg) {
int fsz = ma_file_size(f->full_path);
if (fsz > 0) cache_file_add((struct cache_file_list*)arg, f->full_path, f->ts_ms, (int64_t)fsz);
int64_t fsz = ma_file_size(f->full_path);
if (fsz > 0) cache_file_add((struct cache_file_list*)arg, f->full_path, f->ts_ms, fsz);
}
/* Фактическая сумма размеров файлов, соответствующих сообщениям. */
static uint64_t chat_media_cache_scan_sum(struct chat_core_ctx* cc) {
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
char media_base[MEDIA_BASE_MAX];
if (media_index_get_base(cc->db, media_base)) return cc->media_cache_bytes;
struct cache_file_list files = {0};
chat_media_foreach_media_message(cc, media_base, cache_file_collect_cb, &files);
uint64_t sum = 0;
@@ -1637,12 +1642,13 @@ static void orphan_walk_cb(void* arg, const char* full_path, int64_t size) {
char* found = bsearch(&rel, ow->rels, (size_t)ow->rel_count, sizeof(char*), cache_str_cmp);
if (found) return;
if (remove(full_path) == 0) {
int removed = file_io_remove_orphan(full_path);
if (removed > 0) {
ow->removed++;
ow->removed_bytes += (uint64_t)(size > 0 ? size : 0);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache orphan removed: %s (%lld bytes)",
CC_ID, rel, (long long)size);
} else {
} else if (removed < 0) {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: cache orphan remove failed: %s", CC_ID, full_path);
}
}
@@ -1651,12 +1657,33 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->initialized || !cc->db || !inst) return;
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
char media_base[MEDIA_BASE_MAX];
if (media_index_get_base(cc->db, media_base)) return;
char media_dir[600]; snprintf(media_dir, sizeof(media_dir), "%s/media", media_base);
struct file_io_info directory;
int directory_status = file_io_inspect(media_dir, 0, &directory);
if (directory_status == 1) return; // До первого вложения каталога может не быть.
if (directory_status < 0) return;
struct cache_rel_list rels = {0};
chat_media_foreach_media_message(cc, media_base, cache_rel_collect_cb, &rels);
/* Ещё не опубликованные контейнеры и их временные файлы принадлежат crypto worker. */
sqlite3_stmt* preparations = NULL;
int prep_rc = sqlite3_prepare_v2(cc->db, "SELECT location FROM media_preparations UNION SELECT location FROM media_files",
-1, &preparations, NULL);
if (prep_rc == SQLITE_OK) {
while ((prep_rc = sqlite3_step(preparations)) == SQLITE_ROW) {
const char* location = (const char*)sqlite3_column_text(preparations, 0);
if (location) cache_rel_add(&rels, location);
}
}
int prep_finish = preparations ? sqlite3_finalize(preparations) : SQLITE_OK;
if (prep_rc != SQLITE_DONE || prep_finish != SQLITE_OK) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media orphan scan cannot protect indexed/preparing files rc=%d", prep_rc);
for (int i = 0; i < rels.count; i++) u_free(rels.items[i]);
u_free(rels.items); return;
}
/* Конечные контейнеры принадлежат attachment_keys; активные temp защищены file_io-реестром. */
if (cc->attachment_initialized) {
sqlite3_stmt* active = NULL;
if (sqlite3_prepare_v2(cc->db, "SELECT cipher_path FROM attachment_keys", -1, &active, NULL) == SQLITE_OK) {
@@ -1666,8 +1693,6 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
size_t prefix = strlen(media_base);
if (path && !strncmp(path, media_base, prefix) && path[prefix] == '/') {
cache_rel_add(&rels, path + prefix + 1);
char temporary[1040]; snprintf(temporary, sizeof(temporary), "%s.part", path + prefix + 1);
cache_rel_add(&rels, temporary);
}
}
if (rc != SQLITE_DONE) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted cache: active scan failed rc=%d", rc);
@@ -1680,10 +1705,12 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
ow.media_base = media_base; ow.mb_len = strlen(media_base);
ow.rels = rels.items; ow.rel_count = rels.count;
ow.removed = 0; ow.removed_bytes = 0;
ma_dir_walk(media_dir, orphan_walk_cb, &ow);
int walk_result = ma_dir_walk(media_dir, orphan_walk_cb, &ow);
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache orphan cleanup done removed=%d bytes=%llu",
CC_ID, ow.removed, (unsigned long long)ow.removed_bytes);
if (walk_result) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "%s: cache orphan cleanup incomplete removed=%d bytes=%llu",
CC_ID, ow.removed, (unsigned long long)ow.removed_bytes);
else DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "%s: cache orphan cleanup done removed=%d bytes=%llu",
CC_ID, ow.removed, (unsigned long long)ow.removed_bytes);
for (int i = 0; i < rels.count; i++) u_free(rels.items[i]);
u_free(rels.items);
@@ -1702,7 +1729,8 @@ void chat_media_cache_enforce(struct UTUN_INSTANCE* inst) {
return;
}
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
char media_base[MEDIA_BASE_MAX];
if (media_index_get_base(cc->db, media_base)) return;
struct cache_file_list files = {0};
chat_media_foreach_media_message(cc, media_base, cache_file_collect_cb, &files);
+1 -1
View File
@@ -36,7 +36,7 @@ struct chat_setting_def {
static const struct chat_setting_def g_setting_defs[] = {
{"stream_subblocks", CHAT_SETTING_INT, 8, 1, 255},
{"stream_rs_redundant", CHAT_SETTING_INT, 2, 0, 254},
{"stream_rs_redundant", CHAT_SETTING_INT, 0, 0, 254},
{"storage_autoload", CHAT_SETTING_BOOL, 1, 0, 1},
{"opus_codec_preset", CHAT_SETTING_INT, 1, 0, 4},
{"radio_opus_mode", CHAT_SETTING_INT, 0, 0, 1},
+101 -87
View File
@@ -6,6 +6,8 @@
#include "chat_core_priv.h"
#include "chat_event.h"
#include "chat_member.h"
#include "../member_files/protocol.h"
#include "../utun_instance.h"
#include "../ntp_time.h"
@@ -17,6 +19,7 @@
#include "../transport_layer/stcp_link.h"
#include "../transport_layer/pkt_normalizer.h"
#include "../routing_layer/topo_node.h"
#include "../routing_layer/topo_node_sqlite.h"
#include "../routing_layer/topo_group.h"
#include <string.h>
@@ -278,7 +281,11 @@ static void collect_conn_metrics(struct UTUN_INSTANCE* inst, uint64_t peer_node_
if (conn->input_queue) off += snprintf(buf + off, sizeof(buf) - off, "input_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->input_queue), queue_entry_count(conn->input_queue));
if (conn->input_send_q) off += snprintf(buf + off, sizeof(buf) - off, "send_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->input_send_q), queue_entry_count(conn->input_send_q));
if (conn->input_wait_ack) off += snprintf(buf + off, sizeof(buf) - off, "wait_ack: %uB/%dp\n", (unsigned)queue_total_bytes(conn->input_wait_ack), queue_entry_count(conn->input_wait_ack));
if (conn->ack_q) off += snprintf(buf + off, sizeof(buf) - off, "ack_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->ack_q), queue_entry_count(conn->ack_q));
unsigned ack_bytes = 0, ack_packets = 0;
for (struct ETCP_LINK* link = conn->links; link; link = link->next) {
ack_bytes += (unsigned)queue_total_bytes(link->ack_q); ack_packets += queue_entry_count(link->ack_q);
}
off += snprintf(buf + off, sizeof(buf) - off, "ack_q: %uB/%up ", ack_bytes, ack_packets);
if (conn->recv_q) off += snprintf(buf + off, sizeof(buf) - off, "recv_q: %uB/%dp ", (unsigned)queue_total_bytes(conn->recv_q), queue_entry_count(conn->recv_q));
if (conn->output_queue) off += snprintf(buf + off, sizeof(buf) - off, "out_q: %uB/%dp\n", (unsigned)queue_total_bytes(conn->output_queue), queue_entry_count(conn->output_queue));
@@ -357,20 +364,49 @@ void chat_core_collect_conn_metrics_trampoline(void* arg) {
u_free(arg);
}
/* ── Live-снапшот для панели деталей участника ──
* packet: [node_id:8][flags:1][own_tcp_active:2][link_count:1](link:63B)*[sock_count:1](sock:48B)*
* flags: bit0=conn_present bit1=conn_up(links_up) bit2=conn_initialized
* link: [is_tcp:1][link_state:1][link_status:1][tcp_ready:1][is_server:1]
* [local_family:1][local_addr:16][local_port:2]
* [remote_family:1][remote_addr:16][remote_port:2]
* [sock_name:20]
* sock: [sock_id:4][port:2][links_up:2][links_down:2][ifname:38]
* Собирается в worker-потоке — читает живые структуры UTUN_INSTANCE безопасно. */
/* Диагностика выбранного участника собирается целиком в uasync; wire описан в chat_member.h. */
#if CHAT_MEMBER_DETAIL_MAX_HOPS != MAX_HOPS
#error "member detail hop limit must match routing"
#endif
#define MEMBER_DETAIL_MAX_LINKS 16
#define MEMBER_DETAIL_MAX_SOCKS 16
#define MEMBER_DETAIL_LINK_SIZE 64
#define MEMBER_DETAIL_SOCK_SIZE 48
/* Проверяем размеры, адресата и все строки до изменения состояния GUI. */
int chat_core_parse_member_detail(const uint8_t* data, size_t len, struct chat_member_detail_view* out) {
if (!data || !out || len < CHAT_MEMBER_DETAIL_HEADER_SIZE) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: missing header bytes=%zu", len); return -1;
}
struct chat_member_detail_view v = {0};
v.request_id = mf_get64(data); v.group_id = mf_get64(data + 8); v.node_id = mf_get64(data + 16);
v.result = data[24]; v.transport_flags = data[25];
v.own_tcp_active = (uint16_t)data[26] | ((uint16_t)data[27] << 8);
v.hop_count = data[28]; v.link_count = data[29]; v.sock_count = data[30];
size_t expected = CHAT_MEMBER_DETAIL_HEADER_SIZE + (size_t)v.hop_count * 8 +
(size_t)v.link_count * CHAT_MEMBER_DETAIL_LINK_SIZE + (size_t)v.sock_count * CHAT_MEMBER_DETAIL_SOCK_SIZE;
if (!v.request_id || !v.group_id || !v.node_id || v.result > CHAT_MEMBER_DETAIL_ERROR || v.transport_flags > 7 ||
v.hop_count > CHAT_MEMBER_DETAIL_MAX_HOPS || v.link_count > CHAT_MEMBER_DETAIL_MAX_LINKS ||
v.sock_count > CHAT_MEMBER_DETAIL_MAX_SOCKS || expected != len ||
(v.result && (v.transport_flags || v.own_tcp_active || v.hop_count || v.link_count || v.sock_count))) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: invalid packet request=%llu group=%016llx node=%016llx bytes=%zu expected=%zu",
(unsigned long long)v.request_id, (unsigned long long)v.group_id, (unsigned long long)v.node_id, len, expected);
return -1;
}
v.hops = data + CHAT_MEMBER_DETAIL_HEADER_SIZE; v.links = v.hops + v.hop_count * 8;
v.socks = v.links + v.link_count * CHAT_MEMBER_DETAIL_LINK_SIZE;
for (int i = 0; i < v.hop_count; i++) if (!mf_get64(v.hops + i * 8)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: zero hop index=%d", i); return -1;
}
for (int i = 0; i < v.link_count; i++) {
const uint8_t* link = v.links + i * CHAT_MEMBER_DETAIL_LINK_SIZE;
if (!memchr(link + 43, 0, 20) || link[0] > 1 || link[3] > 1 || link[4] > 1 || link[63] > 1 ||
(link[5] && link[5] != 4 && link[5] != 6) || (link[24] && link[24] != 4 && link[24] != 6)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: invalid link index=%d", i); return -1;
}
}
for (int i = 0; i < v.sock_count; i++) if (!memchr(v.socks + i * CHAT_MEMBER_DETAIL_SOCK_SIZE + 10, 0, 38)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: unterminated interface index=%d", i); return -1;
}
*out = v;
return 0;
}
/* Повторяет Qt: name.lastIndexOf("_v") → name.mid(3, idx - 3) */
static void sock_ifname(const char* name, char* out, size_t out_sz) {
@@ -387,8 +423,35 @@ static void sock_ifname(const char* name, char* out, size_t out_sz) {
out[len] = '\0';
}
static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id) {
if (!CC(inst) || !CC(inst)->initialized || !inst) return;
static void collect_member_detail(const struct chat_member_detail_req* req) {
struct UTUN_INSTANCE* inst = req->inst;
uint64_t node_id = req->node_id;
uint8_t buf[CHAT_MEMBER_DETAIL_MAX_SIZE] = {0};
mf_put64(buf, req->request_id); mf_put64(buf + 8, req->group_id); mf_put64(buf + 16, node_id);
struct TOPO_GROUP* group = inst && inst->topo_groups ? topo_groups_find(inst->topo_groups, req->group_id) : NULL;
if (!req->request_id || !req->group_id || !node_id) buf[24] = CHAT_MEMBER_DETAIL_INVALID;
else if (!inst || !CC(inst) || !CC(inst)->initialized || !group || group->group_type != TOPO_GROUP_TYPE_CHAT)
buf[24] = CHAT_MEMBER_DETAIL_UNAVAILABLE;
if (!buf[24]) {
char table[80], sql[160]; peers_table_name(group->channel_id, table, sizeof(table));
snprintf(sql, sizeof(sql), "SELECT 1 FROM \"%s\" WHERE node_id=?", table);
sqlite3_stmt* st = NULL;
int rc = sqlite3_prepare_v2(CC(inst)->db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc == SQLITE_DONE) buf[24] = CHAT_MEMBER_DETAIL_NOT_MEMBER;
else if (rc != SQLITE_ROW) {
buf[24] = CHAT_MEMBER_DETAIL_ERROR;
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: query request=%llu rc=%d: %s",
(unsigned long long)req->request_id, rc, sqlite3_errmsg(CC(inst)->db));
}
sqlite3_finalize(st);
}
if (buf[24]) {
DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "member detail: rejected request=%llu group=%016llx node=%016llx result=%u",
(unsigned long long)req->request_id, (unsigned long long)req->group_id, (unsigned long long)node_id, buf[24]);
chat_event_post(inst, CHAT_EVT_MEMBER_DETAIL, buf, CHAT_MEMBER_DETAIL_HEADER_SIZE); return;
}
uint8_t flags = 0;
struct ETCP_CONN* conn = instance_find_conn(inst, node_id);
@@ -397,17 +460,6 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
if (conn->links_up) flags |= 2;
if (conn->initialized) flags |= 4;
}
/* bit 3 (0x08): узел присутствует в BGP/topo_group — live-онлайн (self всегда онлайн) */
{
int bgp_present = (node_id == inst->node_id) ? 1 : 0;
struct ll_queue* gl = inst->topo_groups ? inst->topo_groups->group_list : NULL;
for (struct ll_entry* ge = gl ? gl->head : NULL; !bgp_present && ge; ge = ge->next) {
struct TOPO_GROUP* group = (struct TOPO_GROUP*)ge->data;
if (group && topo_node_find_by_id(group, node_id)) bgp_present = 1;
}
if (bgp_present) flags |= 8;
}
uint16_t own_tcp_active = 0;
if (node_id == inst->node_id) {
if (inst->connections) {
@@ -428,23 +480,20 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
}
uint8_t link_count = 0;
for (struct ETCP_LINK* lk = conn ? conn->links : NULL; lk && link_count < MEMBER_DETAIL_MAX_LINKS; lk = lk->next)
for (struct ETCP_LINK* lk = conn ? conn->links : NULL; lk && link_count < CHAT_MEMBER_DETAIL_MAX_LINKS; lk = lk->next)
link_count++;
uint8_t sock_count = 0;
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s && sock_count < MEMBER_DETAIL_MAX_SOCKS; s = s->next)
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s && sock_count < CHAT_MEMBER_DETAIL_MAX_SOCKS; s = s->next)
sock_count++;
size_t buf_sz = 12 + (size_t)link_count * MEMBER_DETAIL_LINK_SIZE
+ 1 + (size_t)sock_count * MEMBER_DETAIL_SOCK_SIZE;
uint8_t* buf = (uint8_t*)u_malloc(buf_sz);
if (!buf) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "collect_member_detail: buf alloc failed"); return; }
uint8_t* p = buf;
memcpy(p, &node_id, 8); p += 8;
*p++ = flags;
memcpy(p, &own_tcp_active, 2); p += 2;
*p++ = link_count;
struct TOPO_GROUP_NODE* node = topo_node_find_by_id(group, node_id);
uint8_t hop_count = 0;
uint64_t* hops = topo_node_best_hop_list(node, &hop_count, NULL);
buf[25] = flags; buf[26] = (uint8_t)own_tcp_active; buf[27] = (uint8_t)(own_tcp_active >> 8);
buf[28] = hop_count; buf[29] = link_count; buf[30] = sock_count;
uint8_t* p = buf + CHAT_MEMBER_DETAIL_HEADER_SIZE;
for (int i = 0; i < hop_count; i++, p += 8) mf_put64(p, hops[i]);
for (struct ETCP_LINK* lk = conn ? conn->links : NULL; lk && link_count; lk = lk->next, link_count--) {
*p++ = lk->is_tcp ? 1 : 0;
@@ -469,7 +518,7 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
lf = 6; memcpy(la, &((const struct sockaddr_in6*)lsa)->sin6_addr, 16); lp = ntohs(((const struct sockaddr_in6*)lsa)->sin6_port);
}
}
*p++ = lf; memcpy(p, la, 16); p += 16; memcpy(p, &lp, 2); p += 2;
*p++ = lf; memcpy(p, la, 16); p += 16; *p++ = (uint8_t)lp; *p++ = (uint8_t)(lp >> 8);
}
{ uint8_t rf = 0; uint8_t ra[16] = {0}; uint16_t rp = 0;
@@ -479,7 +528,7 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
} else if (sa->ss_family == AF_INET6) {
rf = 6; memcpy(ra, &((const struct sockaddr_in6*)sa)->sin6_addr, 16); rp = ntohs(((const struct sockaddr_in6*)sa)->sin6_port);
}
*p++ = rf; memcpy(p, ra, 16); p += 16; memcpy(p, &rp, 2); p += 2;
*p++ = rf; memcpy(p, ra, 16); p += 16; *p++ = (uint8_t)rp; *p++ = (uint8_t)(rp >> 8);
}
{ char sn[20] = {0};
@@ -489,7 +538,6 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
*p++ = lk->reality_set ? 1 : 0;
}
*p++ = sock_count;
for (struct ETCP_SOCKET* s = inst->etcp_sockets; s && sock_count; s = s->next, sock_count--) {
uint32_t sid = s->sock_id;
uint16_t sp = 0;
@@ -514,56 +562,22 @@ static void collect_member_detail(struct UTUN_INSTANCE* inst, uint64_t node_id)
}
}
char ifname[38] = {0}; sock_ifname(s->name, ifname, sizeof(ifname));
memcpy(p, &sid, 4); p += 4;
memcpy(p, &sp, 2); p += 2;
memcpy(p, &lu, 2); p += 2;
memcpy(p, &ld, 2); p += 2;
for (int i = 0; i < 4; i++) *p++ = (uint8_t)(sid >> (8 * i));
*p++ = (uint8_t)sp; *p++ = (uint8_t)(sp >> 8);
*p++ = (uint8_t)lu; *p++ = (uint8_t)(lu >> 8);
*p++ = (uint8_t)ld; *p++ = (uint8_t)(ld >> 8);
memcpy(p, ifname, 38); p += 38;
}
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "member detail: request=%llu group=%016llx node=%016llx hops=%u links=%u socks=%u bytes=%zu",
(unsigned long long)req->request_id, (unsigned long long)req->group_id, (unsigned long long)node_id,
buf[28], buf[29], buf[30], (size_t)(p - buf));
chat_event_post(inst, CHAT_EVT_MEMBER_DETAIL, buf, (int)(p - buf));
u_free(buf);
/* summary text: direct RTT or BGP */
if (conn && conn->links_up) {
uint16_t rtt = conn->links ? conn->links->rtt_last : 0;
if (rtt > 0) {
char tbuf[32];
int off = snprintf(tbuf, sizeof(tbuf), "direct, rtt:%ums", rtt / 10);
chat_event_post(inst, CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off);
}
} else if (conn && !conn->links_up && node_id != inst->node_id) {
struct ll_queue* gl = inst->topo_groups ? inst->topo_groups->group_list : NULL;
for (struct ll_entry* ge = gl ? gl->head : NULL; ge; ge = ge->next) {
struct TOPO_GROUP* group = (struct TOPO_GROUP*)ge->data;
if (!group) continue;
struct TOPO_GROUP_NODE* nq = topo_node_find_by_id(group, node_id);
if (!nq) continue;
uint8_t hc = 0;
uint64_t* hops = topo_node_best_hop_list(nq, &hc, NULL);
if (hc > 0 && hops) {
char tbuf[512] = {0};
uint16_t cr = topo_get_chain_rtt(nq);
int off = snprintf(tbuf, sizeof(tbuf), "rtt:%ums hop:%u via bgp:", cr / 10, hc);
for (uint8_t hi = 0; hi < hc && off < (int)sizeof(tbuf) - 24; hi++) {
char hn[32] = {0};
get_node_name(inst, hops[hi], hn, sizeof(hn));
uint16_t hid = (uint16_t)(hops[hi] & 0xFFFF);
off += snprintf(tbuf + off, sizeof(tbuf) - (size_t)off,
" [%04X]%s", hid, hn[0] ? " " : "");
if (hn[0]) off += snprintf(tbuf + off, sizeof(tbuf) - (size_t)off, "%s", hn);
}
chat_event_post(inst, CHAT_EVT_BGP_INFO, (const uint8_t*)tbuf, off);
}
break;
}
}
}
void chat_core_collect_member_detail_trampoline(void* arg) {
struct chat_node_arg* a = (struct chat_node_arg*)arg;
if (!a) return;
uint64_t node_id = a->node_id;
collect_member_detail(a->inst, node_id);
u_free(arg);
struct chat_member_detail_req* req = arg;
if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "member detail: null request"); return; }
collect_member_detail(req);
u_free(req);
}
+494 -703
View File
File diff suppressed because it is too large. Load diff
+36 -27
View File
@@ -1,10 +1,12 @@
/*
* chat_sync — связь чат-сервиса с транспортом и группами.
* Отслеживает пиров, запускает member_sync, поддерживает кеш каналов и статус в GUI.
* UP/REINIT/пробуждение восстанавливают только общие каналы соответствующего пира.
* Отложенные запросы объединяются по (канал, пир); активными обменами и обновлениями владеет Merkle.
* Вход по ссылке держит отдельный NCD handle до результата join, а не только до UP.
* JOIN_INFO_REQ → JOIN_INFO_RESP → JOIN_REQUEST → JOIN_READY; правила — в chat_join.h.
* Жизненным циклом управляет chat_service_start/stop. Вызовы — в потоке uasync,
* кроме явно отмеченной обёртки, которая копирует аргументы и постит работу в этот поток.
* Жизненным циклом управляет chat_service_start/stop. Вызовы — в потоке uasync.
* Вход из GUI/JNI через отдельный chat_join_port, который копирует ссылку и удерживает очередь до close.
*/
#ifndef CHAT_SYNC_H
@@ -20,7 +22,7 @@ extern "C" {
struct UTUN_INSTANCE;
struct UASYNC;
/* etcp service IDs (direct P2P): ETCP_RT_ID_CHAT_SYNC / ETCP_RT_ID_MEMBER_SYNC
/* etcp service IDs (direct P2P): ETCP_ID_CHAT_SYNC / ETCP_ID_MEMBER_SYNC
* (etcp_bind, см. etcp_api.h) */
/* Message types */
@@ -32,12 +34,18 @@ struct UASYNC;
#define CS_MSG_SYNC_DONE 0x06
#define CS_MSG_ERROR 0x07
#define CS_MSG_CHANNEL_INVITE 0x0F /* push-invite: inviter → joiner */
#define CS_MSG_JOIN_INFO_REQ 0x10 /* joiner → connection: {join_key:8} */
/* В JOIN_INFO_REQ pass_len обязателен даже при пустом пароле (0); password передаётся без NUL. */
#define CS_MSG_JOIN_INFO_REQ 0x10 /* joiner → connection: {join_key:8, pass_len:1, password:pass_len} */
#define CS_MSG_JOIN_INFO_RESP 0x11 /* connection → joiner: channel info + connection's member record */
#define CS_MSG_JOIN_REQUEST 0x12 /* joiner → connection: {join_key:8, member...} */
#define CS_MSG_JOIN_READY 0x13 /* connection → joiner: {group_id:8} */
#define CS_MSG_SOUND_SETTING 0x14 /* peer → server: {sound_on:1} — per-chat play sound on message */
/* Управление JOIN (INFO_REQ/RESP, REQUEST, READY, ERROR): после type идёт attempt_id:8BE,
* затем payload, описанный выше. Ненулевой случайный ID новый для каждой локальной попытки;
* ответы проверяются по (peer, group, attempt_id, phase). CHANNEL_INVITE переносит предложение
* с join_key; получатель создаёт собственный attempt_id при принятии предложения. */
/* Protocol constants */
#define CS_SEND_DATA_MAX 32
#define CS_MAX_SPARSE 16
@@ -52,34 +60,34 @@ struct UASYNC;
/* Создать контекст, обработчики join и member_sync. 0/-1; повтор допустим. */
int chat_sync_init(struct UTUN_INSTANCE* inst);
/* Отменить ожидания join, синхронизацию и таймеры; не удаляет данные каналов. */
/* uasync: закрыть приём команд и отменить локальный JOIN до остановки workers. */
void chat_sync_quiesce(struct UTUN_INSTANCE* inst);
/* uasync: отменить только активный request; чужая попытка сохраняется. */
void chat_sync_cancel_join(struct UTUN_INSTANCE* inst, uint64_t request);
/* Отменить ожидания, синхронизацию и таймеры, освободить контекст; данные каналов сохраняются. */
void chat_sync_destroy(struct UTUN_INSTANCE* inst);
/* Сейчас пустая функция. Для подключения использовать chat_core_connect_node с каналом. */
void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id);
/* Подключиться к пиру по данным invite-ссылки (вызывается из uasync-потока).
join_key — 64-битный ключ входа из ссылки (регистрируется инвайтером на connection-узле).
password может быть NULL — тогда передаётся пустой пароль. */
void chat_sync_connect_from_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint64_t node_id,
const uint8_t* pubkey_bin,
const uint8_t* addrs_data, int addr_count,
int addrs_data_len,
const char* password, uint64_t join_key);
/* Локальная попытка JOIN: uasync. Отрицательный возврат — отказ запуска без события;
* 0 — запущено, CHAT_EVT_CONNECT_RESULT ровно один раз, включая stop/delete.
* request=0 — без адресата UI; ненулевые ID вызывающий не переиспользует до завершения.
* link заимствован до возврата; request — корреляция интерфейса, не сетевой attempt_id. */
#define CHAT_CONNECT_ERROR -1
#define CHAT_CONNECT_TIMEOUT 2
#define CHAT_CONNECT_INVALID -7
#define CHAT_CONNECT_BUSY -8
#define CHAT_CONNECT_CANCELLED -9
int chat_sync_join_link(struct UTUN_INSTANCE* inst, uint64_t request, const char* link, size_t len);
/* Единый формат результата: peer:8,result:4,channel:8,request:8, native; только uasync. */
void chat_sync_join_result(struct UTUN_INSTANCE* inst, uint64_t request, uint64_t channel, uint64_t peer, int result);
/* Пригласить target_node_id в канал ch_id с адресами из invite-ссылки (inviter → joiner).
Принимает pubkey и адреса целевого узла. Если соединения ещё нет — строит TOPO_NODE
с адресами, удерживает NCD для join, и по UP отправляет CS_MSG_CHANNEL_INVITE.
Если соединение уже есть — отправляет CHANNEL_INVITE сразу.
Копирует pubkey_bin и addrs_data (вызывающий может освободить после возврата).
Вызов из любого потока — внутри постит в uasync. */
void chat_sync_invite_to_channel_with_addrs(
struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id,
const uint8_t* pubkey_bin, const uint8_t* addrs_data, int addr_count,
int addrs_data_len);
/* uasync: пригласить адресата ссылки в свой channel; те же правила запуска/результата. */
int chat_sync_offer_link(struct UTUN_INSTANCE* inst, uint64_t request, uint64_t channel, const char* link, size_t len);
/* Пригласить target_node_id в канал ch_id (inviter → joiner).
Отправляет CS_MSG_CHANNEL_INVITE с полной информацией о канале и своих адресах */
Отправляет CS_MSG_CHANNEL_INVITE с описанием канала, подписями/профилем инвайтера и join_key. */
void chat_sync_invite_to_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
/* Принять входящее приглашение — продолжить протокол из chat_join.h. */
@@ -95,12 +103,13 @@ void chat_sync_deny_invite(struct UTUN_INSTANCE* inst, uint64_t channel_id, uint
это само по себе не даёт членства на других узлах. */
void chat_sync_join_channel(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t target_node_id);
/* Перезапустить групповые подключения для каналов без активных соединений.
/* Снять паузу и сбросить задержки повторных попыток во всех кешированных каналах с автоматическим планировщиком.
Планировщик пересматривает подключения; работающие соединения сохраняются.
Вызывается из auto_socket при появлении/изменении сетевой связности. */
void chat_sync_retry_channels_on_socket_change(struct UTUN_INSTANCE* inst);
/* Немедленно убрать канал из кеша chat_sync (локальное удаление канала),
* чтобы cs_on_conn_status не запускал member_sync для удалённого ns до следующего refresh. */
/* Отменить отложенные запросы канала и немедленно убрать его из кеша (локальное удаление).
* Активные Merkle-сессии отменяет вызывающий код через member_sync_cancel_channel(). */
void chat_sync_on_channel_deleted(struct UTUN_INSTANCE* inst, const char* ch_id);
/* Разослать всем подключённым пирам канала смену per-chat настройки «play sound on message». */
+4 -4
View File
@@ -769,14 +769,14 @@ static int db_record_insert_cascade(struct DB_SYNC_INSTANCE* si,
// Send
// ============================================================
// Отправляет сообщение узлу через ETCP с заголовком: [ETCP_RT_ID_DB_SYNC:1][group_id_be:8][payload]. Возвращает код etcp_send
// Отправляет сообщение узлу через ETCP с заголовком: [ETCP_ID_DB_SYNC:1][group_id_be:8][payload]. Возвращает код etcp_send
static int db_sync_send_gid(struct DB_SYNC* db, uint64_t node_id, uint64_t group_id, const uint8_t* payload, size_t plen)
{
struct ll_entry* entry = queue_entry_new(0);
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "queue_entry_new"); return -1; }
uint8_t* buf = u_malloc(plen + 9);
if (!buf) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "db_sync_send_gid: u_malloc(%zu) failed for dst=%016llx", plen + 9, (unsigned long long)node_id); queue_entry_free(entry); return -1; }
buf[0] = ETCP_RT_ID_DB_SYNC;
buf[0] = ETCP_ID_DB_SYNC;
uint64_t gb = htobe64(group_id);
memcpy(buf + 1, &gb, 8);
memcpy(buf + 9, payload, plen);
@@ -1750,7 +1750,7 @@ int db_sync_enable(struct UTUN_INSTANCE* inst) {
}
db->enabled = 1;
etcp_bind(inst, ETCP_RT_ID_DB_SYNC, db_sync_recv_cb);
etcp_bind(inst, ETCP_ID_DB_SYNC, db_sync_recv_cb);
DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "db_sync: initialized (enabled=%d)", db->enabled);
return 0;
@@ -1764,7 +1764,7 @@ void db_sync_destroy(struct UTUN_INSTANCE* inst)
struct DB_SYNC* db = inst->db_sync;
inst->db_sync = NULL;
etcp_unbind(inst, ETCP_RT_ID_DB_SYNC);
etcp_unbind(inst, ETCP_ID_DB_SYNC);
if (db->peer_check_timer) { uasync_cancel_timeout(inst->ua, db->peer_check_timer); db->peer_check_timer = NULL; }
#ifdef UTUN_HAVE_STANDBY
+1 -1
View File
@@ -18,7 +18,7 @@ extern "C" {
struct UTUN_INSTANCE;
struct DB_SYNC_INSTANCE;
// etcp service ID: ETCP_RT_ID_DB_SYNC (etcp_bind, см. etcp_api.h)
// etcp service ID: ETCP_ID_DB_SYNC (etcp_bind, см. etcp_api.h)
// Message types
#define DB_MSG_INIT_SYNC 0x01
+2 -2
View File
@@ -99,8 +99,8 @@ chain_hash[0] = SHA256(0x00..00 [32] || id_0 || ts_0 || author_0 || sig_0)
### 1.4 Интеграция с ETCP
- **Service ID:** `ETCP_RT_ID_DB_SYNC = 0x20`
- **Биндинг:** `etcp_bind(inst, 0x20, db_sync_recv_cb)` — приём сообщений
- **Service ID:** `ETCP_ID_DB_SYNC = 0x20`
- **Биндинг:** `etcp_bind(inst, ETCP_ID_DB_SYNC, db_sync_recv_cb)` — приём сообщений
- **Connection callbacks:** `etcp_add_conn_status_cbk(inst, db_sync_on_conn_status, NULL)` — отслеживание UP/DOWN соединений
- **Отправка:** через `etcp_send()` в прямое P2P-соединение
- **Маршрутизация:** каждый пакет начинается с `[svc:1][hash_be:8]`, hash идентифицирует инстанс на приёмной стороне
Loaded 100 of 608 files, more files were not shown because too many files have changed in this diff. Show more