Author SHA1 Message Date
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 c9a3a1334b Restore shared media playback to speaker after headset loss 2026-10-22 15:24:13 +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 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
276 changed files with 14667 additions and 7470 deletions

No files matched your search

+173 -33
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,55 @@ 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 +419,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 +439,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 +448,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 +477,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 +509,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 +558,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 +584,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 +770,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 +797,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 +812,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 +848,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 +868,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 +889,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 +993,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 +1042,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. Проброс портов
+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. Порядок задач
+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 каждой операции.
+1 -1
View File
@@ -76,7 +76,7 @@ Nonce метаданных использует `part=UINT64_MAX`; файл —
## Адресный протокол
Router-сервис `ETCP_RT_ID_ATTACHMENT_KEYS=0x40`. Каждый пакет обязан иметь
Router-сервис `ETCP_RT_ID_ATTACHMENT_KEYS=0x42`. Каждый пакет обязан иметь
`ROUTER_FLAG_SIGNED | ROUTER_FLAG_ENCRYPTED`; промежуточные узлы ключа не видят.
Протокол не создаёт личную беседу и не публикует служебные сообщения в канал.
+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 устанавливает только одно подключение между двумя узлами. Это подключение может добавлять-убирать каналы связи (динамически) без разрыва соединения.
Также каналы связи мониторятся и трафик распределяется между доступными каналами. Если канал определяется как недоступен - он исключается из обмена.
+67 -38
View File
@@ -65,8 +65,12 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
шифруется строго ВСЁ включая ВСЕ заголовки (кроме обфусцированного 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 ограничено размером пакета)
@@ -114,56 +118,80 @@ Burst-измерения bandwidth (0x07, 0x08) зарезервированы,
- клиент отправляет init запрос и выставляет таймаут
- по таймауту повторяет init запросы с экспоненциально растущим интервалом (начальный 500ms, макс 50s, +25% каждые 10 попыток), ведется счетчик запросов init.
- когда клиент получает 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 (0-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_UNKNOWN=0/PUBLIC=1/NAT=2/PRIVATE=3/LOCAL=4)
- 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 и собственным публичным ключом). Передаётся в конце пакета
в 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 идентификатор канала сервера
- 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,9 +211,9 @@ 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 - проверить ключи на совпадение.
При получении INIT получатель создаёт или переиспользует ETCP_CONN и подходящий линк;
для существующего соединения проверяет совпадение ключа пира. Эпоха из INIT учитывается
через etcp_conn_apply_peer_reset_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
@@ -217,14 +245,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 +265,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.
+11 -6
View File
@@ -136,6 +136,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,8 +165,9 @@ 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.
Статусы (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. При заполнении квитанций новая
@@ -355,11 +357,14 @@ tests/test_member_files_network: настоящие A/B/M, подписанна
Также проверяются чужая запись, виртуальный корень и подписанное удаление читателя.
QUOTA проверяется на настоящем подписанном/зашифрованном канале; удалённый читатель не получает счётчики.
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 логирует переходы
+5 -2
View File
@@ -14,8 +14,11 @@ GUI добавляет заголовок «Приглашение в групп
Доставку, повтор и шифрование обеспечивает существующий механизм DM.
После успешной отправки открывается личная беседа с получателем.
Срок действия ключа — **10 минут с момента создания**, а не получения PM.
Он указан в диалогах отправки и подключения. Поздняя offline-доставка
Срок действия ключа — **10 минут с момента последней успешной регистрации на каждом узле**.
Повторная регистрация того же ключа для того же канала тем же инвайтером продлевает срок.
Connection-узел сохраняет ключ при обработке регистрации, инвайтер — после её подтверждения;
при совпадении этих ролей запись одна. Для JOIN ключ должен действовать у обоих.
Получение PM срок не продлевает. Длительность указана в диалогах отправки и подключения. Поздняя offline-доставка
может принести уже просроченную ссылку; в этом случае нужно новое приглашение.
Ссылка не содержит время истечения, поэтому клиент показывает длительность,
без обратного отсчёта и вычисления оставшегося времени.
+52 -8
View File
@@ -117,6 +117,26 @@ Worker резервирует completion до запуска и передаёт
транскодирование или распознавание речи может задержать stop; принудительного
прерывания этих библиотек нет.
Владелец принимает не более 16 задач, включая уже завершённые workers до доставки
completion. Отказ запуска/неверные аргументы дают синхронный done с -1, остановка
владельца — с `MEDIA_ASYNC_CANCELLED`; при NULL done только логируется ошибка.
Примитивы `media_async.h` синхронные: отмена задачи не откатывает выполненные ими
действия. `ma_copy_file()` публикует копию внутри worker после проверенных записи и
закрытия temp; вызывающий отвечает за очистку своего результата при отмене.
Подробные файловые контракты, 64-битные размеры и ошибки RNG описаны в `media_async.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 +156,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 +181,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 +225,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
+87 -42
View File
@@ -8,7 +8,7 @@
Принятые решения:
- Транспорт — существующий ETCP с быстрыми переповторами, мультилинком, шифрованием и управлением нагрузкой.
- Параметры FEC задаёт источник; 6+2 — значение по умолчанию, а не ограничение протокола.
- Параметры FEC задаёт источник; 8+0 — значение по умолчанию, а не ограничение протокола.
- Источник подписывает всю пачку один раз до RS; подпись восстанавливается вместе с содержимым и проверяется после decode.
- В источник добавляется циклический timestamp uint16_t в миллисекундах; он сохраняется при всей ретрансляции.
Задержка оценивается относительно предыдущих данных по дельтам timestamp и времени прихода, включая полную остановку.
@@ -68,7 +68,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 +78,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 +87,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.
@@ -743,7 +743,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 +811,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.
@@ -828,17 +830,24 @@ Android:
неполную пачку объединяет с аудио/видео до 20ms; продолжение работы через timer 5ms.
При 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 разрешено после первого декодированного кадра.
@@ -851,13 +860,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 +935,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,19 +1039,36 @@ 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.
@@ -1104,7 +1134,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 +1185,30 @@ 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 при наличии данных; фиксированного ожидания перед стартом нет.
Это запас отдельного 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. Суперузлы: помощь в раздаче по запросу
+173 -997
View File
File diff suppressed because it is too large. Load diff
+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 = нет)
+180
View File
@@ -0,0 +1,180 @@
#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
/* Общие пути 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;
}
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));
}
int file_io_temp_open(struct file_io_temp *output, const char *destination) {
if (!output || output->file || output->temporary[0] || !destination || !*destination
|| strlen(destination) + 38 >= FILE_IO_PATH_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary invalid owner/path"); return -1;
}
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]);
snprintf(output->temporary, sizeof(output->temporary), "%s.tmp.%s", destination, suffix);
#ifdef _WIN32
wchar_t wide[FILE_IO_PATH_MAX];
if (file_io_wide(output->temporary, wide)) { output->temporary[0] = 0; return -1; }
int fd = _wopen(wide, _O_CREAT | _O_EXCL | _O_RDWR | _O_BINARY, _S_IREAD | _S_IWRITE);
#else
int fd = open(output->temporary, O_CREAT | O_EXCL | O_RDWR, 0600);
#endif
if (fd < 0) {
output->temporary[0] = 0;
if (errno == EEXIST) continue;
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file temporary open failed destination=%s errno=%d", destination, errno); return -1;
}
#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", output->temporary);
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 (fclose(output->file) && !error) error = errno ? errno : EIO;
output->file = NULL;
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file flush/close failed path=%s errno=%d", output->temporary, error); return -1; }
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 error = file_io_wide(output->temporary, wide) ? -1 : _wremove(wide);
#else
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);
output->temporary[0] = 0;
}
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;
}
#ifdef _WIN32
wchar_t from[FILE_IO_PATH_MAX], to[FILE_IO_PATH_MAX];
if (file_io_wide(output->temporary, from) || file_io_wide(destination, to)) return -1;
if (!MoveFileExW(from, to, MOVEFILE_REPLACE_EXISTING)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s winerr=%lu", destination, GetLastError()); return -1;
}
#else
if (rename(output->temporary, destination)) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "file publish failed destination=%s errno=%d", destination, errno); return -1;
}
#endif
output->temporary[0] = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_SYS, "file published destination=%s", destination);
return 0;
}
+42
View File
@@ -0,0 +1,42 @@
/* file_io — синхронные файловые операции для workers: UTF-8 пути, идентичность и публикация через соседний temp.
* inspect: 0 найден, 1 отсутствует, -1 ошибка; остальные операции 0/-1, paths также 1 для одного файла.
* Temp обнулить → open → писать в file → finish → publish; discard обязателен также при ошибке.
* Исходник и каталоги должны оставаться стабильными на время операции. Это не защита от подмены родительских каталогов.
* Publish заменяет имя атомарно, но не обещает сохранность при аварии питания. POSIX temp имеет права 0600. */
#ifndef FILE_IO_H
#define FILE_IO_H
#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#define FILE_IO_PATH_MAX 4096
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
};
// 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; назначение отсутствует или regular без конечной ссылки. 1 — один файл, 0 — разные, -1 — ошибка.
int file_io_paths(const char *source, const char *destination);
// Создать эксклюзивный temp рядом с destination; структура заранее обнулена, даже после ошибки вызвать discard.
int file_io_temp_open(struct file_io_temp *output, const char *destination);
// Проверить flush/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; одинаковые файлы отклоняются.
int file_io_temp_publish(struct file_io_temp *output, const char *source, const char *destination);
#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 ошибка/нет ключа.
*/
+91 -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,49 @@ 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);
/* ==================== Конфигурация ==================== */
// ==================== Конфигурация ====================
/**
* @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 +251,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 +279,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 +318,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 +394,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 +431,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 +468,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);
+16 -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,27 @@ 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
// Один 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`
+68 -16
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
@@ -119,10 +115,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 +154,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);
@@ -325,7 +332,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 {
@@ -394,7 +408,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;
@@ -467,11 +486,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;
@@ -514,11 +535,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;
}
+46 -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,14 @@
//
// Ошибка: любая ошибка сокета → 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.
#ifndef TCP_IO_H
#define TCP_IO_H
@@ -40,15 +49,16 @@ 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; // Чтение отключено backpressure или pause_read
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 +81,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 +108,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)` для упрощения арифметики кучи.
+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 */
+1 -1
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 \
+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
+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) {
+43 -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,51 @@
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. */
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 +63,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
+79 -47
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,20 @@ 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 остаётся у вызывающего. */
// Создать канал по готовым ключам и метаданным 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;
@@ -132,29 +149,34 @@ struct chat_delete_channel_req {
* (соединения/синк), db_sync инстанса, таблиц msg_ и peers_, медиафайлов и
* merkle-хешей. Работает в uasync-потоке. Другие узлы канал не теряют. */
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 +185,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 +207,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 +220,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 +238,26 @@ 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) */
// Сбор live-снапшота для панели деталей участника (arg = u_malloc struct chat_node_arg)
void chat_core_collect_member_detail_trampoline(void* arg);
/* ── 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 +269,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 +294,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 +329,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 {
+16 -4
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,7 +22,7 @@ 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_NEW_PEER 3 /* [node_id:8] */
@@ -90,12 +92,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
+1 -1
View File
@@ -63,7 +63,7 @@
* - Recovery восстанавливает сессии существующих участников и не использует
* invite-ключ как замену локальной таблицы участников.
*
* J <-> C: ETCP_RT_ID_CHAT_SYNC, заголовок [service:1][group_id:8][type:1], payload
* J <-> C: ETCP_ID_CHAT_SYNC, заголовок [service:1][group_id:8][type:1], payload
* описан в chat_sync.h. A <-> C: ETCP_RT_ID_JOIN через etcp_router с подписью
* и шифрованием; subcommands и payload описаны ниже.
*
+21 -9
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>
@@ -358,7 +359,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);
@@ -1153,8 +1158,8 @@ 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;
}
@@ -1290,7 +1295,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) {
@@ -1588,8 +1593,8 @@ 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);
}
/* Фактическая сумма размеров файлов, соответствующих сообщениям. */
@@ -1654,6 +1659,11 @@ static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst) {
char media_base[512]; chat_media_base(cc, media_base, sizeof(media_base));
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. */
@@ -1680,10 +1690,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);
+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},
+3 -3
View File
@@ -188,7 +188,7 @@ static int cs_send(struct chat_sync* cs, const char* ch_id, uint64_t dst,
uint8_t* buf = u_malloc(total);
if (!buf) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: cs_send alloc(%zu) failed", CS_ID, total); return -1; }
uint8_t* p = buf;
*p++ = ETCP_RT_ID_CHAT_SYNC;
*p++ = ETCP_ID_CHAT_SYNC;
cs_write_group_id(p, ch_id); p += 8;
memcpy(p, payload, len);
struct ll_entry* entry = queue_entry_new(0);
@@ -771,7 +771,7 @@ int chat_sync_init(struct UTUN_INSTANCE* inst) {
member_sync_destroy(inst); inst->chat_sync = NULL; u_free(cs); return -1;
}
etcp_bind(inst, ETCP_RT_ID_CHAT_SYNC, chat_sync_recv_cb);
etcp_bind(inst, ETCP_ID_CHAT_SYNC, chat_sync_recv_cb);
etcp_add_conn_status_cbk(inst, cs_on_conn_status, cs);
cs_refresh_channels(cs);
@@ -812,7 +812,7 @@ void chat_sync_destroy(struct UTUN_INSTANCE* inst) {
chat_join_destroy(inst);
cs->initialized = 0; inst->chat_sync = NULL;
etcp_unbind(inst, ETCP_RT_ID_CHAT_SYNC);
etcp_unbind(inst, ETCP_ID_CHAT_SYNC);
etcp_remove_conn_status_cbk(inst, cs_on_conn_status, cs);
if (cs->refresh_timer) { uasync_cancel_timeout(inst->ua, cs->refresh_timer); cs->refresh_timer = NULL; }
#ifdef UTUN_HAVE_STANDBY
+6 -4
View File
@@ -20,7 +20,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,7 +32,8 @@ 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} */
@@ -79,7 +80,7 @@ void chat_sync_invite_to_channel_with_addrs(
int addrs_data_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,7 +96,8 @@ 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);
+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 идентифицирует инстанс на приёмной стороне
+33 -7
View File
@@ -1,32 +1,58 @@
/* Подписанная аватарка канала: SQLite, максимум 16 КиБ, router-блоки до 4 КиБ.
* Все API/callbacks — uasync; данные публикуются только после полной проверки подписи. */
/* group_avatar — общая подписанная аватарка CHAT-канала, хранение в SQLite и обмен между участниками.
* После init: get читает текущую версию, set публикует картинку владельца, apply принимает проверяемую запись пира.
* Картинка до 16 КиБ передаётся router-блоками до 4 КиБ; данные публикуются после проверки подписи канала.
* Все вызовы/callbacks — uasync. Изменение картинки приходит событием CHAT_EVT_CHANNEL_UPDATED. */
#ifndef GROUP_AVATAR_H
#define GROUP_AVATAR_H
#include <stdint.h>
#include <stddef.h>
struct UTUN_INSTANCE;
struct sqlite3;
// ── Схема хранения ──
// Создать поля хранения в общей SQLite; 0/-1, БД остаётся у вызывающего.
int group_avatar_schema_init(struct sqlite3* db);
#define GROUP_AVATAR_MAX 16384
#define GROUP_AVATAR_BLOCK 4096
struct group_avatar_record {
uint64_t version;
uint16_t size;
uint8_t hash[32], signature[64], data[GROUP_AVATAR_MAX];
uint64_t version; // 0 — картинка ещё не публиковалась; иначе версия подписанной записи
uint16_t size; // байты картинки; size=0 при ненулевой версии — подписанное удаление
uint8_t hash[32], signature[64], data[GROUP_AVATAR_MAX]; // SHA256, подпись канала Ed25519 и готовая картинка
};
// ── Жизненный цикл модуля ──
// Создать контекст и подключить сетевой обмен; 0/-1. Повтор допустим.
int group_avatar_init(struct UTUN_INSTANCE* inst);
// Остановить обмен и освободить контекст; сохранённая картинка остаётся в БД.
void group_avatar_destroy(struct UTUN_INSTANCE* inst);
// Отменить сетевые задачи канала перед удалением группы.
void group_avatar_cancel_group(struct UTUN_INSTANCE* inst, uint64_t group);
// ── Чтение и изменение аватарки ──
// Скопировать запись и проверить её подпись; 0 — прочитано (version=0, если не публиковалась), -1 — ошибка/нет канала.
int group_avatar_get(struct UTUN_INSTANCE* inst, uint64_t group, struct group_avatar_record* record);
/* 1=изменено, 0=прежняя/старая запись; <0 — ошибка. */
// Проверить подпись и сохранить более новую запись; 1=изменено, 0=прежняя/старая, -1=ошибка. Структура остаётся у вызывающего.
int group_avatar_apply(struct UTUN_INSTANCE* inst, uint64_t group, const struct group_avatar_record* record);
/* Публикация своей картинки требует приватный Ed25519 ключ канала. size=0 — подписанное удаление. */
/* Подписать и сохранить картинку: нужен приватный Ed25519 ключ канала. size=0 — удаление; data копируется.
* 0 — сохранено или картинка прежняя, -1 — ошибка/нет прав. Доставка пирам выполняется отдельно. */
int group_avatar_set(struct UTUN_INSTANCE* inst, uint64_t group, const uint8_t* data, size_t size);
// ── Команда интерфейса ──
struct group_avatar_gui_request {
struct UTUN_INSTANCE* inst;
uint64_t id, group;
uint16_t size;
uint8_t data[GROUP_AVATAR_MAX];
};
// arg — u_malloc/u_calloc group_avatar_gui_request, освобождается здесь; результат — CHAT_EVT_GROUP_AVATAR_RESULT.
void group_avatar_gui_set(void* arg);
#endif
+1 -1
View File
@@ -1117,7 +1117,7 @@ void member_sync_subscribe_group(struct UTUN_INSTANCE* inst, struct TOPO_GROUP*
int member_sync_init(struct UTUN_INSTANCE* inst) {
if (!inst) { DEBUG_ERROR(DEBUG_CATEGORY_MEMBER_SYNC, "%s: init — inst is NULL", MS_ID); return -1; }
int rc = merkle_sync_init(inst, ETCP_RT_ID_MEMBER_SYNC, &g_member_ops, inst);
int rc = merkle_sync_init(inst, ETCP_ID_MEMBER_SYNC, &g_member_ops, inst);
if (rc != 0) return rc;
/* очистка битых записей во всех каналах перед построением дерева */
member_sync_verify_and_purge_all(inst);
+5 -2
View File
@@ -88,7 +88,7 @@ void member_sync_remove_apply_cbk(struct UTUN_INSTANCE* inst, member_apply_cbk_f
/*
* Инициализировать модуль: создаёт merkle_sync с коллбэками для мемберов
* (ETCP_RT_ID_MEMBER_SYNC), проверяет локальные записи, восстанавливает деревья
* (ETCP_ID_MEMBER_SYNC), проверяет локальные записи, восстанавливает деревья
* и подписывается на события CHAT-групп. 0/-1.
*/
int member_sync_init(struct UTUN_INSTANCE* inst);
@@ -127,7 +127,8 @@ void member_sync_cancel_channel(struct UTUN_INSTANCE* inst, const char* ch_id);
* x25519 — X25519 публичный ключ (32 байта)
* ed25519 — Ed25519 публичный ключ (32 байта)
* join_sig — Ed25519 подпись join-сообщения (64 байта)
* сообщение: ch_x25519(32) || ch_ed25519(32) || node_id(8LE) || x25519(32) || join_ts(8LE)
* сообщение: ch_x25519(32) || ch_ed25519(32) || node_id(8) || x25519(32) || join_ts(8)
* node_id и join_ts копируются в порядке байтов хоста (см. member_sync_build_join_msg).
* join_ts — NTP-синхронизированное время подписи (unix epoch seconds)
* update_sig — подпись блока A (64 байта), update_ts — версия блока A
* userinfo — метаданные узла (имя) в канале
@@ -151,6 +152,7 @@ int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id,
/*
* Строит сообщение для update_sig блока A (мембер):
* join_sig(64) || update_ts(8) || userinfo(с NUL)
* update_ts копируется в порядке байтов хоста.
* Используется и для подписи, и для верификации — байты обязаны совпадать.
* Возвращает длину сообщения или -1.
*/
@@ -161,6 +163,7 @@ int member_sync_build_update_msg(const uint8_t* join_sig, uint64_t update_ts,
/*
* Строит сообщение для join_sig (каноническая раскладка 112 байт):
* ch_x25519(32) || ch_ed25519(32) || node_id(8) || node_x25519(32) || join_ts(8)
* node_id и join_ts копируются в порядке байтов хоста, без преобразования endian.
* Единственный источник истины для подписи и верификации join_sig во всех модулях.
* Возвращает длину сообщения (112) или -1.
*/
+166
View File
@@ -0,0 +1,166 @@
/* Внутренний разбор INI для config_parser и config_updater: одинаковые пробелы,
* секции, key=value и строгий HEX. Буфер принадлежит вызывающему; trim/key_value
* меняют его на месте. read выделяет текст через u_malloc; освобождение — u_free.
* Строка содержит не более 1022 байтов до LF, встроенный NUL запрещён. */
#ifndef CONFIG_INI_H
#define CONFIG_INI_H
#include "../lib/platform_compat.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../lib/strbuf.h"
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#define CONFIG_INI_LINE_SIZE 1024
/* Удалить пробелы с обеих сторон, сохранив исходный адрес строки. */
static inline char *config_ini_trim(char *text) {
if (!text) return NULL;
char *start = text;
while (isspace((unsigned char)*start)) start++;
size_t length = strlen(start);
while (length && isspace((unsigned char)start[length - 1])) length--;
memmove(text, start, length);
text[length] = '\0';
return text;
}
/* Полный заголовок [name], без префиксного сопоставления; 0/-1. */
static inline int config_ini_section(const char *line, char *name, size_t capacity) {
size_t length = strlen(line);
if (length < 2 || line[0] != '[' || line[length - 1] != ']' || length - 2 >= capacity) return -1;
memcpy(name, line + 1, length - 2);
name[length - 2] = '\0';
config_ini_trim(name);
return 0;
}
/* Разобрать key=value, обрезать пробелы и комментарий # в значении; 0/-1. */
static inline int config_ini_key_value(const char *line, char *key, size_t key_cap, char *value, size_t value_cap) {
const char *equal = strchr(line, '=');
if (!equal || (size_t)(equal - line) >= key_cap || strlen(equal + 1) >= value_cap) return -1;
memcpy(key, line, (size_t)(equal - line));
key[equal - line] = '\0';
config_ini_trim(key);
strcpy(value, equal + 1);
char *comment = strchr(value, '#');
if (comment) *comment = '\0';
config_ini_trim(value);
return key[0] ? 0 : -1;
}
/* Проверить физические строки до разбора, чтобы не принять хвост длинной строки за новую опцию. */
static inline int config_ini_validate(const char *text, size_t length, const char *filename) {
size_t column = 0, line = 1;
if (!text && length) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: NULL configuration buffer", filename); errno = EINVAL; return -1; }
for (size_t i = 0; i < length; i++) {
if (!text[i]) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%zu: embedded NUL at byte %zu", filename, line, i);
errno = EINVAL;
return -1;
}
if (text[i] != '\n' && ++column >= CONFIG_INI_LINE_SIZE - 1) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%zu: INI line exceeds 1022 bytes", filename, line);
errno = EOVERFLOW;
return -1;
}
if (text[i] == '\n') { column = 0; line++; }
}
return 0;
}
/* Прочитать весь поток, проверяя ошибки чтения/роста. Поток закрывает вызывающий; 0/-1. */
static inline int config_ini_read(FILE *file, const char *filename, char **text, size_t *length) {
struct strbuf output = strbuf_new();
char chunk[4096];
size_t count;
*text = NULL;
*length = 0;
while ((count = fread(chunk, 1, sizeof(chunk), file)) != 0) {
const char *nul = memchr(chunk, '\0', count);
if (nul) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: embedded NUL at byte %zu", filename, output.len + (size_t)(nul - chunk));
errno = EINVAL;
goto failed;
}
if (output.len > UINT32_MAX / 2 - count - 1) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: INI text exceeds allocator size limit", filename);
errno = EFBIG;
goto failed;
}
if (strbuf_addf(&output, "%.*s", (int)count, chunk) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: allocation failed while reading INI at byte %zu", filename, output.len);
errno = ENOMEM;
goto failed;
}
}
if (ferror(file)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: read failed at byte %zu: %s", filename, output.len, strerror(errno));
goto failed;
}
if (config_ini_validate(strbuf_str(&output), output.len, filename)) goto failed;
if (!output.buf && strbuf_addf(&output, "%s", "") < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot allocate empty configuration buffer", filename);
errno = ENOMEM;
goto failed;
}
*length = output.len;
*text = strbuf_detach(&output);
return 0;
failed:
strbuf_free(&output);
return -1;
}
static inline int config_ini_nibble(unsigned char ch) {
if (ch >= '0' && ch <= '9') return ch - '0';
if (ch >= 'a' && ch <= 'f') return ch - 'a' + 10;
if (ch >= 'A' && ch <= 'F') return ch - 'A' + 10;
return -1;
}
/* Проверить весь HEX-фрагмент до записи. binary=NULL — только проверка; 0/-1. */
static inline int config_ini_hex_span(const char *text, size_t length, uint8_t *binary, size_t size) {
if (!text || size > SIZE_MAX / 2 || length != size * 2) return -1;
for (size_t i = 0; i < length; i++) if (config_ini_nibble((unsigned char)text[i]) < 0) return -1;
if (binary) {
for (size_t i = 0; i < size; i++) {
binary[i] = (uint8_t)((config_ini_nibble((unsigned char)text[i * 2]) << 4) |
config_ini_nibble((unsigned char)text[i * 2 + 1]));
}
}
return 0;
}
static inline int config_ini_hex(const char *text, uint8_t *binary, size_t size) {
return text ? config_ini_hex_span(text, strlen(text), binary, size) : -1;
}
/* Список HEX через запятую: пустой допустим; непустые элементы имеют item_size байтов.
* Только полностью корректный список копируется в binary; при ошибке *count=0. */
static inline int config_ini_hex_list(const char *text, uint8_t *binary, size_t item_size, int max_count, int *count) {
*count = 0;
for (int pass = 0; pass < 2; pass++) {
const char *cursor = text;
int items = 0;
if (!*cursor) return 0;
do {
const char *end = strchr(cursor, ',');
const char *next = end ? end + 1 : NULL;
if (!end) end = cursor + strlen(cursor);
while (cursor < end && isspace((unsigned char)*cursor)) cursor++;
while (end > cursor && isspace((unsigned char)end[-1])) end--;
if (items >= max_count || config_ini_hex_span(cursor, (size_t)(end - cursor),
pass && binary ? binary + (size_t)items * item_size : NULL, item_size)) return -1;
items++;
cursor = next;
} while (cursor);
if (pass) *count = items;
}
return 0;
}
#endif
+141 -433
View File
@@ -1,6 +1,7 @@
// config_parser.c - Configuration parser for utun application (updated for new structures)
#define _POSIX_C_SOURCE 200809L
#include "config_parser.h"
#include "config_ini.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -25,16 +26,6 @@
static int assign_string(char *dest, size_t dest_size, const char *src);
static int parse_firewall_rule(const char *rule_str, struct global_config *global);
static int hex_to_binary(const char *hex_str, uint8_t *binary, size_t binary_len) {
if (!hex_str || !binary || strlen(hex_str) != binary_len * 2) return -1;
for (size_t i = 0; i < binary_len; i++) {
unsigned int byte;
if (sscanf(hex_str + i * 2, "%2x", &byte) != 1) return -1;
binary[i] = (uint8_t)byte;
}
return 0;
}
typedef enum {
SECTION_UNKNOWN,
SECTION_GLOBAL,
@@ -58,38 +49,6 @@ typedef enum {
SECTION_STUN
} section_type_t;
static char* trim(char *str) {
if (!str) return NULL;
while (isspace((unsigned char)*str)) str++;
char *end = str + strlen(str) - 1;
while (end > str && isspace((unsigned char)*end)) end--;
*(end + 1) = '\0';
return str;
}
static int parse_key_value(const char *line, char *key, size_t key_len, char *value, size_t value_len) {
char *equal = strchr(line, '=');
if (!equal) return -1;
size_t key_size = equal - line;
if (key_size >= key_len) return -1;
strncpy(key, line, key_size);
key[key_size] = '\0';
trim(key);
const char *val_start = equal + 1;
size_t val_len = strlen(val_start);
if (val_len >= value_len) return -1;
strcpy(value, val_start);
char *comment = strchr(value, '#');
if (comment) *comment = '\0';
trim(value);
return 0;
}
static int assign_string(char *dest, size_t dest_size, const char *src) {
if (!dest || !src || strlen(src) >= dest_size) return -1;
strcpy(dest, src);
@@ -242,13 +201,17 @@ static int add_route_entry(struct CFG_ROUTE_ENTRY **list, const char *subnet_str
return 0;
}
/* Разрешение добавляется только после проверки всей строки; ошибочный порт не становится wildcard. */
static int parse_firewall_rule(const char *rule_str, struct global_config *global) {
if (!rule_str || !global) return -1;
if (!rule_str || !global) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: missing rule/config"); return -1; }
char rule_copy[64];
strncpy(rule_copy, rule_str, sizeof(rule_copy) - 1);
rule_copy[sizeof(rule_copy) - 1] = '\0';
trim(rule_copy);
if (strlen(rule_str) >= sizeof(rule_copy)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: rule is too long: %s", rule_str);
return -1;
}
strcpy(rule_copy, rule_str);
config_ini_trim(rule_copy);
if (strcasecmp(rule_copy, "all") == 0) {
global->firewall_bypass_all = 1;
@@ -260,8 +223,15 @@ static int parse_firewall_rule(const char *rule_str, struct global_config *globa
char *port_str = strchr(rule_copy, ':');
uint16_t port = 0;
if (port_str) {
*port_str = '\0';
port = (uint16_t)atoi(port_str + 1);
*port_str++ = '\0';
char *end = NULL;
errno = 0;
unsigned long parsed = strtoul(port_str, &end, 10);
if (!isdigit((unsigned char)*port_str) || errno || *end || parsed > UINT16_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: invalid port in rule: %s", rule_str);
return -1;
}
port = (uint16_t)parsed;
}
// Parse IP to uint32_t (host order)
@@ -275,6 +245,10 @@ static int parse_firewall_rule(const char *rule_str, struct global_config *globa
// Reallocate array if needed
if ((global->firewall_rule_count % INITIAL_ARRAY_CAPACITY) == 0) {
if ((uint32_t)global->firewall_rule_count > UINT32_MAX / sizeof(struct CFG_FIREWALL_RULE) - INITIAL_ARRAY_CAPACITY) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: too many rules");
return -1;
}
int new_cap = global->firewall_rule_count + INITIAL_ARRAY_CAPACITY;
struct CFG_FIREWALL_RULE *new_rules = u_realloc(global->firewall_rules,
new_cap * sizeof(struct CFG_FIREWALL_RULE));
@@ -288,7 +262,6 @@ static int parse_firewall_rule(const char *rule_str, struct global_config *globa
struct CFG_FIREWALL_RULE *rule = &global->firewall_rules[global->firewall_rule_count];
rule->ip = ip;
rule->port = port;
rule->bypass = 0;
global->firewall_rule_count++;
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Added firewall rule: %s:%d", rule_str, port);
@@ -301,7 +274,7 @@ static int parse_control_allow(const char *rule_str, struct global_config *globa
char rule_copy[64];
strncpy(rule_copy, rule_str, sizeof(rule_copy) - 1);
rule_copy[sizeof(rule_copy) - 1] = '\0';
trim(rule_copy);
config_ini_trim(rule_copy);
uint8_t cidr = 32;
char *slash = strchr(rule_copy, '/');
@@ -535,7 +508,7 @@ static int parse_tcp_proxy_client(const char *key, const char *value, struct glo
char buf[256];
strncpy(buf, value, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
trim(buf);
config_ini_trim(buf);
char* local_port_str = strtok(buf, " ");
char* arrow = strtok(NULL, " ");
@@ -569,24 +542,41 @@ static int parse_tcp_proxy_client(const char *key, const char *value, struct glo
static int parse_nat(const char *key, const char *value, struct global_config *global, const char *filename, int line_num) {
if (strcmp(key, "tun_ifname") == 0) {
strncpy(global->nat_tun_ifname, value, sizeof(global->nat_tun_ifname) - 1);
if (assign_string(global->nat_tun_ifname, sizeof(global->nat_tun_ifname), value) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: NAT interface name too long: %s", filename, line_num, value);
return -1;
}
return 0;
}
if (strcmp(key, "tun_ip") == 0) {
uint8_t netmask;
parse_ip_with_netmask(value, &global->nat_tun_ip, &netmask);
if (parse_ip_with_netmask(value, &global->nat_tun_ip, &netmask) != 0 || global->nat_tun_ip.family != AF_INET) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: NAT requires an IPv4 tun_ip: %s", filename, line_num, value);
return -1;
}
return 0;
}
if (strcmp(key, "nat_via") == 0) {
global->nat_via_node_id = strtoull(value, NULL, 16);
char* end;
errno = 0;
uint64_t node_id = strtoull(value, &end, 16);
if (!*value || *value == '-' || *value == '+' || *end || errno == ERANGE) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid NAT provider ID: %s", filename, line_num, value);
return -1;
}
global->nat_via_node_id = node_id;
return 0;
}
if (strcmp(key, "port_start") == 0) {
global->nat_port_start = (uint16_t)atoi(value);
return 0;
}
if (strcmp(key, "port_end") == 0) {
global->nat_port_end = (uint16_t)atoi(value);
if (strcmp(key, "port_start") == 0 || strcmp(key, "port_end") == 0) {
char* end;
errno = 0;
unsigned long port = strtoul(value, &end, 10);
if (!isdigit((unsigned char)*value) || *end || errno == ERANGE || port == 0 || port > 65535) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid NAT %s: %s (expected 1..65535)", filename, line_num, key, value);
return -1;
}
if (strcmp(key, "port_start") == 0) global->nat_port_start = (uint16_t)port;
else global->nat_port_end = (uint16_t)port;
return 0;
}
if (strcmp(key, "forward") == 0) {
@@ -594,21 +584,44 @@ static int parse_nat(const char *key, const char *value, struct global_config *g
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Too many NAT forward rules (max %d)", MAX_NAT_FORWARDS);
return -1;
}
// Format: proto:internal_ip:internal_port:external_port
// Example: tcp:192.168.1.100:8080:8080
// Format: proto:node_id_hex:internal_ip:internal_port:external_port.
char buf[128];
strncpy(buf, value, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
trim(buf);
char* proto_str = strtok(buf, ":");
char* ip_str = strtok(NULL, ":");
char* int_port = strtok(NULL, ":");
char* ext_port = strtok(NULL, ":");
if (!proto_str || !ip_str || !int_port || !ext_port) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Invalid NAT forward format: %s (expected proto:ip:internal_port:external_port)", value);
if (strlen(value) >= sizeof(buf)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: NAT forward value too long", filename, line_num);
return -1;
}
strcpy(buf, value);
char* fields[5];
char* cursor = buf;
for (int i = 0; i < 5; i++) {
fields[i] = cursor;
char* colon = strchr(cursor, ':');
if ((i < 4 && !colon) || (i == 4 && colon) || !*cursor || colon == cursor) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid NAT forward: %s (proto:node_id:ip:internal_port:external_port)",
filename, line_num, value);
return -1;
}
if (colon) { *colon = '\0'; cursor = colon + 1; }
}
char* proto_str = fields[0];
char* ip_str = fields[2];
char* end;
errno = 0;
uint64_t node_id = strtoull(fields[1], &end, 16);
if (!isxdigit((unsigned char)*fields[1]) || !node_id || *end || errno == ERANGE) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid NAT forward node ID: %s", filename, line_num, fields[1]);
return -1;
}
uint16_t ports[2];
for (int i = 0; i < 2; i++) {
errno = 0;
unsigned long port = strtoul(fields[i + 3], &end, 10);
if (!isdigit((unsigned char)*fields[i + 3]) || *end || errno == ERANGE || port == 0 || port > 65535) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid NAT forward port: %s", filename, line_num, fields[i + 3]);
return -1;
}
ports[i] = (uint16_t)port;
}
uint8_t proto;
if (strcasecmp(proto_str, "tcp") == 0) proto = IPPROTO_TCP;
@@ -625,13 +638,14 @@ static int parse_nat(const char *key, const char *value, struct global_config *g
}
int idx = global->nat_forward_count;
global->nat_forwards[idx].node_id = node_id;
global->nat_forwards[idx].proto = proto;
global->nat_forwards[idx].internal_ip_host = ntohl(addr.s_addr);
global->nat_forwards[idx].internal_port_net = htons((uint16_t)atoi(int_port));
global->nat_forwards[idx].external_port = (uint16_t)atoi(ext_port);
global->nat_forwards[idx].internal_port_net = htons(ports[0]);
global->nat_forwards[idx].external_port = ports[1];
global->nat_forward_count++;
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "NAT forward: %s %s:%s -> :%s",
proto_str, ip_str, int_port, ext_port);
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "NAT forward: :%u -> node=%016llx %s %s:%u",
ports[1], (unsigned long long)node_id, proto_str, ip_str, ports[0]);
return 0;
}
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown NAT option '%s'. Valid: tun_ifname, tun_ip, nat_via, port_start, port_end, forward", filename, line_num, key);
@@ -756,14 +770,14 @@ static int parse_client(const char *key, const char *value, struct CFG_CLIENT *c
}
if (strcmp(key, "server_name") == 0) { return assign_string(cli->reality.server_name, sizeof(cli->reality.server_name), value); }
if (strcmp(key, "short_id") == 0) {
if (hex_to_binary(value, cli->reality.short_id, REALITY_SHORT_ID_SIZE) < 0) {
if (config_ini_hex(value, cli->reality.short_id, REALITY_SHORT_ID_SIZE) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: client reality short_id must be %d hex chars", filename, line_num, REALITY_SHORT_ID_SIZE * 2);
return -1;
}
return 0;
}
if (strcmp(key, "public_key") == 0) {
if (hex_to_binary(value, cli->reality.server_static_pubkey, REALITY_AUTH_KEY_SIZE) < 0) {
if (config_ini_hex(value, cli->reality.server_static_pubkey, REALITY_AUTH_KEY_SIZE) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: client reality public_key must be %d hex chars", filename, line_num, REALITY_AUTH_KEY_SIZE * 2);
return -1;
}
@@ -867,34 +881,23 @@ static int parse_reality(const char *key, const char *value, struct reality_conf
if (strcmp(key, "server_name") == 0) { return assign_string(rc->server_name, sizeof(rc->server_name), value); }
if (strcmp(key, "dest") == 0) { return assign_string(rc->dest, sizeof(rc->dest), value); }
if (strcmp(key, "short_id") == 0) {
if (hex_to_binary(value, rc->short_id, REALITY_SHORT_ID_SIZE) < 0) {
if (config_ini_hex(value, rc->short_id, REALITY_SHORT_ID_SIZE) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: reality short_id must be %d hex chars", filename, line_num, REALITY_SHORT_ID_SIZE * 2);
return -1;
}
return 0;
}
if (strcmp(key, "short_ids") == 0) {
rc->short_id_count = 0;
char buf[MAX_LINE_LEN];
assign_string(buf, sizeof(buf), value);
char *tok = strtok(buf, ",");
while (tok) {
char *t = trim(tok);
if (rc->short_id_count >= REALITY_MAX_SHORT_IDS) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: too many reality short_ids (max %d)", filename, line_num, REALITY_MAX_SHORT_IDS);
return -1;
}
if (hex_to_binary(t, rc->short_ids[rc->short_id_count], REALITY_SHORT_ID_SIZE) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: bad reality short_id '%s'", filename, line_num, t);
return -1;
}
rc->short_id_count++;
tok = strtok(NULL, ",");
if (config_ini_hex_list(value, &rc->short_ids[0][0], REALITY_SHORT_ID_SIZE,
REALITY_MAX_SHORT_IDS, &rc->short_id_count)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: invalid reality short_ids list", filename, line_num);
return -1;
}
return 0;
}
if (strcmp(key, "public_key") == 0) {
if (hex_to_binary(value, rc->public_key, REALITY_AUTH_KEY_SIZE) < 0) {
rc->has_public_key = 0;
if (config_ini_hex(value, rc->public_key, REALITY_AUTH_KEY_SIZE) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: reality public_key must be %d hex chars", filename, line_num, REALITY_AUTH_KEY_SIZE * 2);
return -1;
}
@@ -902,7 +905,8 @@ static int parse_reality(const char *key, const char *value, struct reality_conf
return 0;
}
if (strcmp(key, "private_key") == 0) {
if (hex_to_binary(value, rc->private_key, REALITY_AUTH_KEY_SIZE) < 0) {
rc->has_private_key = 0;
if (config_ini_hex(value, rc->private_key, REALITY_AUTH_KEY_SIZE) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: reality private_key must be %d hex chars", filename, line_num, REALITY_AUTH_KEY_SIZE * 2);
return -1;
}
@@ -1002,17 +1006,9 @@ static int parse_stun(const char* key, const char* value, struct global_config*
}
static section_type_t parse_section_header(const char *line, char *name, size_t name_len) {
if (line[0] != '[') return SECTION_UNKNOWN;
size_t line_len = strlen(line);
if (line[line_len - 1] != ']') return SECTION_UNKNOWN;
char section[128];
if (line_len - 2 >= sizeof(section)) return SECTION_UNKNOWN;
strncpy(section, line + 1, line_len - 2);
section[line_len - 2] = '\0';
trim(section);
if (config_ini_section(line, section, sizeof(section))) return SECTION_UNKNOWN;
if (strcasecmp(section, "global") == 0) return SECTION_GLOBAL;
if (strcasecmp(section, "routing") == 0) return SECTION_ROUTING;
if (strcasecmp(section, "debug") == 0) return SECTION_DEBUG;
@@ -1035,8 +1031,8 @@ static section_type_t parse_section_header(const char *line, char *name, size_t
if (!colon) return SECTION_UNKNOWN;
*colon = '\0';
char *type = trim(section);
char *n = trim(colon + 1);
char *type = config_ini_trim(section);
char *n = config_ini_trim(colon + 1);
if (strlen(n) >= name_len) return SECTION_UNKNOWN;
strcpy(name, n);
@@ -1048,313 +1044,6 @@ static section_type_t parse_section_header(const char *line, char *name, size_t
return SECTION_UNKNOWN;
}
static struct utun_config* parse_config_internal(FILE *fp, const char *filename) {
struct utun_config *cfg = u_calloc(1, sizeof(struct utun_config));
if (!cfg) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for config structure");
return NULL;
}
// Set default values
cfg->global.name[0] = '\0';
cfg->global.mtu = 1280; // Default MTU: safe for IPv6 tunnels
cfg->global.keepalive_timeout = 2000; // Default 2 seconds
cfg->global.keepalive_interval = 200; // Default 0.2 s
cfg->global.keepalive_adaptive = 1; // Default: adaptive enabled
cfg->global.bbr_max_cwnd = 1048576; // Default 1MB (was INFLIGHT_LIM_MAX)
cfg->global.auto_sockets = 0; // Default: disabled, use manual [server] sockets
cfg->global.auto_socket_skip_no_default_route = 1; // Default: в авто-режиме пропускать интерфейсы без default-маршрута
cfg->global.tcp_recv_buf = 0; // 0 = OS default
cfg->global.tcp_proxy_server_connect_timeout_ms = 2000; // 2 с, 0 = без таймаута
cfg->global.firewall_rules = NULL;
cfg->global.firewall_rule_count = 0;
cfg->global.firewall_bypass_all = 0;
cfg->global.control_allows = NULL;
cfg->global.control_allow_count = 0;
cfg->global.db_sync_enabled = 0;
cfg->global.db_sync_ttl = 86400;
cfg->global.ntp_enabled = 0;
cfg->global.ntp_servers = NULL;
cfg->global.ntp_server_count = 0;
cfg->global.ntp_resync_interval = 3600;
cfg->global.chatserver_enabled = 0;
cfg->global.chatserver_join_password[0] = '\0';
cfg->global.chatserver_allowed_groups[0] = '\0';
cfg->global.chatserver_storage_total_size = 0;
cfg->global.chatserver_storage_unit_size = 0;
reality_config_set_defaults(&cfg->global.reality);
section_type_t cur_section = SECTION_UNKNOWN;
struct CFG_SERVER *cur_server = NULL;
struct CFG_CLIENT *cur_client = NULL;
struct CFG_NETWORK *cur_network = NULL;
char line[MAX_LINE_LEN];
int line_num = 0;
while (fgets(line, sizeof(line), fp)) {
line_num++;
char *trimmed = trim(line);
if (trimmed[0] == '\0' || trimmed[0] == '#') continue;
if (trimmed[0] == '[') {
// Handle previous section
if (cur_section == SECTION_SERVER && cur_server) {
// Add server to linked list
cur_server->next = cfg->servers;
cfg->servers = cur_server;
cur_server = NULL;
}
if (cur_section == SECTION_CLIENT && cur_client) {
// Add client to linked list
cur_client->next = cfg->clients;
cfg->clients = cur_client;
cur_client = NULL;
}
if (cur_section == SECTION_NETWORK && cur_network) {
cur_network->next = cfg->networks;
cfg->networks = cur_network;
cur_network = NULL;
}
char name[MAX_CONN_NAME_LEN];
cur_section = parse_section_header(trimmed, name, sizeof(name));
if (cur_section == SECTION_SERVER) {
cur_server = u_calloc(1, sizeof(struct CFG_SERVER));
if (!cur_server) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for server %s", name);
goto error;
}
strcpy(cur_server->name, name);
cur_server->fib = -1;
} else if (cur_section == SECTION_CLIENT) {
cur_client = u_calloc(1, sizeof(struct CFG_CLIENT));
if (!cur_client) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for client %s", name);
goto error;
}
strcpy(cur_client->name, name);
reality_client_config_set_defaults(&cur_client->reality);
} else if (cur_section == SECTION_NETWORK) {
cur_network = u_calloc(1, sizeof(struct CFG_NETWORK));
if (!cur_network) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for network %s", name);
goto error;
}
strcpy(cur_network->name, name);
}
continue;
}
char key[MAX_LINE_LEN], value[MAX_LINE_LEN];
if (parse_key_value(trimmed, key, sizeof(key), value, sizeof(value)) < 0) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid key=value format", filename, line_num);
continue;
}
switch (cur_section) {
case SECTION_GLOBAL:
if (parse_global(key, value, &cfg->global, filename, line_num) < 0) {
// parse_global already printed the error
}
break;
case SECTION_SERVER:
if (cur_server && parse_server(key, value, cur_server, filename, line_num) < 0) {
// parse_server already printed the error
}
break;
case SECTION_CLIENT:
if (cur_client && parse_client(key, value, cur_client, cfg->servers, filename, line_num) < 0) {
// parse_client already printed the error
}
break;
case SECTION_ROUTING:
if (strcmp(key, "route_subnet") == 0) {
add_route_entry(&cfg->route_subnets, value);
} else if (strcmp(key, "my_subnet") == 0) {
add_route_entry(&cfg->my_subnets, value);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown routing option '%s'. Valid: route_subnet, my_subnet", filename, line_num, key);
}
break;
case SECTION_DEBUG:
// Format: category=level (e.g., etcp=debug, crypto=info)
if (cfg->global.debug_levels.count < DEBUG_CATEGORY_COUNT) {
strncpy(cfg->global.debug_levels.category[cfg->global.debug_levels.count],
key, 15);
cfg->global.debug_levels.category[cfg->global.debug_levels.count][15] = '\0';
strncpy(cfg->global.debug_levels.level[cfg->global.debug_levels.count],
value, 15);
cfg->global.debug_levels.level[cfg->global.debug_levels.count][15] = '\0';
cfg->global.debug_levels.count++;
} else DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Too many debug categories, ignored %s=%s", filename, line_num, key, value);
break;
case SECTION_FIREWALL:
if (strcmp(key, "allow") == 0) {
if (parse_firewall_rule(value, &cfg->global) < 0) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid firewall rule: %s", filename, line_num, value);
}
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown firewall option '%s'. Valid: allow", filename, line_num, key);
}
break;
case SECTION_CONTROL:
if (parse_control(key, value, &cfg->global, filename, line_num) < 0) {
// parse_control already printed the error
}
break;
case SECTION_ALLOWED_KEYS:
if (strcmp(key, "allow_all") == 0) {
cfg->global.allowed_keys_allow_all = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0;
} else if (strcmp(key, "key") == 0) {
struct CFG_ALLOWED_KEY *new_key = u_calloc(1, sizeof(struct CFG_ALLOWED_KEY));
if (!new_key) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "Failed to allocate allowed key");
} else if (strlen(value) != SC_PUBKEY_SIZE * 2) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid key length %zu, expected %d", filename, line_num, strlen(value), SC_PUBKEY_SIZE * 2);
u_free(new_key);
} else if (hex_to_binary(value, new_key->key_bin, SC_PUBKEY_SIZE) != 0) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid hex key format", filename, line_num);
u_free(new_key);
} else {
new_key->next = cfg->global.allowed_keys;
cfg->global.allowed_keys = new_key;
cfg->global.allowed_keys_count++;
}
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown allowed_keys option '%s'. Valid: allow_all, key", filename, line_num, key);
}
break;
case SECTION_NAT:
cfg->global.nat_enabled = 1;
if (parse_nat(key, value, &cfg->global, filename, line_num) < 0) {
// parse_nat already printed the error
}
break;
case SECTION_TCP_PROXY_CLIENT:
cfg->global.tcp_proxy_client_enabled = 1;
if (parse_tcp_proxy_client(key, value, &cfg->global, filename, line_num) < 0) {
// parse_tcp_proxy_client already printed the error
}
break;
case SECTION_TCP_PROXY_SERVER:
cfg->global.tcp_proxy_server_enabled = 1;
if (strcmp(key, "tcp_recv_buf") == 0) {
cfg->global.tcp_recv_buf = atoi(value);
} else if (strcmp(key, "fib") == 0) {
cfg->global.tcp_proxy_server_fib = atoi(value);
} else if (strcmp(key, "so_mark") == 0) {
cfg->global.tcp_proxy_server_so_mark = atoi(value);
} else if (strcmp(key, "netif") == 0) {
cfg->global.tcp_proxy_server_netif_index = get_netif_index(value);
} else if (strcmp(key, "bind_ip") == 0) {
strncpy(cfg->global.tcp_proxy_server_bind_ip, value, sizeof(cfg->global.tcp_proxy_server_bind_ip) - 1);
cfg->global.tcp_proxy_server_bind_ip[sizeof(cfg->global.tcp_proxy_server_bind_ip) - 1] = '\0';
} else if (strcmp(key, "connect_timeout") == 0) {
cfg->global.tcp_proxy_server_connect_timeout_ms = atoi(value);
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown tcp_proxy_server option '%s'. Valid: tcp_recv_buf, fib, so_mark, netif, bind_ip, connect_timeout", filename, line_num, key);
}
break;
case SECTION_NETWORK:
if (cur_network && parse_network(key, value, cur_network, filename, line_num) < 0) {
// parse_network already printed the error
}
break;
case SECTION_NTP:
if (strcmp(key, "enabled") == 0) {
cfg->global.ntp_enabled = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0;
} else if (strcmp(key, "server") == 0) {
struct CFG_NTP_SERVER *ns = u_calloc(1, sizeof(struct CFG_NTP_SERVER));
if (ns) {
strncpy(ns->name, value, sizeof(ns->name) - 1);
ns->name[sizeof(ns->name) - 1] = '\0';
ns->next = cfg->global.ntp_servers;
cfg->global.ntp_servers = ns;
cfg->global.ntp_server_count++;
} else {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "%s:%d: Failed to allocate NTP server entry", filename, line_num);
}
} else if (strcmp(key, "interval") == 0) {
cfg->global.ntp_resync_interval = atoi(value);
if (cfg->global.ntp_resync_interval < 60) cfg->global.ntp_resync_interval = 60;
} else {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown ntp option '%s'. Valid: enabled, server, interval", filename, line_num, key);
}
break;
case SECTION_CHATSERVER:
cfg->global.chatserver_enabled = 1;
cfg->global.db_sync_enabled = 1;
if (strcmp(key, "db_path") == 0) {
assign_string(cfg->global.db_path, sizeof(cfg->global.db_path), value);
} else if (parse_chatserver(key, value, &cfg->global, filename, line_num) < 0 &&
chat_setting_set_state(&cfg->global.chat_settings, key, value) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown chatserver option '%s'. Valid: db_path, join_password, allowed_groups, storage_total_size, storage_unit_size, storage_autoload, opus_codec_preset, radio_opus_mode, radio_opus_bitrate, radio_capture_stereo, compressor_enabled, compressor_max_gain_db, compressor_rise_rate, radio_compressor_enabled, radio_compressor_max_gain_db, media_download_max_peers", filename, line_num, key);
}
break;
case SECTION_GUI:
break;
case SECTION_REALITY:
parse_reality(key, value, &cfg->global.reality, filename, line_num);
break;
case SECTION_SOCKS:
parse_socks(key, value, &cfg->global, filename, line_num);
break;
case SECTION_STUN:
if (parse_stun(key, value, &cfg->global, filename, line_num) < 0) goto error;
break;
default:
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Key outside section: %s", filename, line_num, key);
break;
}
}
if (cfg->global.stun_enabled && cfg->global.stun_server_count < 2) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: enabled STUN requires at least two servers", filename);
goto error;
}
// Handle final sections
if (cur_section == SECTION_SERVER && cur_server) {
cur_server->next = cfg->servers;
cfg->servers = cur_server;
}
if (cur_section == SECTION_CLIENT && cur_client) {
cur_client->next = cfg->clients;
cfg->clients = cur_client;
}
if (cur_section == SECTION_NETWORK && cur_network) {
cur_network->next = cfg->networks;
cfg->networks = cur_network;
}
// Post-parse defaults: tun_enabled=1 if not explicitly disabled
if (!cfg->global.tun_enabled && cfg->global.tun_ifname[0] == '\0' && cfg->global.tun_ip.family == AF_UNSPEC) {
// No TUN config at all — treat as explicitly off (for non-root SOCKS/HTTP-only mode)
} else if (!cfg->global.tun_enabled) {
// TUN was explicitly disabled via tun_enabled=no
} else {
// TUN enabled (default) — set default ifname if not provided
cfg->global.tun_enabled = 1;
if (cfg->global.tun_ifname[0] == '\0') {
snprintf(cfg->global.tun_ifname, sizeof(cfg->global.tun_ifname), "tun0");
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Auto-generated TUN interface name: %s", cfg->global.tun_ifname);
}
}
return cfg;
error:
if (cur_server) u_free(cur_server);
if (cur_client) {
free_cfg_client_links(cur_client->links);
u_free(cur_client);
}
if (cur_network) u_free(cur_network);
free_config(cfg);
return NULL;
}
/* ── Buffer line reader for parse_config_from_buf ── */
typedef struct { const char *buf; size_t len; size_t pos; } blr_t;
static int blr_get_line(blr_t *blr, char *line, size_t maxlen) {
@@ -1370,6 +1059,8 @@ static int blr_get_line(blr_t *blr, char *line, size_t maxlen) {
}
struct utun_config* parse_config_from_buf(const char *buf, size_t len, const char *filename) {
if (!filename) filename = "<buffer>";
if (config_ini_validate(buf, len, filename)) return NULL;
struct utun_config *cfg = u_calloc(1, sizeof(struct utun_config));
if (!cfg) { DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate memory for config structure"); return NULL; }
cfg->global.name[0] = '\0';
@@ -1381,6 +1072,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
cfg->global.auto_sockets = 0;
cfg->global.auto_socket_skip_no_default_route = 1;
cfg->global.tcp_recv_buf = 0;
cfg->global.tcp_proxy_server_connect_timeout_ms = 2000;
cfg->global.firewall_rules = NULL;
cfg->global.firewall_rule_count = 0;
cfg->global.firewall_bypass_all = 0;
@@ -1409,7 +1101,7 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
while (blr_get_line(&blr, line, sizeof(line))) {
line_num++;
char *trimmed = trim(line);
char *trimmed = config_ini_trim(line);
if (trimmed[0] == '\0' || trimmed[0] == '#') continue;
if (trimmed[0] == '[') {
if (cur_section == SECTION_SERVER && cur_server) { cur_server->next = cfg->servers; cfg->servers = cur_server; cur_server = NULL; }
@@ -1423,7 +1115,11 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
continue;
}
char key[MAX_LINE_LEN], value[MAX_LINE_LEN];
if (parse_key_value(trimmed, key, sizeof(key), value, sizeof(value)) < 0) { DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid key=value format", filename, line_num); continue; }
if (config_ini_key_value(trimmed, key, sizeof(key), value, sizeof(value)) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid key=value format", filename, line_num);
if (cur_section == SECTION_FIREWALL || cur_section == SECTION_NAT) goto error;
continue;
}
switch (cur_section) {
case SECTION_GLOBAL: parse_global(key, value, &cfg->global, filename, line_num); break;
case SECTION_SERVER: if (cur_server) parse_server(key, value, cur_server, filename, line_num); break;
@@ -1436,10 +1132,22 @@ struct utun_config* parse_config_from_buf(const char *buf, size_t len, const cha
strncpy(cfg->global.debug_levels.level[index], value, 15); cfg->global.debug_levels.level[index][15] = '\0';
} else DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s:%d: Too many debug categories, ignored %s=%s", filename, line_num, key, value);
break;
case SECTION_FIREWALL: if (strcmp(key, "allow") == 0) parse_firewall_rule(value, &cfg->global); else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown firewall option '%s'", filename, line_num, key); break;
case SECTION_FIREWALL:
if (strcmp(key, "allow") != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown firewall option '%s'", filename, line_num, key);
goto error;
}
if (parse_firewall_rule(value, &cfg->global) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Invalid firewall rule allow=%s", filename, line_num, value);
goto error;
}
break;
case SECTION_CONTROL: parse_control(key, value, &cfg->global, filename, line_num); break;
case SECTION_ALLOWED_KEYS: if (strcmp(key, "allow_all") == 0) cfg->global.allowed_keys_allow_all = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; else if (strcmp(key, "key") == 0) { struct CFG_ALLOWED_KEY *nk = u_calloc(1, sizeof(struct CFG_ALLOWED_KEY)); if (nk && strlen(value) == SC_PUBKEY_SIZE * 2 && hex_to_binary(value, nk->key_bin, SC_PUBKEY_SIZE) == 0) { nk->next = cfg->global.allowed_keys; cfg->global.allowed_keys = nk; cfg->global.allowed_keys_count++; } else u_free(nk); } else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown allowed_keys option '%s'", filename, line_num, key); break;
case SECTION_NAT: cfg->global.nat_enabled = 1; parse_nat(key, value, &cfg->global, filename, line_num); break;
case SECTION_ALLOWED_KEYS: if (strcmp(key, "allow_all") == 0) cfg->global.allowed_keys_allow_all = strcasecmp(value, "yes") == 0 || strcasecmp(value, "1") == 0 || strcasecmp(value, "true") == 0; else if (strcmp(key, "key") == 0) { struct CFG_ALLOWED_KEY *nk = u_calloc(1, sizeof(struct CFG_ALLOWED_KEY)); if (nk && strlen(value) == SC_PUBKEY_SIZE * 2 && config_ini_hex(value, nk->key_bin, SC_PUBKEY_SIZE) == 0) { nk->next = cfg->global.allowed_keys; cfg->global.allowed_keys = nk; cfg->global.allowed_keys_count++; } else u_free(nk); } else DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s:%d: Unknown allowed_keys option '%s'", filename, line_num, key); break;
case SECTION_NAT:
cfg->global.nat_enabled = 1;
if (parse_nat(key, value, &cfg->global, filename, line_num) < 0) goto error;
break;
case SECTION_TCP_PROXY_CLIENT: cfg->global.tcp_proxy_client_enabled = 1; parse_tcp_proxy_client(key, value, &cfg->global, filename, line_num); break;
case SECTION_TCP_PROXY_SERVER:
cfg->global.tcp_proxy_server_enabled = 1;
@@ -1493,17 +1201,17 @@ error:
return NULL;
}
/* Файл и буфер проходят один разбор; ошибки чтения/закрытия не дают частичный конфиг. */
struct utun_config* parse_config(const char *filename) {
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Opening config file: %s", filename);
FILE *fp = fopen(filename, "r");
if (!fp) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to open config file: %s (errno=%d)", filename, errno);
return NULL;
}
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Successfully opened config file");
struct utun_config *config = parse_config_internal(fp, filename);
fclose(fp);
if (!filename || !*filename) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "empty config filename"); return NULL; }
FILE *file = fopen(filename, "rb");
if (!file) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot open config %s: %s", filename, strerror(errno)); return NULL; }
char *text = NULL;
size_t length = 0;
int rc = config_ini_read(file, filename, &text, &length);
if (fclose(file)) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "cannot close config %s: %s", filename, strerror(errno)); rc = -1; }
struct utun_config *config = rc ? NULL : parse_config_from_buf(text, length, filename);
u_free(text);
return config;
}
@@ -1695,7 +1403,7 @@ void print_chatserver_invite_link(const struct utun_config *config, const struct
char pub_hex_first[SC_PUBKEY_SIZE * 2 + 1];
memcpy(pub_hex_first, g->my_public_key_hex, SC_PUBKEY_SIZE * 2);
pub_hex_first[SC_PUBKEY_SIZE * 2] = '\0';
if (hex_to_binary(pub_hex_first, pub_bin, SC_PUBKEY_SIZE) < 0) {
if (config_ini_hex(pub_hex_first, pub_bin, SC_PUBKEY_SIZE) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "print_chatserver_invite_link: invalid pubkey hex");
return;
}
@@ -1824,7 +1532,7 @@ int config_peer_in_clients(const struct utun_config* config, const uint8_t* pubk
uint8_t pk_bin[SC_PUBKEY_SIZE];
for (const struct CFG_CLIENT* cli = config->clients; cli; cli = cli->next) {
if (!cli->peer_public_key_hex[0]) continue;
if (hex_to_binary(cli->peer_public_key_hex, pk_bin, SC_PUBKEY_SIZE) != 0) continue;
if (config_ini_hex(cli->peer_public_key_hex, pk_bin, SC_PUBKEY_SIZE) != 0) continue;
if (memcmp(pk_bin, pubkey, SC_PUBKEY_SIZE) == 0) return 1;
}
return 0;
+17 -7
View File
@@ -1,6 +1,10 @@
/* INI-конфигурация ядра: слушающие сокеты, исходящие пиры, сети/подсети и настройки сервисов.
* parse_config читает файл, parse_config_from_buf — текст; filename задаёт источник в диагностике.
* Успех возвращает выделенную utun_config со списками; освобождение — только free_config.
* Файл и буфер проходят один разбор. Пробелы вокруг секций, ключей и значений удаляются;
* имена секций нечувствительны к регистру, имена ключей чувствительны. Комментарий — #.
* HEX проверяется целиком; ошибочный список REALITY short_ids не применяется частично.
* Встроенный NUL и строки >1022 байтов до LF запрещены (CR входит в этот предел).
* Парсер заполняет модель, запуск сетевых ресурсов выполняет utun_instance. */
#ifndef CONFIG_PARSER_H
#define CONFIG_PARSER_H
@@ -93,9 +97,8 @@ struct CFG_ROUTE_ENTRY {
};
struct CFG_FIREWALL_RULE {
uint32_t ip; // IPv4 in host byte order for sorting
uint16_t port; // 0 means any port
uint8_t bypass; // 1 for allow=all (bypass all checks)
uint32_t ip; // IPv4 назначения в host order.
uint16_t port; // TCP/UDP-порт назначения в host order; 0 разрешает весь IP.
};
struct CFG_CONTROL_ALLOW {
@@ -133,7 +136,7 @@ struct global_config {
char my_public_key_hex[MAX_KEY_LEN];
uint64_t my_node_id;
uint8_t client_type; // 0=server, 1=desktop, 2=mobile
int tun_enabled; // 1 = create TUN (default), 0 = skip TUN (run without root)
int tun_enabled; // 1 = запрошен TUN, 0 = выключен (значение по умолчанию)
char tun_ifname[16]; // TUN interface name (e.g., "tun12")
struct IP tun_ip;
int mtu;
@@ -141,7 +144,7 @@ struct global_config {
char control_ip[MAX_ADDR_LEN]; // Control server IP address
// Debug and logging configuration
char log_file[256]; // Path to log file (empty = stdout)
char db_path[256]; // Path to SQLite database (empty = disabled)
char db_path[256]; // Каталог chats.db; пусто — ядро использует SQLite :memory:
int db_sync_enabled; // 1 = enable distributed DB sync (default: 0)
int chatserver_enabled; // 1 = chat server mode (set by [chatserver] section presence)
uint32_t db_sync_ttl; // TTL for unsent records in seconds (default: 86400)
@@ -165,7 +168,7 @@ struct global_config {
int keepalive_timeout; // keepalive timeout in ms (default: 2000)
int keepalive_interval; // keepalive interval in ms (default: 200)
int keepalive_adaptive; // 1 = adaptive period (default), 0 = fixed interval
int bbr_max_cwnd; // BBR cwnd cap in bytes (default: 100000)
int bbr_max_cwnd; // BBR cwnd cap in bytes (default: 1048576)
// Firewall configuration
struct CFG_FIREWALL_RULE *firewall_rules;
@@ -181,7 +184,8 @@ struct global_config {
int allowed_keys_count;
int allowed_keys_allow_all;
// NAT configuration ([nat] section)
// Опции [nat] включают NAT; forward=proto:node_id_hex:IPv4:internal_port:external_port.
// Ошибки parse_nat отклоняют весь конфиг; диапазон портов дополнительно проверяет eim_nat_init_ctx.
int nat_enabled;
char nat_tun_ifname[16];
struct IP nat_tun_ip;
@@ -190,6 +194,7 @@ struct global_config {
uint16_t nat_port_end;
#define MAX_NAT_FORWARDS 64
struct {
uint64_t node_id; // Узел назначения статического проброса; ненулевой ID.
uint8_t proto;
uint32_t internal_ip_host;
uint16_t internal_port_net;
@@ -261,7 +266,12 @@ struct utun_config {
struct CFG_NETWORK* networks;
};
/* Непустой путь; результат освобождать free_config. NULL при ошибке открытия, чтения,
* закрытия, памяти или обязательной проверки формата. Часть ошибочных опций только
* логируется и пропускается; ненулевой результат не гарантирует корректность всех полей. */
struct utun_config* parse_config(const char *filename);
/* buf — len байтов без завершающего NUL (NULL допустим при len=0), буфер не меняется.
* filename — источник для диагностики, NULL означает <buffer>. Возврат как у parse_config. */
struct utun_config* parse_config_from_buf(const char *buf, size_t len, const char *filename);
void free_config(struct utun_config *config);
void print_config(const struct utun_config *config);
+19 -10
View File
@@ -10,6 +10,14 @@ INI-style парсер конфигурационного файла uTun. Чи
Формат: стандартные `.ini`-секции `[section]`, строки `key=value`, комментарии `#`. Именованные секции через двоеточие: `[server:main]`, `[client:peer1]`, `[network:mynet]`.
Файл и буфер проходят один разбор. Общие функции в `config_ini.h` удаляют пробелы
вокруг имён секций, ключей и значений. Имена секций нечувствительны к регистру,
имена ключей чувствительны. Встроенный NUL и физические строки длиннее 1022 байтов
до LF запрещены; CR учитывается в этом пределе.
Ошибки разбора опций `[nat]` отклоняют весь конфиг. Формат статического проброса:
`forward=proto:node_id_hex:IPv4:internal_port:external_port`; узел назначения обязателен,
оба порта — десятичные 1..65535. Динамический диапазон дополнительно проверяется при запуске NAT.
## 2. Как пользоваться
```c
@@ -29,11 +37,12 @@ free_config(cfg);
```
**Нюансы:**
- `parse_config` возвращает `NULL` при ошибках открытия/парсинга — проверяй возврат.
- Все строковые поля обрезаются по размерам буфера, переполнение логируется.
- `parse_config` возвращает `NULL` при ошибках открытия, чтения, закрытия, памяти и обязательных проверок формата. Некоторые ошибки отдельных опций только логируются и пропускаются; ненулевой результат не гарантирует корректность всех полей.
- Обработка длинных значений зависит от поля: часть строк обрезается, `assign_string` отклоняет значение без изменения поля.
- Ключи клиента `link` требуют, чтобы сервер (`[server:имя]`) был объявлен выше по файлу. Иначе ошибка.
- `tun_enabled` по умолчанию `1`; если задан `tun_ifname` или `tun_ip` — генерируется имя `tun0` при отсутствии. Если нет ни одного TUN-параметра — считается выключенным (режим без root).
- `tun_enabled` по умолчанию `0`. При явном включении и отсутствии имени интерфейса используется `tun0`.
- IPv6-адреса в `[server:...] addr` поддерживают префиксы `temporary_ipv6:` и `permanent_ipv6:`.
- HEX декодируется после проверки каждого символа. Ошибочный список REALITY `short_ids` отклоняется целиком и оставляет `short_id_count=0`; ошибочный последний REALITY-ключ сбрасывает соответствующий `has_*_key`.
## 3. API
@@ -62,10 +71,10 @@ free_config(cfg);
| `[client:name]` | `parse_client()` | `link` (формат: `server_name:ip:port`), `peer_public_key`, `keepalive` |
| `[network:name]` | `parse_network()` | `id` (hex), `pubkey`, `signing_key` |
| `[routing]` | inline | `route_subnet`, `my_subnet` |
| `[firewall]` | `parse_firewall_rule()` | `allow` (IP:port или `all` для bypass) |
| `[firewall]` | `parse_firewall_rule()` | `allow=IPv4[:port]` или `allow=all`; порт 0–65535, 0 разрешает весь IP; CIDR запрещён |
| `[control]` | `parse_control()` | `ip`, `port`, `allow` (подсеть) |
| `[allowed_keys]` | inline | `allow_all`, `key` (64 hex) |
| `[nat]` | `parse_nat()` | `tun_ifname`, `tun_ip`, `nat_via`, `port_start`, `port_end`, `forward` (proto:ip:in_port:ext_port) |
| `[nat]` | `parse_nat()` | `tun_ifname`, `tun_ip`, `nat_via`, `port_start`, `port_end`, `forward` (`proto:node_id_hex:IPv4:in_port:ext_port`) |
| `[tcp_proxy_client]` | `parse_tcp_proxy_client()` | `enabled`, `tun_name`, `tun_ip`, `mtu`, `via_node`, `socks_*`, `http_proxy_*`, `forward` (port -> ip:port) |
| `[tcp_proxy_server]` | inline | `tcp_recv_buf` |
| `[ntp]` | inline | `enabled`, `server`, `interval` |
@@ -76,7 +85,7 @@ free_config(cfg);
| Функция | Назначение |
|---------|------------|
| `parse_config(filename)` | Открывает файл, парсит через `parse_config_internal`, возвращает `utun_config*`. `NULL` при ошибке. |
| `parse_config(filename)` | Читает и закрывает файл, затем вызывает `parse_config_from_buf`. Возвращает `utun_config*` или `NULL`. |
| `parse_config_from_buf(buf, len, filename)` | Парсит конфиг из буфера (in-memory). `filename` используется только для сообщений об ошибках. |
| `free_config(config)` | Рекурсивно освобождает все связные списки и сам `utun_config`. Безопасен для `NULL`. |
| `print_config(config)` | Выводит всё содержимое конфига через `DEBUG_INFO(DEBUG_CATEGORY_CONFIG, ...)`. Полезно для проверки после парсинга. |
@@ -86,12 +95,12 @@ free_config(cfg);
| Функция | Назначение |
|---------|------------|
| `trim(str)` | Обрезает пробелы слева и справа (in-place) |
| `parse_key_value(line, ...)` | Разбирает строку `key=value`, удаляет комментарии после `#` |
| `config_ini_trim(str)` | Общий INI-код: удаляет пробелы слева и справа на месте, сохраняя адрес строки |
| `config_ini_key_value(line, ...)` | Общий INI-код: разбирает `key=value`, удаляет комментарий после `#` и пробелы |
| `parse_ip_with_netmask(str, ip, netmask)` | Парсит `1.2.3.4/24` или `::1/64` в `struct IP` |
| `parse_sockaddr(addr, port, sockaddr)` | Преобразует адрес+порт в `sockaddr_storage` через `getaddrinfo` |
| `parse_address_and_port(str, sockaddr)` | Разбирает строку `ip:port` → вызов `parse_sockaddr` |
| `get_netif_index(ifname)` | Получает индекс сетевого интерфейса через `if_nametoindex` |
| `parse_firewall_rule(rule_str, global)` | Парсит `allow=IP[:port]` или `all` |
| `parse_firewall_rule(rule_str, global)` | Строго разбирает `allow=IPv4[:port]` или `all`; ошибка/неизвестная опция firewall отменяет весь новый конфиг |
| `parse_control_allow(rule_str, global)` | Парсит `allow=IP[/CIDR]` |
| `hex_to_binary(hex_str, binary, len)` | Конвертит hex-строку в бинарный массив |
| `config_ini_hex(hex_str, binary, len)` | Общий INI-код: проверяет весь HEX до декодирования |
+356 -480
View File
@@ -1,524 +1,400 @@
// config_updater.c - Configuration file updater implementation
/* Идентичность INI: эффективные значения, точечная правка текста и публикация файла. */
#define _POSIX_C_SOURCE 200809L
#include "config_updater.h"
#include "config_parser.h"
#include "config_ini.h"
#include "secure_channel.h"
#include "debug_config.h"
#include "../lib/platform_compat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <errno.h>
#include "../lib/mem.h"
#include <fcntl.h>
#include <stdlib.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <io.h>
#endif
#define PRIV_HEXKEY_LEN 65 // 32 bytes * 2 hex chars + null
#define PUB_HEXKEY_LEN 65 // 32 bytes * 2 hex chars + null
#define HEXNODEID_LEN 17 // 8 bytes * 2 hex chars + null
#define MAX_LINE_LEN 1024
enum { CFG_PRIVATE, CFG_PUBLIC, CFG_NODE_ID, CFG_REALITY_PRIVATE, CFG_REALITY_IDS, CFG_REALITY_ENABLED, CFG_FIELD_COUNT };
enum { CFG_OTHER_SECTION, CFG_GLOBAL_SECTION, CFG_REALITY_SECTION, CFG_SECTION_COUNT };
struct config_field {
const char *key;
int section;
int present;
int update;
char value[CONFIG_INI_LINE_SIZE];
};
/* Форматировать без усечения; проверка ёмкости не переполняет size_t. */
void bytes_to_hex(const uint8_t *bytes, size_t len, char *hex_str, size_t hex_len) {
if (!bytes || !hex_str || hex_len < len * 2 + 1) {
if (hex_str && hex_len > 0) hex_str[0] = '\0';
static const char digits[] = "0123456789abcdef";
if (!bytes || !hex_str || !hex_len || len > (hex_len - 1) / 2) {
if (hex_str && hex_len) hex_str[0] = '\0';
return;
}
for (size_t i = 0; i < len; i++) {
snprintf(hex_str + i * 2, hex_len - i * 2, "%02x", bytes[i]);
}
for (size_t i = 0; i < len; i++) { hex_str[i * 2] = digits[bytes[i] >> 4]; hex_str[i * 2 + 1] = digits[bytes[i] & 15]; }
hex_str[len * 2] = '\0';
}
static int is_valid_priv_key(const char *key) {
if (!key || strlen(key) != 64) return 0;
for (int i = 0; i < 64; i++) {
if (!isxdigit((unsigned char)key[i])) return 0;
}
return 1;
/* Единый перечень полей для сбора, редактирования и проверки результата. */
static void config_fields_init(struct config_field fields[CFG_FIELD_COUNT]) {
memset(fields, 0, sizeof(*fields) * CFG_FIELD_COUNT);
fields[CFG_PRIVATE].key = "my_private_key"; fields[CFG_PRIVATE].section = CFG_GLOBAL_SECTION;
fields[CFG_PUBLIC].key = "my_public_key"; fields[CFG_PUBLIC].section = CFG_GLOBAL_SECTION;
fields[CFG_NODE_ID].key = "my_node_id"; fields[CFG_NODE_ID].section = CFG_GLOBAL_SECTION;
fields[CFG_REALITY_PRIVATE].key = "private_key"; fields[CFG_REALITY_PRIVATE].section = CFG_REALITY_SECTION;
fields[CFG_REALITY_IDS].key = "short_ids"; fields[CFG_REALITY_IDS].section = CFG_REALITY_SECTION;
fields[CFG_REALITY_ENABLED].key = "enabled"; fields[CFG_REALITY_ENABLED].section = CFG_REALITY_SECTION;
}
static int is_valid_pub_key(const char *key) {
if (!key || strlen(key) != 64) return 0;
for (int i = 0; i < 64; i++) {
if (!isxdigit((unsigned char)key[i])) return 0;
}
return 1;
}
static int is_valid_node_id(uint64_t node_id) {
return node_id != 0;
}
static int write_mem_to_file(const char *filename, const char *buffer, size_t size) {
FILE *fp = fopen(filename, "w");
if (!fp) return -1;
size_t written = fwrite(buffer, 1, size, fp);
if (written != size) {
fclose(fp);
/* Любой заголовок завершает предыдущую секцию; значения разбирает общий INI-код. */
static int config_field_line(char *line, int *section, const struct config_field fields[CFG_FIELD_COUNT], char *value) {
config_ini_trim(line);
if (line[0] == '[') {
char name[128];
*section = CFG_OTHER_SECTION;
if (!config_ini_section(line, name, sizeof(name))) {
if (!strcasecmp(name, "global")) *section = CFG_GLOBAL_SECTION;
else if (!strcasecmp(name, "reality")) *section = CFG_REALITY_SECTION;
}
return -1;
}
fclose(fp);
return 0;
}
static char* find_section_global(char *buf, size_t buf_len) {
char *p = buf;
while (p < buf + buf_len) {
if (p[0] == '[' && strncmp(p + 1, "global", 6) == 0) {
char *end = strchr(p, ']');
if (end) return end + 1;
}
p = strchr(p, '\n');
if (!p) break;
p++; // skip '\n'
}
return NULL;
}
static char* find_option(char *buf, size_t buf_len, const char *option) {
char *p = buf;
while (p < buf + buf_len) {
if (strncmp(p, option, strlen(option)) == 0) {
char *after_key = p + strlen(option);
if (after_key[0] == '=') return p;
}
p = strchr(p, '\n');
if (!p) break;
p++;
}
return NULL;
}
// Смещение байта сразу после ']' заголовка секции "[name]" (регистронезависимо), -1 если нет.
static long find_section_header(const char *buf, size_t buf_len, const char *name) {
size_t name_len = strlen(name);
size_t i = 0;
while (i < buf_len) {
if (buf[i] == '[' && i + name_len + 2 <= buf_len &&
strncasecmp(buf + i + 1, name, name_len) == 0 && buf[i + name_len + 1] == ']') {
return (long)(i + name_len + 2);
}
const char *nl = memchr(buf + i, '\n', buf_len - i);
if (!nl) break;
i = (size_t)(nl - buf) + 1;
}
if (!line[0] || line[0] == '#') return -1;
char key[CONFIG_INI_LINE_SIZE];
if (config_ini_key_value(line, key, sizeof(key), value, CONFIG_INI_LINE_SIZE)) return -1;
for (int i = 0; i < CFG_FIELD_COUNT; i++) if (fields[i].section == *section && !strcmp(fields[i].key, key)) return i;
return -1;
}
// Смещение строки, начинающейся с "option=", в диапазоне [start, end), -1 если нет.
static long find_option_in_range(const char *buf, size_t start, size_t end, const char *option) {
size_t olen = strlen(option);
size_t i = start;
while (i < end) {
if (i + olen <= end && strncmp(buf + i, option, olen) == 0 && buf[i + olen] == '=') {
return (long)i;
}
const char *nl = memchr(buf + i, '\n', end - i);
if (!nl) break;
i = (size_t)(nl - buf) + 1;
/* Собрать последние текстовые значения и конец последней секции каждого вида. */
static void config_scan(const char *text, size_t length, struct config_field fields[CFG_FIELD_COUNT], size_t ends[CFG_SECTION_COUNT]) {
int section = CFG_OTHER_SECTION;
for (int i = 0; i < CFG_SECTION_COUNT; i++) ends[i] = SIZE_MAX;
for (size_t pos = 0; pos < length;) {
const char *nl = memchr(text + pos, '\n', length - pos);
size_t end = nl ? (size_t)(nl - text) + 1 : length;
char line[CONFIG_INI_LINE_SIZE], value[CONFIG_INI_LINE_SIZE];
memcpy(line, text + pos, end - pos); line[end - pos] = '\0';
char *start = line;
while (isspace((unsigned char)*start)) start++;
if (*start == '[') ends[section] = pos;
int index = config_field_line(line, &section, fields, value);
if (index >= 0) { strcpy(fields[index].value, value); fields[index].present = 1; }
pos = end;
}
return -1;
ends[section] = length;
}
// Заменить строку option=value внутри секции [section], либо вставить её сразу после
// строки заголовка секции (без пустых строк). Возвращает 0 / -1.
static int insert_or_replace_in_section(char **buf, size_t *buf_len, size_t *buf_capacity,
const char *section, const char *option, const char *value) {
if (!buf || !*buf || !buf_len || !buf_capacity || !section || !option || !value) return -1;
long hdr_off = find_section_header(*buf, *buf_len, section);
if (hdr_off < 0) return -1;
size_t sec_start = (size_t)hdr_off;
char *next_sec = memchr(*buf + sec_start, '[', *buf_len - sec_start);
size_t sec_end = next_sec ? (size_t)(next_sec - *buf) : *buf_len;
long opt_off = find_option_in_range(*buf, sec_start, sec_end, option);
if (opt_off >= 0) {
size_t opos = (size_t)opt_off;
char *nl = memchr(*buf + opos, '\n', sec_end - opos);
int has_trailing_nl = (nl != NULL);
size_t line_end = nl ? (size_t)(nl - *buf) + 1 : sec_end;
size_t old_len = line_end - opos;
char new_line[MAX_LINE_LEN];
int new_len = snprintf(new_line, sizeof(new_line), "%s=%s%s", option, value, has_trailing_nl ? "\n" : "");
if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1;
long len_diff = (long)new_len - (long)old_len;
if ((long)*buf_len + len_diff + 1 > (long)*buf_capacity) {
*buf_capacity = *buf_len + len_diff + 1024;
char *new_buf = u_realloc(*buf, *buf_capacity);
if (!new_buf) return -1;
*buf = new_buf;
hdr_off = find_section_header(*buf, *buf_len, section);
if (hdr_off < 0) return -1;
sec_start = (size_t)hdr_off;
next_sec = memchr(*buf + sec_start, '[', *buf_len - sec_start);
sec_end = next_sec ? (size_t)(next_sec - *buf) : *buf_len;
opt_off = find_option_in_range(*buf, sec_start, sec_end, option);
if (opt_off < 0) return -1;
opos = (size_t)opt_off;
nl = memchr(*buf + opos, '\n', sec_end - opos);
has_trailing_nl = (nl != NULL);
line_end = nl ? (size_t)(nl - *buf) + 1 : sec_end;
/* Вставить отсутствующие поля в нужную секцию, при необходимости создать заголовок. */
static int config_insert_missing(struct strbuf *output, const struct config_field fields[CFG_FIELD_COUNT],
int section, int has_section, const char *eol) {
int started = 0;
for (int i = 0; i < CFG_FIELD_COUNT; i++) {
if (fields[i].section != section || !fields[i].update || fields[i].present) continue;
if (!started) {
if (output->len && output->buf[output->len - 1] != '\n' && strbuf_addf(output, "%s", eol) < 0) return -1;
if (!has_section && strbuf_addf(output, "[%s]%s", section == CFG_GLOBAL_SECTION ? "global" : "reality", eol) < 0) return -1;
started = 1;
}
memmove(*buf + opos + new_len, *buf + line_end, *buf_len - line_end + 1);
memcpy(*buf + opos, new_line, new_len);
*buf_len += len_diff;
} else {
size_t ins = sec_start;
int need_leading_nl = 0;
if (ins < *buf_len && (*buf)[ins] == '\n') {
ins += 1;
} else {
need_leading_nl = 1;
}
char new_line[MAX_LINE_LEN];
int new_len = snprintf(new_line, sizeof(new_line), "%s%s=%s\n", need_leading_nl ? "\n" : "", option, value);
if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1;
if (*buf_len + (size_t)new_len + 1 > *buf_capacity) {
*buf_capacity = *buf_len + (size_t)new_len + 1024;
char *new_buf = u_realloc(*buf, *buf_capacity);
if (!new_buf) return -1;
*buf = new_buf;
hdr_off = find_section_header(*buf, *buf_len, section);
if (hdr_off < 0) return -1;
ins = (size_t)hdr_off;
need_leading_nl = 0;
if (ins < *buf_len && (*buf)[ins] == '\n') {
ins += 1;
} else {
need_leading_nl = 1;
}
new_len = snprintf(new_line, sizeof(new_line), "%s%s=%s\n", need_leading_nl ? "\n" : "", option, value);
if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1;
}
memmove(*buf + ins + new_len, *buf + ins, *buf_len - ins + 1);
memcpy(*buf + ins, new_line, new_len);
*buf_len += new_len;
if (strbuf_addf(output, "%s=%s%s", fields[i].key, fields[i].value, eol) < 0) return -1;
}
return 0;
}
static int insert_or_replace_option(char **buf, size_t *buf_len, size_t *buf_capacity, const char *option, const char *value) {
if (!buf || !*buf || !buf_len || !buf_capacity || !option || !value) return -1;
// Check if option already exists
char *opt_pos = find_option(*buf, *buf_len, option);
if (opt_pos) {
// Determine end of old line (1 byte after \n or end of buffer)
char *nl = strchr(opt_pos, '\n');
int has_trailing_nl = (nl != NULL);
char *line_end = nl ? nl + 1 : *buf + *buf_len;
// Prepare new line
char new_line[MAX_LINE_LEN];
int new_len;
if (has_trailing_nl) {
new_len = snprintf(new_line, sizeof(new_line), "%s=%s\n", option, value);
} else {
new_len = snprintf(new_line, sizeof(new_line), "%s=%s", option, value);
/* Сохранить всё вне значений исправляемых полей, включая комментарии и CRLF. */
static int config_edit(const char *text, size_t length, const struct config_field fields[CFG_FIELD_COUNT],
const size_t ends[CFG_SECTION_COUNT], struct strbuf *output) {
const char *nl = memchr(text, '\n', length);
const char *eol = nl && nl > text && nl[-1] == '\r' ? "\r\n" : "\n";
int section = CFG_OTHER_SECTION;
for (size_t pos = 0;;) {
/* Запас покрывает вставки полей и строки: strbuf не должен вырасти до 2^32 байтов. */
if (output->len > UINT32_MAX / 2 - 16384) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "edited INI exceeds allocator size limit");
return -1;
}
if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1;
// Calculate length difference
size_t old_line_len = line_end - opt_pos;
long len_diff = new_len - old_line_len;
// Ensure buffer capacity
if (*buf_len + len_diff + 1 > *buf_capacity) {
*buf_capacity = *buf_len + len_diff + 1024;
char *new_buf = u_realloc(*buf, *buf_capacity);
if (!new_buf) return -1;
*buf = new_buf;
// Recalculate positions after realloc
opt_pos = find_option(*buf, *buf_len, option);
if (!opt_pos) return -1;
nl = strchr(opt_pos, '\n');
has_trailing_nl = (nl != NULL);
line_end = nl ? nl + 1 : *buf + *buf_len;
for (int s = CFG_GLOBAL_SECTION; s < CFG_SECTION_COUNT; s++) {
if (ends[s] == pos && config_insert_missing(output, fields, s, 1, eol)) return -1;
}
// Move content and insert new line
memmove(opt_pos + new_len, line_end, *buf_len - (line_end - *buf) + 1);
memcpy(opt_pos, new_line, new_len);
*buf_len += len_diff;
} else {
// Insert after [global] section
char *section_end = find_section_global(*buf, *buf_len);
if (!section_end) return -1;
// Find end of global section (next [ or end of buffer)
char *insert_pos = strchr(section_end, '[');
int need_leading_nl = 0;
if (!insert_pos) {
insert_pos = *buf + *buf_len;
if (*buf_len > 0 && (*buf)[*buf_len - 1] != '\n')
need_leading_nl = 1;
} else {
// Move to beginning of next line
char *prev_nl = insert_pos;
while (prev_nl > *buf && *prev_nl != '\n') prev_nl--;
if (*prev_nl == '\n') insert_pos = prev_nl + 1;
}
// Prepare new line
char new_line[MAX_LINE_LEN];
int new_len;
if (need_leading_nl)
new_len = snprintf(new_line, sizeof(new_line), "\n%s=%s\n", option, value);
else
new_len = snprintf(new_line, sizeof(new_line), "%s=%s\n", option, value);
if (new_len <= 0 || new_len >= (int)sizeof(new_line)) return -1;
// Ensure buffer capacity
if (*buf_len + new_len + 1 > *buf_capacity) {
*buf_capacity = *buf_len + new_len + 1024;
char *new_buf = u_realloc(*buf, *buf_capacity);
if (!new_buf) return -1;
*buf = new_buf;
// Recalculate insert position after realloc
section_end = find_section_global(*buf, *buf_len);
if (!section_end) return -1;
insert_pos = strchr(section_end, '[');
need_leading_nl = 0;
if (!insert_pos) {
insert_pos = *buf + *buf_len;
if (*buf_len > 0 && (*buf)[*buf_len - 1] != '\n')
need_leading_nl = 1;
} else {
char *prev_nl = insert_pos;
while (prev_nl > *buf && *prev_nl != '\n') prev_nl--;
if (*prev_nl == '\n') insert_pos = prev_nl + 1;
}
}
// Move content and insert new line
memmove(insert_pos + new_len, insert_pos, *buf_len - (insert_pos - *buf) + 1);
memcpy(insert_pos, new_line, new_len);
*buf_len += new_len;
if (pos == length) break;
nl = memchr(text + pos, '\n', length - pos);
size_t end = nl ? (size_t)(nl - text) + 1 : length;
char line[CONFIG_INI_LINE_SIZE], value[CONFIG_INI_LINE_SIZE];
memcpy(line, text + pos, end - pos); line[end - pos] = '\0';
int index = config_field_line(line, &section, fields, value);
if (index >= 0 && fields[index].update) {
const char *start = (const char *)memchr(text + pos, '=', end - pos) + 1;
const char *tail = text + end;
while (tail > start && (tail[-1] == '\n' || tail[-1] == '\r')) tail--;
const char *comment = memchr(start, '#', (size_t)(tail - start));
if (comment) tail = comment;
while (start < tail && isspace((unsigned char)*start)) start++;
while (tail > start && isspace((unsigned char)tail[-1])) tail--;
if (strbuf_addf(output, "%.*s%s%.*s", (int)(start - text - pos), text + pos, fields[index].value,
(int)(text + end - tail), tail) < 0) return -1;
} else if (strbuf_addf(output, "%.*s", (int)(end - pos), text + pos) < 0) return -1;
pos = end;
}
if (output->len > UINT32_MAX / 2 - 16384) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "edited INI exceeds allocator size limit");
return -1;
}
for (int s = CFG_GLOBAL_SECTION; s < CFG_SECTION_COUNT; s++) {
if (ends[s] == SIZE_MAX && config_insert_missing(output, fields, s, 0, eol)) return -1;
}
return 0;
}
int config_ensure_keys_and_node_id(const char *filename) {
// ── Read config file into memory (single open) ──
FILE *fp = fopen(filename, "rb");
char *file_buf = NULL;
size_t file_size = 0;
struct utun_config *config = NULL;
if (fp) {
fseek(fp, 0, SEEK_END); long fsz = ftell(fp); fseek(fp, 0, SEEK_SET);
if (fsz < 0) { fclose(fp); return -1; }
file_size = (size_t)fsz;
file_buf = u_malloc(file_size + 1);
if (!file_buf || fread(file_buf, 1, file_size, fp) != file_size) { u_free(file_buf); fclose(fp); return -1; }
file_buf[file_size] = '\0';
fclose(fp);
config = parse_config_from_buf(file_buf, file_size, filename);
if (!config) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to parse config from buffer: %s", filename); u_free(file_buf); return -1; }
} else {
// No file — create minimal config
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Config file not found, creating new: %s (errno=%d)", filename, errno);
config = u_calloc(1, sizeof(struct utun_config));
if (!config) return -1;
config->global.my_node_id = 0;
config->global.my_private_key_hex[0] = '\0';
config->global.my_public_key_hex[0] = '\0';
config->global.name[0] = '\0';
config->global.keepalive_timeout = 2000;
config->global.keepalive_interval = 200;
config->global.keepalive_adaptive = 1;
config->global.bbr_max_cwnd = 1048576;
}
struct global_config *global = &config->global;
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Checking config - priv_key='%s' (len=%zu), pub_key='%s' (len=%zu), node_id=%016llx",
global->my_private_key_hex[0] ? global->my_private_key_hex : "NULL",
global->my_private_key_hex[0] ? strlen(global->my_private_key_hex) : 0,
global->my_public_key_hex[0] ? global->my_public_key_hex : "NULL",
global->my_public_key_hex[0] ? strlen(global->my_public_key_hex) : 0,
(unsigned long long)global->my_node_id);
int need_priv_key = !is_valid_priv_key(global->my_private_key_hex);
int need_pub_key = 0;
int need_node_id = 0;
char new_priv_key[PRIV_HEXKEY_LEN] = {0};
char new_pub_key[PUB_HEXKEY_LEN] = {0};
uint64_t new_node_id = 0;
uint8_t priv_bin[SC_PRIVKEY_SIZE];
// ── Step 1: ensure valid privkey ──
if (need_priv_key) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Generating NEW keypair — private key was invalid/missing in %s", filename);
struct SC_MYKEYS mykeys;
if (sc_generate_keypair(&mykeys) != SC_OK) { free_config(config); u_free(file_buf); return -1; }
bytes_to_hex(mykeys.private_key, SC_PRIVKEY_SIZE, new_priv_key, sizeof(new_priv_key));
memcpy(priv_bin, mykeys.private_key, SC_PRIVKEY_SIZE);
} else {
for (int i = 0; i < SC_PRIVKEY_SIZE; i++) {
unsigned int byte;
if (sscanf(global->my_private_key_hex + i * 2, "%2x", &byte) != 1) { free_config(config); u_free(file_buf); return -1; }
priv_bin[i] = (uint8_t)byte;
}
bytes_to_hex(priv_bin, SC_PRIVKEY_SIZE, new_priv_key, sizeof(new_priv_key));
}
// ── Step 2: derive pubkey from privkey ──
uint8_t pub_bin[SC_PUBKEY_SIZE];
if (sc_compute_public_key_from_private(priv_bin, pub_bin) != SC_OK) { free_config(config); u_free(file_buf); return -1; }
bytes_to_hex(pub_bin, SC_PUBKEY_SIZE, new_pub_key, sizeof(new_pub_key));
if (!is_valid_pub_key(global->my_public_key_hex) || strcmp(global->my_public_key_hex, new_pub_key) != 0) {
need_pub_key = 1;
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Pubkey mismatch (or missing), fixing: config=%s derived=%s",
global->my_public_key_hex[0] ? global->my_public_key_hex : "NULL", new_pub_key);
}
// ── Step 3: derive node_id from pubkey ──
new_node_id = sc_derive_node_id_from_pubkey(pub_bin);
if (!is_valid_node_id(global->my_node_id) || global->my_node_id != new_node_id) {
need_node_id = 1;
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Node_id mismatch (or missing), fixing: config=%016llx derived=%016llx file=%s",
(unsigned long long)global->my_node_id, (unsigned long long)new_node_id, filename);
}
// ── Step 4: reality-камуфляж — private_key и short_ids при enabled=1 ──
int need_reality_priv = 0, need_reality_ids = 0;
char new_reality_priv_hex[REALITY_AUTH_KEY_SIZE * 2 + 1] = {0};
char new_short_ids_str[4 * (REALITY_SHORT_ID_SIZE * 2) + 4] = {0};
if (global->reality.enabled) {
if (!global->reality.has_private_key) {
need_reality_priv = 1;
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Reality private_key missing, generating new one in %s", filename);
uint8_t rpriv[REALITY_AUTH_KEY_SIZE], rpub[REALITY_AUTH_KEY_SIZE];
if (reality_generate_keypair(rpriv, rpub) != REALITY_OK) { free_config(config); u_free(file_buf); return -1; }
bytes_to_hex(rpriv, REALITY_AUTH_KEY_SIZE, new_reality_priv_hex, sizeof(new_reality_priv_hex));
}
if (global->reality.short_id_count == 0) {
need_reality_ids = 1;
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Reality short_ids missing, generating 4 ids in %s", filename);
char *p = new_short_ids_str;
size_t remain = sizeof(new_short_ids_str);
for (int i = 0; i < 4; i++) {
uint8_t sid[REALITY_SHORT_ID_SIZE];
if (random_bytes(sid, sizeof(sid)) != 0) { free_config(config); u_free(file_buf); return -1; }
char hex[REALITY_SHORT_ID_SIZE * 2 + 1];
bytes_to_hex(sid, REALITY_SHORT_ID_SIZE, hex, sizeof(hex));
int w = snprintf(p, remain, "%s%s", i ? "," : "", hex);
if (w < 0 || (size_t)w >= remain) { free_config(config); u_free(file_buf); return -1; }
p += w; remain -= (size_t)w;
}
}
}
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Validation results - need_priv_key=%d, need_pub_key=%d, need_node_id=%d, need_reality_priv=%d, need_reality_ids=%d",
need_priv_key, need_pub_key, need_node_id, need_reality_priv, need_reality_ids);
if (!need_priv_key && !need_pub_key && !need_node_id && !need_reality_priv && !need_reality_ids) {
free_config(config); u_free(file_buf); return 0;
}
char cfg_name[MAX_CONN_NAME_LEN]; snprintf(cfg_name, sizeof(cfg_name), "%s", global->name[0] ? global->name : "utun");
/* node_id должен целиком разбираться как ненулевой uint64_t. */
static int config_node_id(const char *text, uint64_t *id) {
char *end;
if (!*text || *text == '-' || *text == '+') return -1;
errno = 0;
unsigned long long parsed = strtoull(text, &end, 16);
if (errno || *end || !parsed) return -1;
*id = (uint64_t)parsed;
return 0;
}
/* Проверить выбранную идентичность и REALITY строгими декодерами и штатным парсером. */
static int config_verify(const char *text, size_t length, const char *filename, const uint8_t private_key[SC_PRIVKEY_SIZE],
const uint8_t public_key[SC_PUBKEY_SIZE], uint64_t node_id, int reality_enabled,
const uint8_t reality_private[REALITY_AUTH_KEY_SIZE],
const uint8_t ids[REALITY_MAX_SHORT_IDS][REALITY_SHORT_ID_SIZE], int id_count) {
struct config_field fields[CFG_FIELD_COUNT];
size_t ends[CFG_SECTION_COUNT];
uint8_t decoded[SC_PRIVKEY_SIZE], decoded_ids[REALITY_MAX_SHORT_IDS][REALITY_SHORT_ID_SIZE];
uint64_t parsed_id;
int parsed_count;
config_fields_init(fields);
config_scan(text, length, fields, ends);
if (config_ini_hex(fields[CFG_PRIVATE].value, decoded, sizeof(decoded)) || memcmp(decoded, private_key, sizeof(decoded)) ||
config_ini_hex(fields[CFG_PUBLIC].value, decoded, sizeof(decoded)) || memcmp(decoded, public_key, sizeof(decoded)) ||
config_node_id(fields[CFG_NODE_ID].value, &parsed_id) || parsed_id != node_id) return -1;
if (reality_enabled && (config_ini_hex(fields[CFG_REALITY_PRIVATE].value, decoded, sizeof(decoded)) ||
memcmp(decoded, reality_private, sizeof(decoded)) ||
config_ini_hex_list(fields[CFG_REALITY_IDS].value, &decoded_ids[0][0], REALITY_SHORT_ID_SIZE, REALITY_MAX_SHORT_IDS, &parsed_count) ||
parsed_count != id_count || memcmp(decoded_ids, ids, (size_t)id_count * REALITY_SHORT_ID_SIZE))) return -1;
struct utun_config *config = parse_config_from_buf(text, length, filename);
if (!config) return -1;
int valid = !config_ini_hex(config->global.my_private_key_hex, decoded, sizeof(decoded)) &&
!memcmp(decoded, private_key, sizeof(decoded)) && config->global.my_node_id == node_id &&
!config_ini_hex(config->global.my_public_key_hex, decoded, sizeof(decoded)) && !memcmp(decoded, public_key, sizeof(decoded));
if (reality_enabled) valid = valid && config->global.reality.enabled && config->global.reality.has_private_key &&
!memcmp(config->global.reality.private_key, reality_private, REALITY_AUTH_KEY_SIZE) &&
config->global.reality.short_id_count == id_count &&
!memcmp(config->global.reality.short_ids, ids, (size_t)id_count * REALITY_SHORT_ID_SIZE);
free_config(config);
return valid ? 0 : -1;
}
char node_id_hex[HEXNODEID_LEN + 1];
snprintf(node_id_hex, sizeof(node_id_hex), "%016llx", (unsigned long long)new_node_id);
/* Синхронизировать временный файл в том же каталоге, затем заменить имя. */
static int config_publish(const char *filename, const char *text, size_t length, int exists, const struct stat *original) {
FILE *file = NULL;
char *temp = NULL;
int result = -1, published = 0, temp_owned = 0;
const char *phase = "inspect destination";
#ifndef _WIN32
int directory = -1;
char *parent = NULL;
struct stat current;
if (!lstat(filename, &current)) {
if (!exists || !S_ISREG(current.st_mode) || current.st_nlink != 1 ||
current.st_dev != original->st_dev || current.st_ino != original->st_ino) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: destination changed or unsupported: existed=%d mode=%o links=%llu",
filename, exists, (unsigned)current.st_mode, (unsigned long long)current.st_nlink);
errno = EINVAL;
goto done;
}
} else if (exists || errno != ENOENT) goto done;
parent = u_strdup(filename);
if (!parent) goto done;
char *slash = strrchr(parent, '/');
if (!slash) strcpy(parent, ".");
else if (slash == parent) slash[1] = '\0';
else *slash = '\0';
phase = "open parent directory";
directory = open(parent, O_RDONLY | O_DIRECTORY);
if (directory < 0) goto done;
#else
(void)original;
DWORD attributes = GetFileAttributesA(filename);
if (attributes != INVALID_FILE_ATTRIBUTES && (!exists || (attributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)))) {
errno = EINVAL;
goto done;
}
if (exists) {
HANDLE handle = CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
BY_HANDLE_FILE_INFORMATION info;
int valid = handle != INVALID_HANDLE_VALUE && GetFileInformationByHandle(handle, &info) && info.nNumberOfLinks == 1;
if (handle != INVALID_HANDLE_VALUE) CloseHandle(handle);
if (!valid) { errno = EINVAL; goto done; }
}
#endif
phase = "create temporary file";
size_t name_length = strlen(filename);
if (name_length > UINT32_MAX - 64) { errno = EOVERFLOW; goto done; }
temp = u_malloc(name_length + 64);
if (!temp) goto done;
temp[0] = '\0';
for (int attempt = 0; attempt < 8; attempt++) {
uint8_t random[16];
char suffix[33];
if (random_bytes(random, sizeof(random))) goto done;
bytes_to_hex(random, sizeof(random), suffix, sizeof(suffix));
snprintf(temp, name_length + 64, "%s.tmp.%s", filename, suffix);
#ifdef _WIN32
int fd = _open(temp, _O_CREAT | _O_EXCL | _O_WRONLY | _O_BINARY, _S_IREAD | _S_IWRITE);
#else
int fd = open(temp, O_CREAT | O_EXCL | O_WRONLY, 0600);
#endif
if (fd < 0) { if (errno == EEXIST) continue; goto done; }
temp_owned = 1;
#ifdef _WIN32
file = _fdopen(fd, "wb");
if (!file) _close(fd);
#else
file = fdopen(fd, "wb");
if (!file) close(fd);
#endif
if (!file) goto done;
break;
}
if (!file) goto done;
phase = "write temporary file";
if (fwrite(text, 1, length, file) != length || fflush(file)) goto done;
#ifdef _WIN32
phase = "sync temporary file";
if (_commit(_fileno(file))) goto done;
#else
phase = "preserve ownership and permissions";
struct stat temporary;
if (fstat(fileno(file), &temporary)) goto done;
if (exists && (temporary.st_uid != original->st_uid || temporary.st_gid != original->st_gid) &&
fchown(fileno(file), original->st_uid, original->st_gid)) goto done;
if (fchmod(fileno(file), exists ? original->st_mode & 0777 : 0600)) goto done;
phase = "sync temporary file";
if (fsync(fileno(file))) goto done;
#endif
phase = "close temporary file";
int close_result = fclose(file);
file = NULL;
if (close_result) goto done;
phase = "replace destination";
#ifdef _WIN32
BOOL replaced = exists ? ReplaceFileA(filename, temp, NULL, 0, NULL, NULL) : MoveFileExA(temp, filename, MOVEFILE_WRITE_THROUGH);
if (!replaced) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: Windows replace failed: %lu", filename, GetLastError()); errno = EIO; goto done; }
#else
if (rename(temp, filename)) goto done;
#endif
published = 1;
#ifndef _WIN32
phase = "sync parent directory after replacement";
if (fsync(directory)) goto done;
#endif
result = 0;
done:
if (result) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: config update failed at '%s': %s (replaced=%d)", filename, phase, strerror(errno), published);
if (file && fclose(file)) DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: temporary close during cleanup failed: %s", filename, strerror(errno));
if (temp_owned && !published && remove(temp) && errno != ENOENT) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: temporary cleanup failed: %s", temp, strerror(errno));
}
u_free(temp);
#ifndef _WIN32
if (directory >= 0 && close(directory)) DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: parent directory close failed: %s", filename, strerror(errno));
u_free(parent);
#endif
if (!result) DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Config file updated: %s (%zu bytes)", filename, length);
return result;
}
// ── Modify/create config buffer ──
char *work_buf = NULL; size_t work_len = 0; size_t buf_capacity = 0;
int is_new_file = (!file_buf || !file_size);
if (is_new_file) {
buf_capacity = 2048;
work_buf = u_malloc(buf_capacity);
if (!work_buf) { u_free(file_buf); return -1; }
work_len = (size_t)snprintf(work_buf, buf_capacity,
"[global]\nmy_node_name=%s\nmy_private_key=%s\nmy_public_key=%s\nmy_node_id=%s\n",
cfg_name, new_priv_key, new_pub_key, node_id_hex);
u_free(file_buf); file_buf = NULL;
} else {
buf_capacity = file_size + 1024;
work_buf = u_malloc(buf_capacity);
if (!work_buf) { u_free(file_buf); return -1; }
memcpy(work_buf, file_buf, file_size + 1); /* Helpers use strchr; preserve the terminating NUL. */
work_len = file_size;
u_free(file_buf); file_buf = NULL;
/* Прочитать проверенный снимок, выбрать идентичность и опубликовать необходимые изменения. */
int config_ensure_keys_and_node_id(const char *filename) {
if (!filename || !*filename) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "empty config filename"); return -1; }
char *text = NULL;
size_t length = 0;
int exists = 0, result = -1;
struct stat original = {0};
FILE *file = fopen(filename, "rb");
if (file) {
exists = 1;
int rc = fstat(fileno(file), &original);
if (!rc && !S_ISREG(original.st_mode)) { errno = EINVAL; rc = -1; }
if (!rc) rc = config_ini_read(file, filename, &text, &length);
if (fclose(file)) rc = -1;
if (rc) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot read/close configuration: %s", filename, strerror(errno)); u_free(text); return -1; }
} else if (errno != ENOENT) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot open configuration: %s", filename, strerror(errno));
return -1;
}
if (!text) text = u_strdup("");
if (!text) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot allocate configuration buffer", filename); return -1; }
struct config_field fields[CFG_FIELD_COUNT];
size_t ends[CFG_SECTION_COUNT];
config_fields_init(fields);
config_scan(text, length, fields, ends);
uint8_t private_key[SC_PRIVKEY_SIZE], public_key[SC_PUBKEY_SIZE], decoded[SC_PUBKEY_SIZE];
uint8_t reality_private[REALITY_AUTH_KEY_SIZE] = {0}, ids[REALITY_MAX_SHORT_IDS][REALITY_SHORT_ID_SIZE] = {{0}};
int id_count = 0, updates = 0;
uint64_t parsed_id, node_id;
struct strbuf output = strbuf_new();
if (config_ini_hex(fields[CFG_PRIVATE].value, private_key, sizeof(private_key))) {
struct SC_MYKEYS keys;
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: generating identity: effective private key %s (length=%zu)", filename,
fields[CFG_PRIVATE].present ? "invalid" : "missing", strlen(fields[CFG_PRIVATE].value));
if (sc_generate_keypair(&keys) != SC_OK) goto done;
memcpy(private_key, keys.private_key, sizeof(private_key));
bytes_to_hex(private_key, sizeof(private_key), fields[CFG_PRIVATE].value, sizeof(fields[CFG_PRIVATE].value));
fields[CFG_PRIVATE].update = 1;
}
if (sc_compute_public_key_from_private(private_key, public_key) != SC_OK) goto done;
node_id = sc_derive_node_id_from_pubkey(public_key);
if (!node_id) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: derived zero node_id", filename); goto done; }
if (config_ini_hex(fields[CFG_PUBLIC].value, decoded, sizeof(decoded)) || memcmp(decoded, public_key, sizeof(decoded))) {
fields[CFG_PUBLIC].update = 1;
bytes_to_hex(public_key, sizeof(public_key), fields[CFG_PUBLIC].value, sizeof(fields[CFG_PUBLIC].value));
}
if (config_node_id(fields[CFG_NODE_ID].value, &parsed_id) || parsed_id != node_id) {
fields[CFG_NODE_ID].update = 1;
snprintf(fields[CFG_NODE_ID].value, sizeof(fields[CFG_NODE_ID].value), "%016llx", (unsigned long long)node_id);
}
int ret = 0;
if (!is_new_file) {
int write_keys = (need_priv_key || need_pub_key || need_node_id);
if (need_node_id) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing my_node_id=%s to %s", node_id_hex, filename);
if (insert_or_replace_option(&work_buf, &work_len, &buf_capacity, "my_node_id", node_id_hex) < 0) ret = -1;
int reality_enabled = atoi(fields[CFG_REALITY_ENABLED].value) != 0;
if (reality_enabled) {
if (config_ini_hex(fields[CFG_REALITY_PRIVATE].value, reality_private, sizeof(reality_private))) {
uint8_t public_part[REALITY_AUTH_KEY_SIZE];
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: generating REALITY private key: %s", filename,
fields[CFG_REALITY_PRIVATE].present ? "invalid" : "missing");
if (reality_generate_keypair(reality_private, public_part) != REALITY_OK) goto done;
bytes_to_hex(reality_private, sizeof(reality_private), fields[CFG_REALITY_PRIVATE].value, sizeof(fields[CFG_REALITY_PRIVATE].value));
fields[CFG_REALITY_PRIVATE].update = 1;
}
if (write_keys && ret == 0) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing my_private_key to %s", filename);
if (insert_or_replace_option(&work_buf, &work_len, &buf_capacity, "my_private_key", new_priv_key) < 0) ret = -1;
if (config_ini_hex_list(fields[CFG_REALITY_IDS].value, &ids[0][0], REALITY_SHORT_ID_SIZE, REALITY_MAX_SHORT_IDS, &id_count) || !id_count) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "%s: generating 4 REALITY short IDs: effective list missing, empty or invalid", filename);
id_count = 4;
if (random_bytes(&ids[0][0], (size_t)id_count * REALITY_SHORT_ID_SIZE)) goto done;
char *cursor = fields[CFG_REALITY_IDS].value;
for (int i = 0; i < id_count; i++) {
if (i) *cursor++ = ',';
bytes_to_hex(ids[i], REALITY_SHORT_ID_SIZE, cursor, REALITY_SHORT_ID_SIZE * 2 + 1);
cursor += REALITY_SHORT_ID_SIZE * 2;
}
fields[CFG_REALITY_IDS].update = 1;
}
if (write_keys && ret == 0) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing my_public_key to %s", filename);
if (insert_or_replace_option(&work_buf, &work_len, &buf_capacity, "my_public_key", new_pub_key) < 0) ret = -1;
}
if (need_reality_ids && ret == 0) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing reality short_ids to %s", filename);
if (insert_or_replace_in_section(&work_buf, &work_len, &buf_capacity, "reality", "short_ids", new_short_ids_str) < 0) ret = -1;
}
if (need_reality_priv && ret == 0) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Writing reality private_key to %s", filename);
if (insert_or_replace_in_section(&work_buf, &work_len, &buf_capacity, "reality", "private_key", new_reality_priv_hex) < 0) ret = -1;
}
}
for (int i = 0; i < CFG_FIELD_COUNT; i++) {
if (!fields[i].update) continue;
updates++;
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "%s: updating [%s] %s", filename,
fields[i].section == CFG_GLOBAL_SECTION ? "global" : "reality", fields[i].key);
}
if (ret == 0 && work_buf) {
if (write_mem_to_file(filename, work_buf, work_len) < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Failed to write updated config file: %s", filename);
ret = -1;
} else {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "Config file updated: %s (gen_priv=%d gen_pub=%d gen_nodeid=%d gen_reality_priv=%d gen_reality_ids=%d)", filename, need_priv_key, need_pub_key, need_node_id, need_reality_priv, need_reality_ids);
}
if (!updates) { DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "%s: identity and REALITY unchanged", filename); result = 0; goto done; }
if (config_edit(text, length, fields, ends, &output) || config_ini_validate(strbuf_str(&output), output.len, filename) ||
config_verify(strbuf_str(&output), output.len, filename, private_key, public_key, node_id, reality_enabled, reality_private, ids, id_count)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: prepared config failed construction/verification", filename);
goto done;
}
u_free(work_buf);
return ret;
result = config_publish(filename, strbuf_str(&output), output.len, exists, &original);
done:
if (result) DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "%s: cannot ensure identity/REALITY", filename);
strbuf_free(&output);
u_free(text);
return result;
}
+23 -5
View File
@@ -1,5 +1,13 @@
/* Обеспечивает идентичность X25519/node_id в INI-файле: проверяет поля и сохраняет исправленные ключи.
* config_ensure_keys_and_node_id меняет файл на диске; bytes_to_hex только форматирует буфер вызывающего. */
/* Обеспечивает согласованные X25519-ключи/node_id и параметры включённого REALITY в INI.
* Вызывать до загрузки конфигурации; вызывающий исключает параллельную запись файла.
* Последнее значение в соответствующих секциях определяет результат, включая ошибочное.
* Корректный приватный ключ сохраняется; pubkey/node_id выводятся из него. При ошибочном
* или отсутствующем приватном ключе создаётся новая идентичность. HEX сравнивается по байтам.
* REALITY: недостающий/ошибочный private_key генерируется; пустой/ошибочный short_ids
* заменяется четырьмя новыми ID. Корректные параметры и выключенный REALITY сохраняются.
* Исправный файл остаётся побайтно неизменным. При правке сохраняются посторонний текст,
* комментарии и существующие окончания строк; обновляются все дубли исправляемого поля.
* Новый файл создаётся только при ENOENT. Встроенный NUL и строки >1022 байтов до LF запрещены. */
#ifndef CONFIG_UPDATER_H
#define CONFIG_UPDATER_H
@@ -10,11 +18,21 @@
extern "C" {
#endif
// Ensures config file has valid my_private_key, my_public_key, and my_node_id
// If any are missing or invalid, generates and updates them
// Returns 0 on success, -1 on error
/* filename — непустой путь. 0 — значения корректны, изменения при необходимости записаны;
* -1 — ошибка с диагностикой CONFIG. Содержимое готовится и проверяется до записи.
* Временный файл создаётся эксклюзивно рядом с исходным, синхронизируется и закрывается.
* POSIX: rename + fsync каталога; до rename исходный файл сохраняется при ошибках.
* После rename ошибка fsync каталога даёт -1 с replaced=1: файл уже заменён.
* Сохраняются uid/gid и права 0777; новый файл имеет 0600. ACL/xattrs не копируются.
* Windows: _commit + ReplaceFileA для существующего файла, MoveFileExA для нового;
* гарантии восстановления после ошибок Windows API этим модулем не расширяются.
* При необходимости записи символические ссылки/reparse points и несколько hard links
* отклоняются. Корректный файл по ссылке допускает проверку без записи. */
int config_ensure_keys_and_node_id(const char *filename);
/* len байтов bytes -> 2*len строчных HEX-символов и NUL в hex_str, ёмкость hex_len.
* bytes/hex_str не NULL, буферы не перекрываются. При недостаточной ёмкости или NULL
* результат не усекать: доступный hex_str[0] обнуляется. len=0 даёт пустую строку. */
void bytes_to_hex(const uint8_t *bytes, size_t len, char *hex_str, size_t hex_len);
#ifdef __cplusplus
+67 -50
View File
@@ -1,67 +1,84 @@
# Config Updater (config_updater.c/h)
## 1. Назначение
Модуль обеспечивает согласованные X25519-ключи и node_id в INI-файле до его загрузки ядром.
При включённом REALITY также обеспечивает private_key и непустой список short_ids.
Публичный контракт приведён в config_updater.h, общий разбор строк — в config_ini.h.
Утилита автоматической инициализации и обновления ключей в конфигурационном файле uTun.
## Выбор значений
Основная задача — гарантировать, что в конфиге всегда присутствуют корректные и согласованные:
- `my_private_key` (X25519, 64 hex символа)
- `my_public_key` (производный от private, 64 hex)
- `my_node_id` (SHA256-идентификатор узла, 16 hex символов, вычисляется из private key)
Имена секций сравниваются целиком без учёта регистра, пробелы вокруг имени секции,
ключа и значения удаляются. Имена ключей чувствительны к регистру. Комментарии — #.
Последнее текстовое значение ключа во всех соответствующих секциях определяет результат,
включая пустое или некорректное. Опции из других секций не участвуют.
Если чего-то нет или значение неконсистентно — модуль генерирует недостающее через `secure_channel.h` и атомарно перезаписывает конфиг, сохраняя всё остальное содержимое файла нетронутым.
- Корректный my_private_key — ровно 64 HEX-символа; его бинарное значение сохраняется.
- my_public_key вычисляется через sc_compute_public_key_from_private().
- my_node_id вычисляется через sc_derive_node_id_from_pubkey(): SHA256 публичного ключа,
первые восемь байтов в представлении функции, старший бит сброшен.
- Если приватный ключ отсутствует или некорректен, создаётся новая идентичность.
- Сравнение HEX выполняется по байтам: регистр букв не вызывает перезапись.
- При включённом REALITY некорректный/отсутствующий private_key генерируется заново.
Некорректный/пустой/отсутствующий short_ids заменяется четырьмя случайными ID.
Корректный список сохраняется целиком. Ошибка одного элемента отклоняет весь список.
- При выключенном REALITY его параметры не исправляются.
## 2. Как пользоваться
HEX проверяется посимвольно до декодирования. Частичные результаты проверки не применяются.
Встроенный NUL и физические строки длиннее 1022 байтов до LF запрещены; CR учитывается в этом пределе.
```c
#include "config_updater.h"
## Изменение текста
int ret = config_ensure_keys_and_node_id("/etc/utun/config.ini");
if (ret == 0) {
// Конфиг готов: ключи и node_id гарантированно на месте
} else {
// Ошибка — смотри DEBUG_*(DEBUG_CATEGORY_CONFIG)
}
```
Исправный файл остаётся побайтно неизменным. Если поле требует исправления,
обновляются все его вхождения в соответствующих секциях. Недостающие поля добавляются
в конец последней подходящей секции; отсутствующая секция создаётся в конце файла.
**Типовой сценарий (первый запуск):**
1. Файла нет → создаётся минимальный `[global]` с `my_node_name`, сгенерированными ключами и `my_node_id`.
2. Файл есть, но нет `my_private_key` → генерируется новая X25519-пара, записываются все три поля.
3. Файл есть с `my_private_key`, но нет `my_public_key` или `my_node_id` → вычисляются из private key, дописываются.
4. Все поля есть, но `my_public_key` не соответствует `my_private_key` или `my_node_id` не совпадает → перезаписываются на корректные.
Посторонние строки сохраняются побайтно. В изменяемых строках сохраняются пробелы,
комментарии и окончания строк вокруг заменяемого значения. Добавляемые строки используют
тип первого окончания строки в файле, либо LF. При необходимости добавляется разделитель
после последней строки без LF. Новый или пустой файл получает [global] с тремя полями идентичности.
**Нюансы:**
- Вычисление pubkey: `sc_compute_public_key_from_private(priv_bin, pub_bin)`.
- Вычисление node_id: `sc_derive_node_id(priv_bin)`.
- Генерация новой пары ключей: `sc_generate_keypair(&mykeys)`.
- Файл читается целиком в память, модифицируется через `insert_or_replace_option`, затем пишется обратно. Это атомарно: либо файл полностью перезаписан, либо остался нетронутым (при ошибке).
- Редактирование in-place через `memmove`: строки не сдвигают остальной конфиг, только вставляются/заменяются нужные опции внутри секции `[global]`.
- Для нового файла создаётся минимальный `[global]` с именем узла из `my_node_name` (или `"utun"` если не задано).
Перед записью итоговый текст проверяется строгими декодерами и штатным парсером.
Это проверка полей идентичности и необходимых параметров REALITY; парсер продолжает
логировать и пропускать некоторые ошибки прочих опций.
## 3. API
## Запись и ошибки
### Функции
config_ensure_keys_and_node_id(filename) принимает непустой путь. Вызывающая сторона
исключает параллельную запись конфигурации. Возврат 0 означает корректные значения и,
если требовалось обновление, успешную запись. Возврат -1 означает ошибку с диагностикой CONFIG.
| Функция | Назначение |
|---------|------------|
| `config_ensure_keys_and_node_id(filename)` | **Основная функция.** Читает конфиг, проверяет/генерирует ключи и node_id, записывает обратно. Возвращает 0 при успехе, -1 при ошибке. |
| `bytes_to_hex(bytes, len, hex_str, hex_len)` | Вспомогательная: конвертация бинарного массива в hex-строку. Безопасна для нулевых указателей и маленьких буферов. |
Новый файл создаётся только при ENOENT. Ошибка доступа, чтения, выделения памяти или
закрытия входного файла прекращает операцию. Временный файл создаётся эксклюзивно
рядом с исходным. Проверяются запись, fflush, синхронизация и fclose до замены имени.
### Внутренние функции (static)
На POSIX применяется rename, затем fsync каталога:
- До успешного rename исходный файл при ошибках сохраняется.
- Ошибка fsync каталога после rename возвращает -1 и логируется с replaced=1.
Файл уже заменён; устойчивость замены к аварии не подтверждена.
- Сохраняются uid/gid и обычные права доступа (0777); новый файл имеет 0600.
ACL, xattrs и специальные биты режима не копируются.
- Временный файл удаляется при ошибках до публикации; ошибка удаления логируется.
| Функция | Назначение |
|---------|------------|
| `is_valid_priv_key(key)` | Проверяет, что строка — 64 hex-символа |
| `is_valid_pub_key(key)` | То же для публичного ключа |
| `is_valid_node_id(node_id)` | Проверяет `node_id != 0` |
| `write_mem_to_file(filename, buffer, size)` | Пишет буфер в файл (одной операцией) |
| `find_section_global(buf, buf_len)` | Ищет позицию после `[global]` в текстовом буфере |
| `find_option(buf, buf_len, option)` | Ищет строку `option=...` в буфере |
| `insert_or_replace_option(buf, buf_len, buf_capacity, option, value)` | Заменяет значение опции при наличии, либо вставляет новую строку после `[global]`. Работает с `memmove`/`memcpy`, перевыделяет буфер при необходимости. |
На Windows используются _commit и ReplaceFileA для существующего файла либо
MoveFileExA с MOVEFILE_WRITE_THROUGH для нового. Модуль не расширяет гарантии
восстановления после ошибок этих Windows API.
### Зависимости
Если требуется запись, символические ссылки/reparse points и файлы с несколькими
жёсткими ссылками отклоняются. Проверка исправного файла по ссылке возможна без записи.
- `secure_channel.h` — `sc_generate_keypair()`, `sc_compute_public_key_from_private()`, `sc_derive_node_id()`
- `config_parser.h` — `parse_config_from_buf()`, `free_config()`, структуры `utun_config`/`global_config`
- `lib/mem.h` — `u_malloc`, `u_realloc`, `u_free`
- `lib/debug_config.h` — `DEBUG_*`
Логи показывают причину генерации, изменяемые поля, этап ошибки и факт замены файла.
Значения приватных ключей updater не выводит.
## Вспомогательная функция
bytes_to_hex(bytes, len, hex_str, hex_len) записывает 2*len строчных HEX-символов и NUL.
Буферы не перекрываются. bytes и hex_str обязательны; hex_len задаёт ёмкость результата.
При недостаточной ёмкости или NULL функция обнуляет доступный hex_str[0]; усечения нет.
Проверка ёмкости не вычисляет переполняющееся 2*len+1. При len=0 результат пустой.
## Проверки
tests/test_config_updater.c проверяет сохранение идентичности, регистр, пробелы,
секции и дубли, CRLF, повторный вызов, строгий HEX/списки, длинные строки/NUL, ссылки,
права доступа, ошибки файловых операций и отказы памяти. Внедрение ошибок через
Linux/ELF linker wrappers не добавляет тестовых веток в рабочий код.
tests/test_reality_config.c проверяет базовые сценарии генерации REALITY.
+82 -31
View File
@@ -377,7 +377,6 @@ static void accept_callback(socket_t fd, void* arg) {
#endif
return;
}
DEBUG_WARN(DEBUG_CATEGORY_CONTROL, "Accept...");
/* Check max clients */
if (server->client_count >= server->max_clients) {
@@ -429,7 +428,6 @@ static void accept_callback(socket_t fd, void* arg) {
}
client->subscribed_nodes = 0;
client->write_registered = 0;
client->idle_timer = uasync_set_timeout(server->ua, 300000, client, control_client_idle_cb, "ctrl_idle");
/* Register with uasync */
client->socket_id = uasync_add_socket_t(server->ua, client_fd,
@@ -446,6 +444,7 @@ static void accept_callback(socket_t fd, void* arg) {
fflush(server->log_file);
}
DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Failed to register client socket with uasync");
queue_free(client->output_queue);
u_free(client);
#ifdef _WIN32
closesocket(client_fd);
@@ -460,6 +459,13 @@ static void accept_callback(socket_t fd, void* arg) {
server->clients = client;
server->client_count++;
client->idle_timer = uasync_set_timeout(server->ua, 300000, client, control_client_idle_cb, "ctrl_idle");
if (!client->idle_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Failed to arm control client idle timer fd=%d", (int)client_fd);
close_client(server, client);
return;
}
const char* addr_str;
if (client_addr.ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)&client_addr;
@@ -587,7 +593,7 @@ static void client_write_callback(socket_t fd, void* arg) {
struct ll_entry* entry = q->head;
#ifdef _WIN32
int sent = send(fd, (const char*)entry->dgram, entry->len, 0);
int sent = send(fd, (const char*)entry->dgram + client->output_offset, entry->len - client->output_offset, 0);
if (sent == SOCKET_ERROR) {
int err = WSAGetLastError();
if (err != WSAEWOULDBLOCK) {
@@ -598,7 +604,7 @@ static void client_write_callback(socket_t fd, void* arg) {
return;
}
#else
ssize_t sent = send(fd, entry->dgram, entry->len, 0);
ssize_t sent = send(fd, entry->dgram + client->output_offset, entry->len - client->output_offset, 0);
if (sent < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Client send error: %s, closing", strerror(errno));
@@ -608,10 +614,21 @@ static void client_write_callback(socket_t fd, void* arg) {
return;
}
#endif
if ((size_t)sent < entry->len) { entry->dgram += sent; entry->len -= (uint16_t)sent; q->total_bytes -= (size_t)sent; return; }
if (sent == 0) {
DEBUG_WARN(DEBUG_CATEGORY_CONTROL, "Control client send made no progress fd=%d, closing", (int)fd);
close_client(server, client);
return;
}
client->output_offset += (uint16_t)sent;
if (client->output_offset < entry->len) {
DEBUG_DEBUG(DEBUG_CATEGORY_CONTROL, "Control partial send fd=%d offset=%u size=%u queued_bytes=%zu",
(int)fd, client->output_offset, entry->len, queue_total_bytes(q));
return;
}
struct ll_entry* e = queue_data_get(q);
queue_dgram_free(e); queue_entry_free(e);
client->output_offset = 0;
}
uasync_set_socket_write(server->ua, client->socket_id, 0);
@@ -631,14 +648,22 @@ static void client_except_callback(socket_t fd, void* arg) {
(void)fd;
}
/* Принимает владение буфером, включая ветки ошибки постановки в очередь. */
static int control_client_send(struct control_server* server, struct control_client* client, uint8_t* dgram, uint16_t len) {
(void)server;
if (!client || !client->output_queue || !dgram || len == 0) return -1;
if (!client || !client->output_queue || !dgram || len == 0 || len > ETCPMON_MAX_MSG_SIZE) {
DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Invalid control output message client=%p size=%u", (void*)client, len);
u_free(dgram);
return -1;
}
struct ll_entry* entry = queue_entry_new(0);
if (!entry) { DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Failed to allocate output queue entry"); return -1; }
if (!entry) {
DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Failed to allocate output queue entry size=%u", len);
u_free(dgram);
return -1;
}
entry->dgram = dgram;
entry->len = len;
entry->int_len = len;
queue_data_put(client->output_queue, entry);
if (!client->write_registered) {
uasync_set_socket_write(client->server->ua, client->socket_id, 1);
@@ -657,6 +682,10 @@ static void control_client_idle_cb(void* arg) {
static void close_client(struct control_server* server, struct control_client* client) {
if (!server || !client) return;
DEBUG_DEBUG(DEBUG_CATEGORY_CONTROL, "Closing control client fd=%d queued_packets=%d queued_bytes=%zu output_offset=%u timer=%d",
(int)client->fd, queue_entry_count(client->output_queue), queue_total_bytes(client->output_queue),
client->output_offset, client->idle_timer != NULL);
if (client->idle_timer) { uasync_cancel_timeout(server->ua, client->idle_timer); client->idle_timer = NULL; }
@@ -680,7 +709,15 @@ static void close_client(struct control_server* server, struct control_client* c
}
#endif
if (client->output_queue) { queue_free(client->output_queue); client->output_queue = NULL; }
if (client->output_queue) {
struct ll_entry* entry;
while ((entry = queue_data_get(client->output_queue))) {
queue_dgram_free(entry);
queue_entry_free(entry);
}
queue_free(client->output_queue);
client->output_queue = NULL;
}
/* Remove from list */
struct control_client** curr = &server->clients;
@@ -822,25 +859,29 @@ static void handle_client_data(struct control_server* server, struct control_cli
send_debug_config(server, client, req_seq);
break;
case ETCPMON_CMD_SET_DEBUG_CONFIG:
if (payload_size >= 2) {
uint8_t global_lvl = payload[0];
uint8_t cat_count = payload[1];
if (cat_count > ETCPMON_MAX_DEBUG_CATEGORIES) cat_count = ETCPMON_MAX_DEBUG_CATEGORIES;
if (global_lvl <= DEBUG_LEVEL_TRACE) debug_set_level((debug_level_t)global_lvl);
const uint8_t* levels = payload + 2;
for (uint8_t i = 0; i < cat_count && i + 1 < DEBUG_CATEGORY_COUNT; i++) {
if (levels[i] <= DEBUG_LEVEL_TRACE)
debug_set_category_level((debug_category_t)(i + 1), (debug_level_t)levels[i]);
}
DEBUG_INFO(DEBUG_CATEGORY_CONTROL, "Debug config applied: global=%d categories=%d", global_lvl, cat_count);
if (server->log_file) {
fprintf(server->log_file, "%llu: [LOG] Debug config set: global=%d categories=%d\n",
(unsigned long long)get_timestamp_ms(), global_lvl, cat_count);
fflush(server->log_file);
}
case ETCPMON_CMD_SET_DEBUG_CONFIG: {
if (payload_size < 2 || payload[1] > ETCPMON_MAX_DEBUG_CATEGORIES || payload_size != 2u + payload[1]) {
DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Bad SET_DEBUG_CONFIG payload size=%u categories=%d",
payload_size, payload_size >= 2 ? payload[1] : -1);
close_client(server, client);
return;
}
uint8_t global_lvl = payload[0];
uint8_t cat_count = payload[1];
if (global_lvl <= DEBUG_LEVEL_TRACE) debug_set_level((debug_level_t)global_lvl);
const uint8_t* levels = payload + 2;
for (uint8_t i = 0; i < cat_count && i + 1 < DEBUG_CATEGORY_COUNT; i++) {
if (levels[i] <= DEBUG_LEVEL_TRACE)
debug_set_category_level((debug_category_t)(i + 1), (debug_level_t)levels[i]);
}
DEBUG_INFO(DEBUG_CATEGORY_CONTROL, "Debug config applied: global=%d categories=%d", global_lvl, cat_count);
if (server->log_file) {
fprintf(server->log_file, "%llu: [LOG] Debug config set: global=%d categories=%d\n",
(unsigned long long)get_timestamp_ms(), global_lvl, cat_count);
fflush(server->log_file);
}
break;
}
case ETCPMON_CMD_SUBSCRIBE_NODES:
client->subscribed_nodes = 1;
@@ -871,6 +912,11 @@ static void handle_client_data(struct control_server* server, struct control_cli
/* Reset idle timer on each valid command */
if (client->idle_timer) { uasync_cancel_timeout(server->ua, client->idle_timer); }
client->idle_timer = uasync_set_timeout(server->ua, 300000, client, control_client_idle_cb, "ctrl_idle");
if (!client->idle_timer) {
DEBUG_ERROR(DEBUG_CATEGORY_CONTROL, "Failed to reset control client idle timer fd=%d", (int)client->fd);
close_client(server, client);
return;
}
/* Remove processed message from buffer */
uint16_t msg_size = hdr->size;
@@ -957,12 +1003,18 @@ static void send_socket_list(struct control_server* server, struct control_clien
uint8_t count = 0;
struct ETCP_SOCKET* sock = instance->etcp_sockets;
while (sock && count < ETCPMON_MAX_CONNECTIONS) {
if (!sock->probe_only) count++;
if (!sock->probe_only) {
if (ETCPMON_SOCKET_LIST_SIZE(count + 1u) > ETCPMON_MAX_MSG_SIZE) {
DEBUG_WARN(DEBUG_CATEGORY_CONTROL, "Socket list exceeds protocol limit: count>%u next_size=%zu max_size=%u",
count, ETCPMON_SOCKET_LIST_SIZE(count + 1u), ETCPMON_MAX_MSG_SIZE);
send_error(client, ETCPMON_ERR_SERVER_BUSY, "Socket list exceeds protocol limit", seq_id);
return;
}
count++;
}
sock = sock->next;
}
if (count > ETCPMON_MAX_CONNECTIONS) count = ETCPMON_MAX_CONNECTIONS;
uint16_t rsp_size = ETCPMON_SOCKET_LIST_SIZE(count);
uint8_t* buffer = (uint8_t*)u_malloc(rsp_size);
if (!buffer) {
@@ -1241,6 +1293,7 @@ static void send_metrics(struct control_server* server, struct control_client* c
struct etcpmon_link_metrics* link_info = (struct etcpmon_link_metrics*)(buffer + sizeof(*hdr) + sizeof(*rsp));
link = conn->links;
for (uint8_t i = 0; i < links_count && link; i++) {
memset(&link_info[i], 0, sizeof(link_info[i]));
link_info[i].local_link_id = link->local_link_id;
link_info[i].status = link->link_status;
link_info[i].encrypt_errors = (uint32_t)link->encrypt_errors;
@@ -1278,8 +1331,6 @@ static void send_metrics(struct control_server* server, struct control_client* c
uint32_t max_bw = link->bbr->bw_hi[0] > link->bbr->bw_hi[1] ? link->bbr->bw_hi[0] : link->bbr->bw_hi[1];
link_info[i].bbr_bw_hi = max_bw ? (uint32_t)((uint64_t)max_bw * link->mtu * 1000000ULL / 16777216ULL) : 0;
link_info[i].bbr_bw_lo = link->bbr->bw_lo != ~0U ? (uint32_t)((uint64_t)link->bbr->bw_lo * link->mtu * 1000000ULL / 16777216ULL) : 0;
} else {
memset(&link_info[i].bbr_mode, 0, 11 * 4 + 2 + 2); // zero out all BBR fields
}
link = link->next;
}
+5 -5
View File
@@ -1,7 +1,8 @@
/*
* control_server.h - Control Socket Server for ETCP Monitoring
*
* Provides TCP control interface for ETCP connection monitoring
* TCP-мониторинг ETCP: init → обработка команд в uasync-потоке → shutdown.
* Метрики отправляются по запросу; доступ — по IPv4 control_allow, idle timeout — 30 секунд.
*/
#ifndef CONTROL_SERVER_H
@@ -40,6 +41,7 @@ struct control_client {
uint64_t selected_peer_id; /* 0 = none selected */
struct ll_queue* output_queue; /* Output queue for async sends */
uint16_t output_offset; /* Отправленные байты головы очереди; dgram/len не меняются */
void* idle_timer; /* Client idle timeout */
uint8_t subscribed_nodes; /* 1 = subscribed to node updates */
uint8_t write_registered; /* 1 = write events registered in uasync */
@@ -97,10 +99,8 @@ int control_server_init(struct control_server* server,
*/
void control_server_shutdown(struct control_server* server);
/* Process periodic updates
*
* Should be called periodically (e.g., from main loop) to send
* metrics updates to clients that have selected connections
/* Разобрать накопленные команды всех клиентов в uasync-потоке.
* Чтение сокета также запускает разбор; метрики отправляются только по CMD_GET_METRICS.
*
* Parameters:
* server - Server state
+10 -4
View File
@@ -10,9 +10,15 @@ TCP-сервер для мониторинга ETCP-соединений. При
4. При остановке вызвать `control_server_shutdown()`.
**Нюансы:**
- Доступ по IP контролируется через `control_allow` в конфиге (белый список); без правил доступ запрещён.
- Клиенты отключаются по idle-таймауту (300 секунд без команд).
- Лог пишется в `control_server.log` (полные дампы RX/TX).
- Доступ разрешён только IPv4-клиентам по `control_allow` в конфиге; без правил доступ запрещён. IPv6-клиенты отклоняются.
- Клиенты отключаются по idle-таймауту (30 секунд без команд). Каждая обработанная команда перезапускает таймер.
- Лог пишется в `control_server.log`: дампы содержат первые 256 байт каждого чтения RX и некоторых ответов TX.
- `control_server_process_updates()` разбирает команды; метрики отправляются только по `CMD_GET_METRICS`.
- Очередь отправки владеет исходными буферами до полной отправки или отключения; частичная отправка хранит отдельное смещение.
- Если список сокетов превышает предел сообщения (32 768 байт; сейчас помещается 230 записей),
возвращается `RSP_ERROR` с кодом `SERVER_BUSY`. Список не усекается; сокеты `probe_only` не учитываются.
- `SET_DEBUG_CONFIG` требует ровно `2 + category_count` байт, не более 64 категорий.
Несоответствие длины закрывает соединение без применения настроек.
## 3. API
@@ -42,7 +48,7 @@ TCP-сервер для мониторинга ETCP-соединений. При
| `CMD_SELECT_CONN` | — | Выбрать соединение по `peer_node_id` для метрик. |
| `CMD_GET_METRICS` | `RSP_METRICS` | Полные метрики: ETCP, TUN, router, все каналы с BBR. |
| `CMD_LIST_SOCKETS` | `RSP_SOCKET_LIST` | Список локальных сокетов с NAT-типом и NAT-адресом. |
| `CMD_ACTION` | `RSP_ACTION_RESULT` | `"nat"` — запрос NAT-проверки; `"nodes"` — дамп BGP-узлов текстом. |
| `CMD_ACTION` | Для `"nodes"` — `RSP_ACTION_RESULT`; для `"nat"` — без ответа | `"nat"` — запрос NAT-проверки; `"nodes"` — дамп BGP-узлов текстом. |
| `CMD_GET_DEBUG_CONFIG` | `RSP_DEBUG_CONFIG` | Текущие уровни отладки (глобальный + по категориям). |
| `CMD_SET_DEBUG_CONFIG` | — | Установить уровни отладки. |
| `CMD_SUBSCRIBE_NODES` | `RSP_NODE_INFO` (список) + `RSP_NODE_INFO_END` | Подписка на изменения узлов + текущий список. |
+7 -3
View File
@@ -80,7 +80,8 @@ int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
/* Отправить сообщение в беседу. Шифрует, подписывает, сохраняет локально и
* доставляет (прямое соединение при доступности пира, иначе mailbox).
* conv_id — строковое представление (как в dm_conversations.conv_id).
* Для ct="file" data=[descriptor:56][basename:utf8], только после подготовки dm_media.
* Для ct="file" data=[descriptor:56][attachment_info в формате attachment_encode()], после подготовки dm_media.
* Descriptor подписывается открыто, метаданные вложения шифруются; формат — media_async/attachment.h.
* Для ct="invite" data=[group_name:utf8][NUL][полная ссылка:utf8], без завершающего NUL.
* Имя и ссылка шифруются и подписываются вместе; GUI получает data=ссылка, tags.group_name=имя. */
int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content_type,
@@ -94,8 +95,11 @@ void dm_cancel_pending_history(struct UTUN_INSTANCE* inst, const char* conv);
/* Список бесед в JSON: [{conv_id,peer_node_id,peer_name,last_ts,out_seq,in_seq}] */
int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len);
/* Сообщения беседы в JSON (расшифрованные): [{dir,seq,ts,author,ct,data}]
* count=0 → все, offset=0 → с начала. */
/* Сообщения беседы в JSON: расшифрованное содержимое, статусы доставки и медиа.
* count=1..200 задаёт предел; count<=0 или count>200 означает 200.
* offset>=0 отсчитывается от новых сообщений; offset=0 выбирает последние count.
* Выбранная страница возвращается по возрастанию ts, seq, dir.
* buf — выходной буфер с NUL; 0 и *out_len байт без NUL при успехе, -1 при ошибке. */
int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len);
+7 -6
View File
@@ -1,10 +1,9 @@
/*
* dm_crypto.h — криптография прямого чата (DM)
*
* Детерминированная идентичность и E2E-шифрование:
* - conv_id / content_key выводятся обеими сторонами из своих ключей и
* pubkey пира (известен из общей группы) — без переговоров.
* - шифрование AES-256-CCM с детерминированным nonce из (dir, seq).
* conv_id выводится из ID двух узлов, content_key — из своего X25519-ключа и
* публичного ключа пира. Переговоров нет; nonce сообщения зависит от conv_id/author/seq.
* CCM и файловые примитивы предоставляет общий media_crypto.h.
*/
#ifndef DM_CRYPTO_H
@@ -25,8 +24,10 @@ extern "C" {
#define DM_CCM_PLAIN_MAX 65535u
#define DM_MEDIA_PLAIN_SIZE 32768u
/* conv_id = SHA256("utun_dm_v1" || min(a,b) || max(a,b))[..8] & 0x7FFF...,
* 63-битное число, одинаковое на обеих сторонах (одна ветка на пару). */
/* conv_id: SHA256("utun_dm_v" || min(a,b) || max(a,b)), префикс — 9 ASCII-байт без NUL.
* ID узлов хешируются в порядке байтов хоста; первые 8 байт SHA256 читаются как uint64_t того же порядка.
* Снимается старший бит (маска 0x7FFFFFFFFFFFFFFF), нулевой результат заменяется на 1.
* Положительное 63-битное число, одинаковое для обоих направлений пары узлов. */
uint64_t dm_derive_conv_id(uint64_t a, uint64_t b);
/* content_key = SHA256(X25519(my_priv, peer_pub) || "utun_dm_content").
+218 -72
View File
@@ -2,6 +2,7 @@
#include "config_parser.h"
#include "../lib/debug_config.h"
#include "../lib/mem.h"
#include "../lib/u_async.h"
#include <string.h>
#include <stdlib.h>
@@ -33,10 +34,11 @@ static ip_str_t ip_host_to_str(uint32_t ip_host) {
return ip_to_str(&a, AF_INET);
}
// Сложение с дополнением старых слов сохраняет end-around carry и не теряет заём.
static uint16_t csum_update_n(uint16_t old_csum, const uint16_t* old_vals,
const uint16_t* new_vals, int n) {
uint32_t sum = (uint32_t)(uint16_t)(~old_csum);
for (int i = 0; i < n; i++) { sum -= old_vals[i]; sum += new_vals[i]; }
for (int i = 0; i < n; i++) sum += (uint16_t)~old_vals[i] + new_vals[i];
sum = (sum & 0xFFFF) + (sum >> 16); sum = (sum & 0xFFFF) + (sum >> 16);
return (uint16_t)(~sum);
}
@@ -54,10 +56,12 @@ static void csum_update_transport(uint8_t* transport, int csum_off,
uint32_t old_ip_host, uint32_t new_ip_host,
uint16_t old_port_net, uint16_t new_port_net) {
uint16_t old_csum; memcpy(&old_csum, transport + csum_off, 2);
if (csum_off == UDP_CHECKSUM_OFFSET && old_csum == 0) return; // IPv4 UDP: проверка отключена отправителем.
uint32_t old_ip_net = htonl(old_ip_host), new_ip_net = htonl(new_ip_host);
uint16_t old_w[3] = {(uint16_t)(old_ip_net & 0xFFFF), (uint16_t)(old_ip_net >> 16), old_port_net};
uint16_t new_w[3] = {(uint16_t)(new_ip_net & 0xFFFF), (uint16_t)(new_ip_net >> 16), new_port_net};
uint16_t new_csum = csum_update_n(old_csum, old_w, new_w, 3);
if (csum_off == UDP_CHECKSUM_OFFSET && new_csum == 0) new_csum = 0xffff;
memcpy(transport + csum_off, &new_csum, 2);
}
@@ -67,43 +71,100 @@ static void csum_update_icmp(uint8_t* icmp, uint16_t old_word, uint16_t new_word
memcpy(icmp + ICMP_CHECKSUM_OFFSET, &new_csum, 2);
}
// Смешать все поля ключа численно: padding и порядок полей C-структуры не участвуют.
static uint16_t eim_nat_bucket(uint8_t proto, uint32_t ip, uint16_t port, uint64_t node) {
uint64_t value = node ^ ((uint64_t)ip << 24) ^ ((uint64_t)port << 8) ^ proto;
value ^= value >> 30; value *= UINT64_C(0xbf58476d1ce4e5b9);
value ^= value >> 27; value *= UINT64_C(0x94d049bb133111eb);
value ^= value >> 31;
return (uint16_t)value;
}
// Исходящий поиск по хешу; сравнение полного ключа разрешает любые коллизии.
static struct eim_nat_entry* eim_nat_find_egress(struct eim_nat_ctx* ctx,
uint8_t proto, uint32_t ip_host, uint16_t port_net) {
for (uint16_t i = ctx->port_start; i <= ctx->port_end; i++) {
struct eim_nat_entry* e = &ctx->table[i];
if (e->state != EIM_NAT_ENTRY_FREE && e->proto == proto &&
uint8_t proto, uint32_t ip_host, uint16_t port_net, uint64_t src_node_id) {
uint16_t slot = ctx->buckets[eim_nat_bucket(proto, ip_host, port_net, src_node_id)];
while (slot) {
struct eim_nat_entry* e = &ctx->table[slot];
if (e->proto == proto && e->src_node_id == src_node_id &&
e->internal_ip == ip_host && e->internal_port == port_net) return e;
slot = e->hash_next;
}
return NULL;
}
static uint16_t eim_nat_alloc_port(struct eim_nat_ctx* ctx) {
uint16_t start = ctx->next_port;
do {
if (ctx->table[ctx->next_port].state == EIM_NAT_ENTRY_FREE) {
uint16_t port = ctx->next_port;
ctx->next_port++;
if (ctx->next_port > ctx->port_end || ctx->next_port < ctx->port_start) ctx->next_port = ctx->port_start;
return port;
}
ctx->next_port++;
if (ctx->next_port > ctx->port_end || ctx->next_port < ctx->port_start) ctx->next_port = ctx->port_start;
} while (ctx->next_port != start);
return 0;
// Единственная публикация записи: резервировать свободный порт за O(1), затем включить в хеш.
static struct eim_nat_entry* eim_nat_insert(struct eim_nat_ctx* ctx, uint16_t external_port, uint8_t state,
uint8_t proto, uint32_t ip, uint16_t port, uint64_t node, struct ETCP_CONN* conn) {
struct eim_nat_entry* e = &ctx->table[external_port];
if (external_port >= ctx->port_start && external_port <= ctx->port_end) {
if (e->free_prev) ctx->table[e->free_prev].free_next = e->free_next;
else ctx->next_port = e->free_next;
if (e->free_next) ctx->table[e->free_next].free_prev = e->free_prev;
else ctx->free_tail = e->free_prev;
ctx->free_count--;
}
memset(e, 0, sizeof(*e));
e->internal_ip = ip; e->internal_port = port; e->proto = proto; e->state = state;
e->src_node_id = node; e->src_conn = conn; e->last_seen = get_time_tb();
uint16_t bucket = eim_nat_bucket(proto, ip, port, node);
e->hash_next = ctx->buckets[bucket]; ctx->buckets[bucket] = external_port;
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT mapping published: external=%u node=%016llx proto=%u state=%u free=%u",
external_port, (unsigned long long)node, proto, state, ctx->free_count);
return e;
}
// ==================== Public API ====================
// Проверить полный IPv4 и границы транспорта до изменения пакета/таблицы. memcpy допускает любой alignment.
int eim_nat_validate_packet(const uint8_t* ip, size_t len, uint16_t* header_len) {
if (!header_len || !ip || len < IP_HDR_MIN_SIZE) {
DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT invalid packet: data=%p bytes=%zu header=%p", (const void*)ip, len, (void*)header_len);
return EIM_NAT_ERROR;
}
*header_len = (ip[0] & 15) * 4;
uint16_t total, fragment;
memcpy(&total, ip + 2, 2); memcpy(&fragment, ip + 6, 2);
if ((ip[0] >> 4) != 4 || *header_len < 20 || *header_len > len || ntohs(total) != len) {
DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT malformed IPv4: version=%u ihl=%u total=%u bytes=%zu", ip[0] >> 4, *header_len, ntohs(total), len);
return EIM_NAT_ERROR;
}
if (ntohs(fragment) & (IP_FRAG_MF_MASK | IP_FRAG_OFF_MASK)) return EIM_NAT_SKIP;
const uint8_t* transport = ip + *header_len;
size_t remaining = len - *header_len;
int valid;
switch (ip[9]) {
case IPPROTO_TCP_UINT8:
valid = remaining >= 20 && (transport[12] >> 4) >= 5 && (size_t)(transport[12] >> 4) * 4 <= remaining;
break;
case IPPROTO_UDP_UINT8:
if (remaining < 8) { valid = 0; break; }
memcpy(&total, transport + 4, 2);
valid = ntohs(total) >= 8 && ntohs(total) <= remaining;
break;
case IPPROTO_ICMP_UINT8:
if (remaining < 8) { valid = 0; break; }
if (transport[0] != ICMP_ECHO_REQUEST && transport[0] != ICMP_ECHO_REPLY) return EIM_NAT_SKIP;
valid = transport[1] == 0;
break;
default: return EIM_NAT_SKIP;
}
if (!valid) {
DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT malformed transport: proto=%u header=%u bytes=%zu", ip[9], *header_len, len);
return EIM_NAT_ERROR;
}
return EIM_NAT_OK;
}
int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
uint64_t src_node_id, struct ETCP_CONN* src_conn) {
if (ip_len < IP_HDR_MIN_SIZE) return -1;
uint8_t ihl = ip_data[IP_IHL_OFFSET] & 0x0F;
if (ihl < 5) return -1;
uint16_t ip_hdr_len = ihl * 4;
if (ip_len < ip_hdr_len) return -1;
uint16_t frag_off = ntohs(*(uint16_t*)(ip_data + 6));
if ((frag_off & (IP_FRAG_MF_MASK | IP_FRAG_OFF_MASK)) != 0) return 0;
if (!ctx || !ctx->initialized || !ctx->table || !ctx->buckets) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT egress requires initialized context");
return EIM_NAT_ERROR;
}
uint16_t ip_hdr_len;
int parsed = eim_nat_validate_packet(ip_data, ip_len, &ip_hdr_len);
if (parsed != EIM_NAT_OK) return parsed;
uint8_t proto = ip_data[IP_PROTO_OFFSET];
uint32_t src_ip_net, src_ip_host;
@@ -125,33 +186,27 @@ int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
is_icmp_echo = 1; memcpy(&src_port_net, transport + ICMP_ID_OFFSET, 2);
}
}
if (!is_tcp && !is_udp && !is_icmp_echo) return 0;
if (!is_tcp && !is_udp && !is_icmp_echo) return EIM_NAT_SKIP;
struct eim_nat_entry* entry = eim_nat_find_egress(ctx, proto, src_ip_host, src_port_net);
struct eim_nat_entry* entry = eim_nat_find_egress(ctx, proto, src_ip_host, src_port_net, src_node_id);
uint16_t ext_port_host;
if (entry) {
ext_port_host = (uint16_t)(entry - ctx->table);
} else {
ext_port_host = eim_nat_alloc_port(ctx);
ext_port_host = ctx->next_port;
if (ext_port_host == 0) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT port range exhausted (%s:%u proto=%u)",
ip_host_to_str(src_ip_host).str, ntohs(src_port_net), proto);
return -1;
}
entry = &ctx->table[ext_port_host];
entry->internal_ip = src_ip_host;
entry->internal_port = src_port_net;
entry->proto = proto;
entry->state = EIM_NAT_ENTRY_ACTIVE;
entry->src_node_id = src_node_id;
entry->src_conn = src_conn;
entry->last_seen = 0;
entry = eim_nat_insert(ctx, ext_port_host, EIM_NAT_ENTRY_ACTIVE, proto, src_ip_host, src_port_net, src_node_id, src_conn);
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT map: %s:%u -> %s:%u (proto=%u) from node %016llx",
ip_host_to_str(src_ip_host).str, ntohs(src_port_net),
ip_host_to_str(ctx->gateway_ip).str, ext_port_host, proto,
(unsigned long long)src_node_id);
}
entry->last_seen = get_time_tb();
uint32_t gw_ip_net = htonl(ctx->gateway_ip);
memcpy(ip_data + IP_SRC_ADDR_OFFSET, &gw_ip_net, 4);
csum_update_ip(ip_data, src_ip_host, ctx->gateway_ip);
@@ -172,19 +227,19 @@ int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
int eim_nat_ingress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
struct eim_nat_entry** out_entry) {
if (ip_len < IP_HDR_MIN_SIZE) return -1;
uint8_t ihl = ip_data[IP_IHL_OFFSET] & 0x0F;
if (ihl < 5) return -1;
uint16_t ip_hdr_len = ihl * 4;
if (ip_len < ip_hdr_len) return -1;
uint16_t frag_off = ntohs(*(uint16_t*)(ip_data + 6));
if ((frag_off & (IP_FRAG_MF_MASK | IP_FRAG_OFF_MASK)) != 0) return 0;
if (out_entry) *out_entry = NULL;
if (!ctx || !ctx->initialized || !ctx->table || !ctx->buckets) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT ingress requires initialized context");
return EIM_NAT_ERROR;
}
uint16_t ip_hdr_len;
int parsed = eim_nat_validate_packet(ip_data, ip_len, &ip_hdr_len);
if (parsed != EIM_NAT_OK) return parsed;
uint8_t proto = ip_data[IP_PROTO_OFFSET];
uint32_t dst_ip_net;
memcpy(&dst_ip_net, ip_data + IP_DST_ADDR_OFFSET, 4);
if (dst_ip_net != htonl(ctx->gateway_ip)) return 0;
if (dst_ip_net != htonl(ctx->gateway_ip)) return EIM_NAT_SKIP;
uint8_t* transport = ip_data + ip_hdr_len;
size_t tlen = ip_len - ip_hdr_len;
@@ -201,14 +256,12 @@ int eim_nat_ingress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
is_icmp_echo = 1; memcpy(&dst_port_net, transport + ICMP_ID_OFFSET, 2);
}
}
if (!is_tcp && !is_udp && !is_icmp_echo) return 0;
if (!is_tcp && !is_udp && !is_icmp_echo) return EIM_NAT_SKIP;
uint16_t ext_port_host = ntohs(dst_port_net);
if (ext_port_host < ctx->port_start || ext_port_host > ctx->port_end) return 0;
struct eim_nat_entry* entry = &ctx->table[ext_port_host];
if (entry->state == EIM_NAT_ENTRY_FREE) return 0;
if (entry->proto != proto) return 0;
if (entry->state == EIM_NAT_ENTRY_FREE || entry->proto != proto) return EIM_NAT_SKIP;
entry->last_seen = get_time_tb();
uint32_t dst_ip_host = ntohl(dst_ip_net);
uint32_t internal_ip_net = htonl(entry->internal_ip);
@@ -260,47 +313,140 @@ int eim_nat_init_ctx(struct eim_nat_ctx* ctx, const struct global_config* g) {
return -1;
}
ctx->next_port = ctx->port_start;
if (g->nat_forward_count < 0 || g->nat_forward_count > MAX_NAT_FORWARDS) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Invalid NAT forward count: %d", g->nat_forward_count);
return -1;
}
ctx->table_size = EIM_NAT_TABLE_SIZE;
ctx->table = u_calloc(ctx->table_size, sizeof(struct eim_nat_entry));
if (!ctx->table) { DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Failed to allocate NAT table"); return -1; }
ctx->buckets = u_calloc(EIM_NAT_TABLE_SIZE, sizeof(uint16_t));
if (!ctx->buckets) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Failed to allocate NAT hash buckets");
u_free(ctx->table); memset(ctx, 0, sizeof(*ctx));
return -1;
}
ctx->free_tail = ctx->port_end;
ctx->free_count = (uint32_t)ctx->port_end - ctx->port_start + 1;
ctx->gc_cursor = 1;
for (uint32_t i = ctx->port_start; i <= ctx->port_end; i++) {
ctx->table[i].free_prev = i == ctx->port_start ? 0 : i - 1;
ctx->table[i].free_next = i == ctx->port_end ? 0 : i + 1;
}
ctx->initialized = 1;
for (int i = 0; i < g->nat_forward_count; i++) {
uint16_t ext = g->nat_forwards[i].external_port;
if (ext >= ctx->table_size) continue;
struct eim_nat_entry* e = &ctx->table[ext];
e->internal_ip = g->nat_forwards[i].internal_ip_host;
e->internal_port = g->nat_forwards[i].internal_port_net;
e->proto = g->nat_forwards[i].proto;
e->state = EIM_NAT_ENTRY_STATIC;
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Port forward: %s:%u <- :%u (proto=%u)",
ip_host_to_str(e->internal_ip).str, ntohs(e->internal_port), ext, e->proto);
if (eim_nat_add_forward(ctx, g->nat_forwards[i].proto, g->nat_forwards[i].internal_ip_host,
g->nat_forwards[i].internal_port_net, g->nat_forwards[i].external_port, g->nat_forwards[i].node_id) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Invalid NAT forward at index %d; initialization aborted", i);
eim_nat_destroy_ctx(ctx);
return -1;
}
}
ctx->initialized = 1;
DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT engine initialized: gw=%s ports=%u-%u",
ip_host_to_str(ctx->gateway_ip).str, ctx->port_start, ctx->port_end);
DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT engine initialized: gw=%s ports=%u-%u free=%u hash_buckets=%u bytes=%zu",
ip_host_to_str(ctx->gateway_ip).str, ctx->port_start, ctx->port_end, ctx->free_count, EIM_NAT_TABLE_SIZE,
ctx->table_size * sizeof(struct eim_nat_entry) + EIM_NAT_TABLE_SIZE * sizeof(uint16_t));
return 0;
}
void eim_nat_destroy_ctx(struct eim_nat_ctx* ctx) {
if (!ctx || !ctx->initialized) return;
u_free(ctx->table);
u_free(ctx->buckets);
memset(ctx, 0, sizeof(*ctx));
DEBUG_INFO(DEBUG_CATEGORY_NAT, "NAT engine destroyed");
}
int eim_nat_add_forward(struct eim_nat_ctx* ctx, uint8_t proto,
uint32_t internal_ip_host, uint16_t internal_port_net,
uint16_t external_port) {
if (!ctx || !ctx->initialized || external_port >= ctx->table_size) return -1;
if (ctx->table[external_port].state != EIM_NAT_ENTRY_FREE) return -1;
uint16_t external_port, uint64_t node_id) {
if (!ctx || !ctx->initialized || !node_id || !external_port || !internal_port_net ||
(proto != IPPROTO_TCP_UINT8 && proto != IPPROTO_UDP_UINT8) || external_port >= ctx->table_size) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Invalid NAT forward: ctx=%p node=%016llx proto=%u internal_port=%u external_port=%u",
(void*)ctx, (unsigned long long)node_id, proto, ntohs(internal_port_net), external_port);
return -1;
}
if (ctx->table[external_port].state != EIM_NAT_ENTRY_FREE ||
eim_nat_find_egress(ctx, proto, internal_ip_host, internal_port_net, node_id)) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT forward conflicts with existing mapping: node=%016llx %s:%u <- :%u proto=%u",
(unsigned long long)node_id, ip_host_to_str(internal_ip_host).str, ntohs(internal_port_net), external_port, proto);
return -1;
}
eim_nat_insert(ctx, external_port, EIM_NAT_ENTRY_STATIC, proto, internal_ip_host, internal_port_net, node_id, NULL);
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Port forward: node=%016llx %s:%u <- :%u (proto=%u)",
(unsigned long long)node_id, ip_host_to_str(internal_ip_host).str, ntohs(internal_port_net), external_port, proto);
return 0;
}
// Сначала удалить из хеша, затем очистить запись и вернуть динамический порт в хвост свободного списка.
int eim_nat_remove(struct eim_nat_ctx* ctx, uint16_t external_port) {
if (!ctx || !ctx->initialized || !ctx->table || !ctx->buckets || !external_port) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Invalid NAT removal: ctx=%p port=%u", (void*)ctx, external_port);
return -1;
}
struct eim_nat_entry* e = &ctx->table[external_port];
e->internal_ip = internal_ip_host;
e->internal_port = internal_port_net;
e->proto = proto;
e->state = EIM_NAT_ENTRY_STATIC;
DEBUG_INFO(DEBUG_CATEGORY_NAT, "Port forward: %s:%u <- :%u (proto=%u)",
ip_host_to_str(internal_ip_host).str, ntohs(internal_port_net), external_port, proto);
if (e->state == EIM_NAT_ENTRY_FREE) return 0;
if (e->holds) {
DEBUG_WARN(DEBUG_CATEGORY_NAT, "NAT mapping is held by pending transmission: port=%u holds=%u", external_port, e->holds);
return -1;
}
uint16_t* link = &ctx->buckets[eim_nat_bucket(e->proto, e->internal_ip, e->internal_port, e->src_node_id)];
while (*link && *link != external_port) link = &ctx->table[*link].hash_next;
if (!*link) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "NAT removal: missing hash entry port=%u", external_port);
return -1;
}
*link = e->hash_next;
memset(e, 0, sizeof(*e));
if (external_port >= ctx->port_start && external_port <= ctx->port_end) {
e->free_prev = ctx->free_tail;
if (ctx->free_tail) ctx->table[ctx->free_tail].free_next = external_port;
else ctx->next_port = external_port;
ctx->free_tail = external_port; ctx->free_count++;
}
DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT mapping removed: external=%u free=%u", external_port, ctx->free_count);
return 1;
}
// Ограниченная работа вне обработки пакетов; истечение не затрагивает статические пробросы.
size_t eim_nat_expire(struct eim_nat_ctx* ctx, uint64_t now_tb, size_t budget) {
if (!ctx || !ctx->initialized || !ctx->table || !ctx->buckets) return 0;
if (budget > EIM_NAT_TABLE_SIZE - 1) budget = EIM_NAT_TABLE_SIZE - 1;
size_t removed = 0;
while (budget--) {
uint32_t port = ctx->gc_cursor++;
if (ctx->gc_cursor == EIM_NAT_TABLE_SIZE) ctx->gc_cursor = 1;
struct eim_nat_entry* e = &ctx->table[port];
if (e->state != EIM_NAT_ENTRY_ACTIVE || e->holds) continue;
uint64_t timeout = e->proto == IPPROTO_TCP_UINT8 ? EIM_NAT_TCP_IDLE_TB :
e->proto == IPPROTO_UDP_UINT8 ? EIM_NAT_UDP_IDLE_TB : EIM_NAT_ICMP_IDLE_TB;
if (now_tb >= e->last_seen && now_tb - e->last_seen >= timeout && eim_nat_remove(ctx, port) == 1) removed++;
}
if (removed) DEBUG_DEBUG(DEBUG_CATEGORY_NAT, "NAT expired mappings=%zu free=%u", removed, ctx->free_count);
return removed;
}
// Зафиксировать внешний порт, уже записанный в IP-пакет, до завершения асинхронной передачи.
int eim_nat_hold(struct eim_nat_ctx* ctx, uint16_t external_port) {
if (!ctx || !ctx->initialized || !ctx->table || !external_port ||
ctx->table[external_port].state == EIM_NAT_ENTRY_FREE || ctx->table[external_port].holds == UINT16_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Invalid NAT hold: ctx=%p port=%u", (void*)ctx, external_port);
return -1;
}
ctx->table[external_port].holds++;
return 0;
}
// Освободить удержание; время простоя после отправки отсчитывается от фактической записи в TUN.
int eim_nat_release(struct eim_nat_ctx* ctx, uint16_t external_port, int refresh) {
if (!ctx || !ctx->initialized || !ctx->table || !external_port || !ctx->table[external_port].holds) {
DEBUG_ERROR(DEBUG_CATEGORY_NAT, "Invalid NAT release: ctx=%p port=%u", (void*)ctx, external_port);
return -1;
}
struct eim_nat_entry* entry = &ctx->table[external_port];
entry->holds--;
if (refresh) entry->last_seen = get_time_tb();
return 0;
}
+72 -19
View File
@@ -1,9 +1,16 @@
/* eim_nat — таблица Endpoint-Independent Mapping для IPv4 TCP/UDP и ICMP echo.
* Ключ mapping: (src_node_id, протокол, внутренний IP, внутренний порт/echo ID), без внешнего адресата.
* Исходящий поиск — хеш, в среднем O(1); входящий — table[port], O(1); свободные порты — двусвязный FIFO, O(1).
* Таблица и 65536 uint16_t-корзин выделяются только при init; обработка/создание mapping не выделяют память.
* init_ctx создаёт состояние из global_config; при отключённом NAT оставляет initialized=0.
* egress/ingress меняют IP/порты и checksums на месте, не отправляют пакеты и не владеют TUN/ETCP.
* IPv4 UDP: отключённая checksum=0 сохраняется; вычисленный ноль записывается как 0xffff.
* Перед обработкой нужен инициализированный ctx; вызовы последовательны в uasync-потоке.
* gateway/internal IP — host order, internal_port — network order, внешний порт — host order.
* src_conn заимствован; срок жизни и выбор транспорта обратной отправки контролирует nat_transport. */
* Динамические записи истекают по idle-timeout; владелец регулярно вызывает expire с ограниченным budget.
* Исходящие/входящие пакеты обновляют last_seen. STATIC и временно удержанные записи не истекают.
* Фрагменты и неподдерживаемые протоколы возвращаются без изменений с EIM_NAT_SKIP.
* src_conn только сохраняется при создании динамической записи, для отправки не используется. */
#ifndef EIM_NAT_H
#define EIM_NAT_H
@@ -18,6 +25,12 @@ extern "C" {
struct ETCP_CONN;
#define EIM_NAT_TABLE_SIZE 65536
#define EIM_NAT_OK 0 // Пакет преобразован.
#define EIM_NAT_SKIP 1 // Фрагмент, неподдерживаемый протокол или отсутствует mapping; пакет не изменён.
#define EIM_NAT_ERROR -1 // Некорректный пакет/контекст или исчерпан пул; пакет не изменён.
#define EIM_NAT_TCP_IDLE_TB (7440ULL * 10000) // Консервативный TCP idle timeout без отслеживания TCP-состояния (RFC 5382).
#define EIM_NAT_UDP_IDLE_TB (120ULL * 10000) // RFC 4787: не менее двух минут.
#define EIM_NAT_ICMP_IDLE_TB (60ULL * 10000) // RFC 5508: не менее минуты.
#define EIM_NAT_ENTRY_FREE 0
#define EIM_NAT_ENTRY_ACTIVE 1
@@ -28,42 +41,82 @@ struct ETCP_CONN;
#define IPPROTO_ICMP_UINT8 1
struct eim_nat_entry {
uint32_t internal_ip;
uint16_t internal_port;
uint8_t proto;
uint8_t state;
uint64_t src_node_id; // which node sent the original packet
uint64_t last_seen;
struct ETCP_CONN* src_conn; // cached connection for sendback
uint32_t internal_ip; // Внутренний IPv4, host order.
uint16_t internal_port; // Внутренний TCP/UDP-порт или ICMP echo ID, network order.
uint8_t proto; // IPPROTO_*_UINT8.
uint8_t state; // EIM_NAT_ENTRY_FREE / ACTIVE / STATIC.
uint64_t src_node_id; // Узел-владелец mapping и адресат обратной отправки.
uint64_t last_seen; // Монотонное время последнего пакета в любом направлении, единицы 0.1 мс.
struct ETCP_CONN* src_conn; // Заимствованная метка исходного conn, может быть NULL; модуль её не разыменовывает.
uint16_t hash_next; // Внутренняя цепочка корзины; 0 — конец. Ключи и состояние извне не менять.
uint16_t free_prev, free_next; // Внутренний список свободных динамических портов; 0 — конец.
uint16_t holds; // Временные удержания уже преобразованных исходящих пакетов до записи в TUN.
};
// Состояние преобразования; TUN и транспортные ресурсы принадлежат nat_transport.
struct eim_nat_ctx {
uint32_t gateway_ip;
uint16_t port_start;
uint16_t port_end;
uint16_t next_port;
struct eim_nat_entry* table;
size_t table_size;
int initialized;
uint32_t gateway_ip; // Внешний IPv4 NAT, host order; nat_tun_ip, иначе tun_ip.
uint16_t port_start; // Начало динамического диапазона включительно; 1 <= port_start < port_end.
uint16_t port_end; // Конец диапазона включительно; допускается 65535.
uint16_t next_port; // Голова списка свободных портов; 0 — пул исчерпан.
uint16_t free_tail; // Хвост: освобождённые порты возвращаются сюда.
uint32_t free_count; // Число свободных динамических портов.
uint32_t gc_cursor; // Следующий слот порционной очистки, 1..65535.
struct eim_nat_entry* table; // Владеет table_size записями; индекс — внешний порт/echo ID для всех протоколов вместе.
uint16_t* buckets; // 65536 голов хеш-цепочек полного внутреннего ключа, 0 — пустая корзина.
size_t table_size; // EIM_NAT_TABLE_SIZE при успешной инициализации.
int initialized; // 1 — таблица создана; 0 — NAT отключён, не создан или уничтожен.
};
struct global_config;
/* Проверить IPv4 и транспортные границы без изменения данных. header_len обязателен и получает IHL в байтах.
* EIM_NAT_OK — поддерживаемый полный пакет; SKIP — фрагмент/неподдерживаемый протокол; ERROR — неверный формат.
* Checksums не проверяются. Не требует созданного NAT ctx. */
int eim_nat_validate_packet(const uint8_t* ip, size_t len, uint16_t* header_len);
/* Создать ctx по g: IPv4 шлюза, 1 <= port_start < port_end <= 65535, статические пробросы.
* ctx не должен владеть предыдущей таблицей: перед повторным init вызвать destroy_ctx.
* 0 — успех (при !nat_enabled таблица не создаётся); -1 — ошибка, созданная таблица освобождена. */
int eim_nat_init_ctx(struct eim_nat_ctx* ctx, const struct global_config* g);
/* Освободить таблицу и обнулить созданный ctx; NULL и повторное уничтожение допустимы. */
void eim_nat_destroy_ctx(struct eim_nat_ctx* ctx);
/* 0 — обработано или пакет не подлежит NAT, -1 — ошибка; буфер принадлежит вызывающему. */
/* Изменить source IP и порт/echo ID в ip_data (ip_len байтов IPv4), выделив/переиспользовав mapping.
* src_node_id — владелец записи; src_conn — необязательная заимствованная метка только для новой ACTIVE-записи.
* Контрольные суммы должны быть корректны до вызова: модуль их обновляет, но не проверяет.
* ctx создан, ip_data — полный доступный для записи IPv4-пакет; буфер остаётся у вызывающего.
* Возврат EIM_NAT_OK / SKIP / ERROR. Проверяются версия, IHL, total_length и границы транспортного заголовка. */
int eim_nat_egress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
uint64_t src_node_id, struct ETCP_CONN* src_conn);
/* out_entry изменяется только при найденном mapping; заранее установить *out_entry=NULL.
* Запись заимствована у ctx. 0 само по себе не означает, что пакет был преобразован. */
/* Для пакета на gateway_ip найти запись по внешнему порту/echo ID и протоколу, восстановить destination.
* ctx/ip_data/checksum — как у egress. out_entry необязателен, перед обработкой обнуляется.
* Указатель заимствован до remove/expiry/destroy; для ожидания сохранить node_id/преобразованный пакет или вызвать hold.
* При EIM_NAT_OK получает заимствованную запись; иначе остаётся NULL. Возврат EIM_NAT_OK / SKIP / ERROR. */
int eim_nat_ingress(struct eim_nat_ctx* ctx, uint8_t* ip_data, size_t ip_len,
struct eim_nat_entry** out_entry);
/* Добавить TCP/UDP-проброс на node_id != 0: internal_ip_host — IPv4 host order,
* internal_port_net — ненулевой порт network order; external_port — 1..65535 host order,
* включая порты вне динамического диапазона. Ответ владельца использует тот же внешний порт.
* 0 — добавлен; -1 — ctx не создан, неверные аргументы, занят внешний порт или уже есть такой внутренний ключ. */
int eim_nat_add_forward(struct eim_nat_ctx* ctx, uint8_t proto,
uint32_t internal_ip_host, uint16_t internal_port_net,
uint16_t external_port);
uint16_t external_port, uint64_t node_id);
/* Удалить запись по внешнему порту из всех индексов; порт из динамического диапазона вернуть в пул.
* 1 — удалена, 0 — уже свободна, -1 — неверный ctx/порт, удержанная запись или нарушен индекс. Включая STATIC-записи.
* Все ранее полученные указатели на эту запись теряют смысл: слот может быть переиспользован. */
int eim_nat_remove(struct eim_nat_ctx* ctx, uint16_t external_port);
/* Проверить не более budget слотов начиная с gc_cursor; now_tb — get_time_tb(), не календарное время.
* Возврат — число удалённых ACTIVE-записей; STATIC не истекают. Очистка не выполняется из обработки пакета.
* budget ограничивается 65535. NULL/неинициализированный ctx — 0. */
size_t eim_nat_expire(struct eim_nat_ctx* ctx, uint64_t now_tb, size_t budget);
/* Удержать существующий mapping на время асинхронной отправки; удаление/expiry запрещены до release.
* release с refresh != 0 обновляет last_seen при фактической отправке; при отбрасывании refresh=0.
* 0 — успех, -1 — неверная/свободная запись, переполнение holds или непарный release. */
int eim_nat_hold(struct eim_nat_ctx* ctx, uint16_t external_port);
int eim_nat_release(struct eim_nat_ctx* ctx, uint16_t external_port, int refresh);
#ifdef __cplusplus
+69 -16
View File
@@ -1,47 +1,100 @@
# EIM NAT (Endpoint-Independent Mapping NAT)
## 1. Назначение
Трансляция сетевых адресов с endpoint-independent поведением: для каждого кортежа `(внутренний IP, внутренний порт, протокол)` выделяется уникальный внешний порт на шлюзе, не зависящий от внешнего адресата. Поддерживает TCP, UDP и ICMP Echo. Инкрементальный пересчёт контрольных сумм.
Преобразование IPv4 TCP/UDP и ICMP Echo на месте. Ключ mapping —
`(src_node_id, протокол, внутренний IP, внутренний порт/echo ID)`;
внешний адресат в ключ не входит. Разные узлы с одинаковыми частными адресами получают разные внешние порты.
Отправка пакетов и владение TUN/ETCP находятся в `nat_transport`, нормативный API — в `eim_nat.h`.
## 2. Как пользоваться
```c
struct eim_nat_ctx nat;
eim_nat_init_ctx(&nat, &global_cfg); // таблица 64K записей
eim_nat_egress(&nat, pkt, len, node_id, conn); // src IP/port -> внешние
eim_nat_ingress(&nat, pkt, len, &entry); // dst IP/port -> внутренние
eim_nat_add_forward(&nat, proto, ip, port, ext_port); // статический проброс
eim_nat_destroy_ctx(&nat);
if (eim_nat_init_ctx(&nat, &global_cfg) == 0 && nat.initialized) {
int result = eim_nat_egress(&nat, pkt, len, node_id, conn);
if (result == EIM_NAT_OK) { /* отправить преобразованный пакет */ }
// EIM_NAT_SKIP — неподдерживаемый пакет; EIM_NAT_ERROR — ошибка. В обоих случаях пакет не изменён.
struct eim_nat_entry* entry = NULL;
result = eim_nat_ingress(&nat, reply, reply_len, &entry);
if (result == EIM_NAT_OK) { /* сохранить entry->src_node_id для обратной отправки */ }
eim_nat_expire(&nat, get_time_tb(), 128); // регулярно из housekeeping, вне обработки пакета
eim_nat_destroy_ctx(&nat);
}
```
**Таблица:** 64K записей, индексируется по внешнему порту (внешний порт == индекс в таблице).
**Таблица:** 65536 записей, индекс — внешний порт/echo ID. Для всех протоколов используется один пул:
один внешний порт занимает только одна запись. Динамический диапазон включителен,
`1 <= port_start < port_end <= 65535`; `next_port` — голова двусвязного списка свободных портов,
0 означает исчерпание. Выделение порта и резервирование статического порта выполняются за O(1), удалённый порт возвращается в хвост.
При исчерпании новый поток получает `-1`, существующий продолжает использовать свой mapping.
Исходящий индекс — 65536 хеш-корзин по 2 байта, цепочки состоят из индексов портовой таблицы.
Сравнивается полный внутренний ключ; поиск в среднем O(1), в худшем случае зависит от длины коллизий.
После init обработка пакетов, создание и удаление mapping не выделяют память.
**eim_nat_egress:** выделяет внешний порт (аллоцирует из циклического пула `next_port`), заменяет src IP на шлюзовой и src порт на внешний, корректирует IP- и транспортную контрольные суммы. Фрагментированные пакеты пропускаются без обработки.
**eim_nat_egress:** находит внутренний ключ в хеше, включая статические порты вне динамического пула,
либо создаёт ACTIVE-запись. Меняет source IP и порт/echo ID, обновляет checksums.
`src_conn` только сохраняется при создании ACTIVE-записи; отправка ответа использует `src_node_id`, а не этот указатель.
`last_seen` обновляется по `get_time_tb()` в обоих направлениях. Время простоя до удаления:
TCP — 7440 секунд без отслеживания TCP-состояния ([RFC 5382, REQ-5](https://www.rfc-editor.org/rfc/rfc5382#section-5)),
UDP — 120 секунд ([RFC 4787, REQ-5](https://www.rfc-editor.org/rfc/rfc4787#section-4.3)),
ICMP Echo — 60 секунд ([RFC 5508](https://www.rfc-editor.org/rfc/rfc5508#section-3.2)).
`expire` проверяет не более заданного числа слотов с сохранением курсора, статические записи не удаляет.
В nat_transport проверяются 128 слотов каждые 100мс: очистка после timeout может задержаться примерно на 51.2с
плюс задержки выполнения event loop. Полного обхода таблицы на пути пакета нет.
**eim_nat_ingress:** ищет запись по внешнему dst-порту в таблице, заменяет dst IP на внутренний и dst порт на внутренний, корректирует контрольные суммы. Возвращает `out_entry` — указатель на запись (содержит `src_node_id` и `src_conn` для маршрутизации ответа).
**eim_nat_ingress:** для пакета на `gateway_ip` проверяет запись по внешнему destination-порту/echo ID и протоколу,
меняет destination на внутренний адрес. `out_entry` необязателен; при совпадении получает заимствованную запись.
До обработки `*out_entry` обнуляется. Только EIM_NAT_OK возвращает запись; указатель действует до удаления/истечения/уничтожения.
Для асинхронного ожидания ответа транспорт сохраняет преобразованный пакет и ID получателя.
Если исходящий пакет провайдера ждёт записи в TUN, `hold/release` запрещают удаление и повторное использование его внешнего порта.
**eim_nat_add_forward:** добавляет статическую запись (`EIM_NAT_ENTRY_STATIC`), которая не вытесняется динамическими.
**Статический проброс:** TCP/UDP-запись с явным ненулевым ID узла назначения, внутренним портом в network order
и внешним портом 1..65535 в host order. Внешний порт может быть вне динамического диапазона.
Первый входящий пакет уже имеет адресата; исходящий пакет этого же узла переиспользует статический порт.
Занятый внешний порт и повтор внутреннего ключа отклоняются. Некорректный проброс при init освобождает всю создаваемую таблицу.
**Контрольные суммы:** пересчитываются инкрементально через `csum_update_n()` — старые значения вычитаются из ~csum, новые прибавляются. Это работает для TCP pseudo-header checksum и UDP.
**Контрольные суммы:** `csum_update_n()` складывает `~csum`, 16-битные дополнения старых слов и новые слова,
затем переносит старшие биты в младшие и инвертирует результат. Обычное беззнаковое вычитание здесь теряет заём.
IP checksum учитывает замену адреса; TCP/UDP — адрес в pseudo-header и порт; ICMP Echo — идентификатор.
В IPv4 UDP исходная checksum=0 остаётся нулевой; вычисленный ноль записывается как 0xffff.
Исходные checksums не проверяются: инкрементальное обновление требует корректного входного пакета.
**Ограничения:** работа с полными доступными для записи IPv4-пакетами, последовательные вызовы в потоке ядра.
Проверяются версия IPv4, IHL, точное соответствие total_length размеру буфера, TCP data offset,
границы UDP length и ICMP Echo. Невыровненные буферы допустимы. Исходные checksums не проверяются.
Фрагменты (MF или ненулевой offset), неподдерживаемые протоколы и ICMP без Echo возвращают EIM_NAT_SKIP;
nat_transport их отбрасывает. Повреждённые пакеты возвращают EIM_NAT_ERROR до изменения данных/таблицы.
Перед повторным init нужен destroy. При удалении запись сначала исключается из хеша, затем порт возвращается в пул.
Ключи/состояние записей извне изменять нельзя: для удаления использовать `eim_nat_remove`.
**Конфиг:**
```ini
[nat]
enabled = 1
tun_ip = 10.0.0.1
port_start = 10000
port_end = 20000
[nat_forward]
rule = tcp 192.168.1.2:80 :8080
forward = tcp:0123456789abcdef:192.168.1.2:80:8080
```
Опции `[nat]` включают NAT; параметра `enabled` и секции `[nat_forward]` нет.
`forward=proto:node_id_hex:IPv4:internal_port:external_port` задаётся на провайдере;
узел назначения должен работать как NAT-клиент этого провайдера. Ошибки разбора NAT отклоняют весь конфиг.
Шлюз берётся из IPv4 `nat_tun_ip`, иначе из IPv4 основного `tun_ip`.
## 3. API
| Функция/Структура | Описание |
|---|---|
| `struct eim_nat_entry` | Запись: internal_ip, internal_port, proto, state (FREE/ACTIVE/STATIC), src_node_id, src_conn, last_seen |
| `struct eim_nat_ctx` | Контекст: gateway_ip, port_start/end, next_port (циклический аллокатор), table[64K], initialized |
| `struct eim_nat_ctx` | Портовая таблица, хеш-индекс, свободный FIFO и курсор порционной очистки |
| `eim_nat_init_ctx(ctx, cfg)` | Инициализация таблицы, загрузка статических пробросов из конфига |
| `eim_nat_destroy_ctx(ctx)` | Освобождение таблицы, обнуление |
| `eim_nat_egress(ctx, ip_data, ip_len, src_node_id, src_conn)` | Трансляция исходящего пакета: src IP/port → шлюзовые. Создаёт динамическую запись при первом проходе |
| `eim_nat_ingress(ctx, ip_data, ip_len, out_entry)` | Обратная трансляция входящего пакета: dst IP/port → внутренние. Заполняет out_entry для маршрутизации |
| `eim_nat_add_forward(ctx, proto, ip_host, port_net, ext_port)` | Добавление статического проброса (runtime API) |
| `eim_nat_add_forward(ctx, proto, ip_host, port_net, ext_port, node_id)` | Добавление статического проброса на заданный узел |
| `eim_nat_remove(ctx, ext_port)` | Согласованное удаление из хеша и возврат порта; удержанная запись не удаляется |
| `eim_nat_expire(ctx, now_tb, budget)` | Ограниченная по числу слотов очистка динамических записей |
| `eim_nat_hold/release` | Удержание внешнего порта на время асинхронной исходящей передачи |
Проверка скорости: `make -C tests test_nat_stress`, затем `tests/test_nat_stress --benchmark`.
Измеряются создание/попадание/промах заполненного пула для 1/1000/10000/60000 потоков и 64/1500 байтов;
подготовка payload и выделения памяти вынесены из измерений, NAT-диагностика отключена.
Это benchmark движка, без стоимости TUN, шифрования и сети.
+66 -28
View File
@@ -12,55 +12,48 @@ static int compare_rules(const void *a, const void *b) {
if (ra->ip != rb->ip) {
return (ra->ip > rb->ip) ? 1 : -1;
}
return (ra->port > rb->port) ? 1 : -1;
return (ra->port > rb->port) - (ra->port < rb->port);
}
int fw_init(struct firewall_ctx *ctx) {
if (!ctx) return -1;
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: init without context"); return -1; }
memset(ctx, 0, sizeof(struct firewall_ctx));
return 0;
}
/* Подготовить полный набор до освобождения старого: OOM не меняет политику. */
int fw_load_rules(struct firewall_ctx *ctx, const struct global_config *cfg) {
if (!ctx || !cfg) return -1;
fw_free(ctx);
fw_init(ctx);
ctx->bypass_all = cfg->firewall_bypass_all;
if (cfg->firewall_rule_count > 0) {
ctx->rules = u_calloc(cfg->firewall_rule_count, sizeof(struct CFG_FIREWALL_RULE));
if (!ctx->rules) {
DEBUG_ERROR(DEBUG_CATEGORY_SYS, "failed to allocate rules");
if (!ctx || !cfg || cfg->firewall_rule_count < 0 ||
(uint32_t)cfg->firewall_rule_count > UINT32_MAX / sizeof(struct CFG_FIREWALL_RULE) ||
(cfg->firewall_rule_count && !cfg->firewall_rules)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: invalid rules/context");
return -1;
}
struct firewall_ctx next = { .count = cfg->firewall_rule_count, .bypass_all = !!cfg->firewall_bypass_all };
if (next.count > 0) {
next.rules = u_calloc(next.count, sizeof(*next.rules));
if (!next.rules) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "Firewall: allocation failed for %d rules; previous policy retained", next.count);
return -1;
}
memcpy(ctx->rules, cfg->firewall_rules,
cfg->firewall_rule_count * sizeof(struct CFG_FIREWALL_RULE));
ctx->count = cfg->firewall_rule_count;
// Sort rules by IP (and port)
qsort(ctx->rules, ctx->count, sizeof(struct CFG_FIREWALL_RULE), compare_rules);
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Loaded %d firewall rules (sorted)", ctx->count);
memcpy(next.rules, cfg->firewall_rules, next.count * sizeof(*next.rules));
qsort(next.rules, next.count, sizeof(*next.rules), compare_rules);
}
if (ctx->bypass_all) {
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Firewall bypass_all mode enabled");
}
fw_free(ctx);
*ctx = next;
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Firewall: loaded %d rules, bypass_all=%d, default=deny", ctx->count, ctx->bypass_all);
return 0;
}
int fw_check(const struct firewall_ctx *ctx, uint32_t ip, uint16_t port) {
if (!ctx) return 0;
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_ROUTING, "Firewall: check without context"); return 0; }
if (ctx->bypass_all) return 1;
// Binary search for IP
int low = 0;
int high = ctx->count - 1;
while (low <= high) {
int mid = (low + high) / 2;
int mid = low + (high - low) / 2;
if (ctx->rules[mid].ip == ip) {
// Found matching IP, check port
int i = mid;
@@ -88,8 +81,53 @@ int fw_check(const struct firewall_ctx *ctx, uint32_t ip, uint16_t port) {
return 0;
}
/* Разбирать заголовки по байтам: TUN/ETCP не гарантируют выравнивание полей. */
int fw_check_packet(const struct firewall_ctx *ctx, const uint8_t *packet, size_t len) {
if (!ctx || !packet || len < 20 || (packet[0] >> 4) != 4) {
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: invalid IPv4 packet/context, len=%zu", len);
return -1;
}
size_t ihl = (packet[0] & 15) * 4;
size_t total = ((uint16_t)packet[2] << 8) | packet[3];
if (ihl < 20 || ihl > len || total != len) {
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: invalid IPv4 lengths, ihl=%zu total=%zu len=%zu", ihl, total, len);
return -1;
}
uint16_t fragment = ((uint16_t)packet[6] << 8) | packet[7];
int fragmented = (fragment & 0x3fff) != 0;
size_t payload = len - ihl;
if ((fragment & 0x8000) || (fragmented && ((fragment & 0x4000) || !payload)) ||
((fragment & 0x2000) && payload % 8)) {
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: invalid IPv4 fragmentation, flags_offset=%04x payload=%zu", fragment, payload);
return -1;
}
uint8_t protocol = packet[9];
uint16_t port = 0;
if (!fragmented && (protocol == 6 || protocol == 17)) {
size_t minimum = protocol == 6 ? 20 : 8;
if (payload < minimum) {
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: truncated transport header, protocol=%u payload=%zu", protocol, payload);
return -1;
}
size_t transport_len = protocol == 6 ? (packet[ihl + 12] >> 4) * 4 :
((uint16_t)packet[ihl + 4] << 8) | packet[ihl + 5];
if (transport_len < minimum || transport_len > payload || (protocol == 17 && transport_len != payload)) {
DEBUG_WARN(DEBUG_CATEGORY_ROUTING, "Firewall: invalid transport length, protocol=%u header_length=%zu payload=%zu",
protocol, transport_len, payload);
return -1;
}
port = ((uint16_t)packet[ihl + 2] << 8) | packet[ihl + 3];
}
uint32_t ip = ((uint32_t)packet[16] << 24) | ((uint32_t)packet[17] << 16) | ((uint32_t)packet[18] << 8) | packet[19];
int allowed = fw_check(ctx, ip, port);
DEBUG_DEBUG(DEBUG_CATEGORY_ROUTING, "Firewall: %s dst=%u.%u.%u.%u port=%u protocol=%u fragmented=%d bypass_all=%d",
allowed ? "allow" : "deny", packet[16], packet[17], packet[18], packet[19], port, protocol, fragmented, ctx->bypass_all);
return allowed;
}
void fw_free(struct firewall_ctx *ctx) {
if (!ctx) return;
DEBUG_DEBUG(DEBUG_CATEGORY_CONFIG, "Firewall: freeing %d rules, bypass_all=%d", ctx->count, ctx->bypass_all);
u_free(ctx->rules);
memset(ctx, 0, sizeof(struct firewall_ctx));
}
+12 -3
View File
@@ -1,11 +1,13 @@
/* firewall — список разрешённых пар IPv4/порт из global_config, без изменения системного firewall.
* init/обнуление → load_rules (копия и сортировка правил) → check → free; контекст использует один поток.
* check: 1 разрешено, 0 запрещено; port=0 в правиле разрешает все порты IP, bypass_all разрешает всё.
* Оба направления основного TUN проверяются по назначению; автоматического допуска ответов нет.
* IP и порт передавать в host order, как в CFG_FIREWALL_RULE. */
#ifndef FIREWALL_H
#define FIREWALL_H
#include <stdint.h>
#include <stddef.h>
#include "config_parser.h"
#ifdef __cplusplus
@@ -13,14 +15,21 @@ extern "C" {
#endif
struct firewall_ctx {
struct CFG_FIREWALL_RULE *rules;
int count;
int bypass_all;
struct CFG_FIREWALL_RULE *rules; // Собственная отсортированная копия правил.
int count; // Число правил; пустой набор запрещает всё.
int bypass_all; // allow=all: разрешение независимо от списка.
};
// Обнулить новый/освобождённый ctx; повторный init живого контекста запрещён. Возврат: 0/-1.
int fw_init(struct firewall_ctx *ctx);
// Заменить правила копией cfg; ctx должен быть инициализирован. Ошибка (-1) сохраняет ctx целиком; успех — 0.
int fw_load_rules(struct firewall_ctx *ctx, const struct global_config *cfg);
// Проверить IP/порт назначения в host order: 1/0. port=0 проверяет только разрешение всего IP.
int fw_check(const struct firewall_ctx *ctx, uint32_t ip, uint16_t port);
// Проверить чистый IPv4-пакет длиной len по назначению: 1 разрешён, 0 запрещён, -1 повреждён/не IPv4.
// Порт относится к TCP/UDP; фрагменты и другие протоколы требуют allow=IP либо allow=all.
int fw_check_packet(const struct firewall_ctx *ctx, const uint8_t *packet, size_t len);
// Освободить и обнулить инициализированный ctx; NULL и повторный вызов допустимы.
void fw_free(struct firewall_ctx *ctx);
#ifdef __cplusplus
-28
View File
@@ -1,28 +0,0 @@
# Firewall rule engine
## 1. Назначение
Фильтр пакетов по IP/порту для входящего/исходящего трафика. Правила загружаются из конфига, сортируются по IP и порту, затем проверка идёт бинарным поиском. Используется для разрешения/запрета прохождения пакетов через туннель.
## 2. Как пользоваться
```c
struct firewall_ctx fw;
fw_init(&fw);
fw_load_rules(&fw, &global_cfg);
if (fw_check(&fw, ip_host, port)) { /* пакет разрешён */ }
fw_free(&fw);
```
Правила задаются в конфиге в секции `[firewall]`:
- `rule = ip port` — разрешить IP:port (port=0 — любой порт)
- `bypass_all = 1` — пропускать всё независимо от правил
Сортировка — по IP, затем по порту. При проверке: бинарный поиск по IP, затем линейный скан соседних записей с тем же IP для сверки порта. Правило с `port=0` означает любой порт для этого IP.
## 3. API
| Функция/Структура | Описание |
|---|---|
| `struct firewall_ctx` | Набор правил (`rules`), их количество (`count`), флаг `bypass_all` |
| `fw_init(ctx)` | Обнуление контекста |
| `fw_load_rules(ctx, cfg)` | Копирование правил из конфига, сортировка qsort, установка bypass_all |
| `fw_check(ctx, ip, port)` | Возвращает 1 если пакет разрешён (bypass_all=1 или IP+port есть в правилах), 0 — запрещён |
| `fw_free(ctx)` | Освобождение памяти, обнуление полей |
+14 -2
View File
@@ -127,6 +127,7 @@ struct lwip_tcp_ctx *lwip_tcp_init(struct UASYNC *ua, tcp_output_fn output, void
ctx->ua = ua;
ctx->output = output;
ctx->output_arg = output_arg;
ctx->local_mss = TCP_MSS;
ctx->pcb_pool = memory_pool_init(sizeof(struct tcp_pcb), "pcb_pool");
ctx->pcb_listen_pool = memory_pool_init(sizeof(struct tcp_pcb_listen), "pcb_listen_pool");
ctx->seg_pool = memory_pool_init(sizeof(struct tcp_seg), "seg_pool");
@@ -143,6 +144,17 @@ struct lwip_tcp_ctx *lwip_tcp_init(struct UASYNC *ua, tcp_output_fn output, void
}
/* Локальный предел не заменяется MSS из SYN пира. Настройка выполняется до открытия соединений. */
err_t lwip_tcp_set_mtu(struct lwip_tcp_ctx *ctx, uint16_t mtu) {
if (!ctx || mtu < 68 || mtu > TCP_MSS + 40 || ctx->active_pcbs || ctx->listen_pcbs || ctx->bound_pcbs || ctx->tw_pcbs) {
DEBUG_ERROR(DEBUG_CATEGORY_PROXY, "lwIP MTU setup rejected: ctx=%p mtu=%u", (void*)ctx, mtu);
return LERR_ARG;
}
ctx->local_mss = (uint16_t)(mtu - 40);
DEBUG_INFO(DEBUG_CATEGORY_PROXY, "lwIP IPv4 MTU=%u local MSS=%u", mtu, ctx->local_mss);
return LERR_OK;
}
// ============================================
// 2. lwip_tcp_set_timer
// ============================================
@@ -644,7 +656,7 @@ err_t tcp_connect(struct tcp_pcb *pcb, uint32_t ipaddr, uint16_t port, tcp_conne
pcb->rcv_wnd = pcb->rcv_ann_wnd = TCPWND16(TCP_WND);
pcb->rcv_ann_right_edge = pcb->rcv_nxt;
pcb->snd_wnd = TCP_WND;
pcb->mss = INITIAL_MSS;
pcb->mss = LWIP_MIN(INITIAL_MSS, pcb->ctx->local_mss);
pcb->cwnd = 1;
pcb->connected = connected;
@@ -1158,7 +1170,7 @@ struct tcp_pcb *tcp_alloc(struct lwip_tcp_ctx *ctx, uint8_t prio)
pcb->snd_buf = TCP_SND_BUF;
pcb->rcv_wnd = pcb->rcv_ann_wnd = TCPWND16(TCP_WND);
pcb->ttl = 64;
pcb->mss = INITIAL_MSS;
pcb->mss = LWIP_MIN(INITIAL_MSS, pcb->ctx->local_mss);
pcb->rto = (int16_t)(TCP_RTO_MIN_MS / TCP_SLOW_INTERVAL);
pcb->sv = (int16_t)(TCP_RTO_MIN_MS / TCP_SLOW_INTERVAL);
pcb->rtime = -1;
+6 -2
View File
@@ -32,7 +32,7 @@
/* Адаптированный lwIP TCP для TUN-прокси: TCP-состояния, retransmit, окна и callbacks потоков.
* init создаёт контекст с uasync-таймером; input получает TCP-сегмент после IPv4-заголовка,
* output callback передаёт сегмент внешнему IP/TUN-слою. destroy освобождает PCB и таймер.
* output callback передаёт полный IPv4-пакет внешнему TUN-слою. destroy освобождает PCB и таймер.
* API и callbacks требуют последовательного выполнения; input использует общий рабочий контекст. */
#ifndef LWIP_TCP_H
#define LWIP_TCP_H
@@ -120,7 +120,7 @@ typedef err_t (*tcp_poll_fn) (void *arg, struct tcp_pcb *pcb);
typedef void (*tcp_err_fn) (void *arg, err_t err);
typedef err_t (*tcp_connected_fn)(void *arg, struct tcp_pcb *pcb, err_t err);
// Output callback получает TCP-сегмент и IP адреса отдельно; IPv4-заголовок строит вызывающий слой.
// Output callback получает полный IPv4-пакет; src_ip/dst_ip также передаются отдельно.
typedef err_t (*tcp_output_fn)(void *arg, struct pbuf *p, uint32_t src_ip, uint32_t dst_ip);
#define TCP_TRACE_SIZE 384
@@ -250,6 +250,7 @@ struct lwip_tcp_ctx {
uint8_t active_pcbs_changed; // коллбэк изменил список active_pcbs — перезапустить обход
uint16_t iss_seed;
uint16_t ip_id;
uint16_t local_mss; // Максимальный TCP payload локального IPv4 MTU; по умолчанию 1460.
uint16_t port_seed;
char trace_id; // 'A' or 'B' for trace dump identification
@@ -272,6 +273,9 @@ struct lwip_tcp_ctx {
// Lifecycle
struct lwip_tcp_ctx* lwip_tcp_init(struct UASYNC *ua, tcp_output_fn output, void *output_arg);
void lwip_tcp_destroy(struct lwip_tcp_ctx *ctx);
// До создания PCB: задать IPv4 MTU 68..1500 для начального/объявляемого/согласованного MSS.
// ctx принадлежит вызывающему; LERR_OK при успехе, LERR_ARG при неверных аргументах/уже созданных списках PCB.
err_t lwip_tcp_set_mtu(struct lwip_tcp_ctx *ctx, uint16_t mtu);
void lwip_tcp_set_timer(struct lwip_tcp_ctx *ctx,
uint16_t interval_ms, uint16_t rto_min_ms, uint16_t rto_max_ms);
+1 -1
View File
@@ -1077,7 +1077,7 @@ static void tcp_parseopt(struct tcp_pcb *pcb)
}
mss = (uint16_t)(tcp_get_next_optbyte() << 8);
mss |= tcp_get_next_optbyte();
pcb->mss = ((mss > TCP_MSS) || (mss == 0)) ? TCP_MSS : mss;
pcb->mss = (mss == 0) ? pcb->ctx->local_mss : LWIP_MIN(mss, pcb->ctx->local_mss);
break;
default:
data = tcp_get_next_optbyte();
+1 -1
View File
@@ -713,7 +713,7 @@ tcp_output_segment(struct tcp_seg *seg, struct tcp_pcb *pcb)
opts = (uint32_t *)(void *)(seg->tcphdr + 1);
if (seg->flags & TF_SEG_OPTS_MSS) {
*opts = TCP_BUILD_MSS_OPTION(TCP_MSS);
*opts = TCP_BUILD_MSS_OPTION(pcb->ctx->local_mss);
opts += 1;
}
+196 -108
View File
@@ -3,6 +3,7 @@
#include "../../lib/u_async.h"
#include "../../lib/mem.h"
#include "../../lib/debug_config.h"
#include "../../lib/file_io.h"
#include <openssl/sha.h>
#include <openssl/evp.h>
@@ -12,6 +13,7 @@
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <errno.h>
#ifdef _WIN32
#include <windows.h>
@@ -39,6 +41,7 @@ static void ma_finish(struct ma_thread_ctx* job, int error) {
while (*p && *p != job) p = &(*p)->next;
if (*p) *p = job->next;
pthread_join(job->thread, NULL);
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "media task completed job=%p error=%d remaining=%d", (void*)job, error, job->owner->jobs != NULL);
job->done(job->arg, error);
u_free(job);
}
@@ -66,6 +69,7 @@ struct media_async* media_async_create(void) {
void media_async_destroy(struct media_async* ma) {
if (!ma) return;
if (ma->closing) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media owner recursive destroy rejected"); return; }
ma->closing = 1;
unsigned cancelled = 0;
while (ma->jobs) {
@@ -82,14 +86,18 @@ void media_async_destroy(struct media_async* ma) {
void media_async_submit(struct media_async* ma, struct UASYNC* ua,
ma_work_fn work, void* data, ma_done_fn done, void* arg) {
if (!ma || !ua || !work || !done || ma->closing) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media task rejected: invalid arguments or stopping owner");
if (done) done(arg, MEDIA_ASYNC_CANCELLED);
if (!ma || !ua || !work || !done) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media task rejected: invalid arguments ma=%p ua=%p work=%d done=%d",
(void*)ma, (void*)ua, work != NULL, done != NULL);
if (done) done(arg, -1);
return;
}
if (ma->closing) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media task rejected: owner stopping"); done(arg, MEDIA_ASYNC_CANCELLED); return; }
unsigned active = 0;
for (struct ma_thread_ctx* pending = ma->jobs; pending; pending = pending->next) active++;
if (active >= 16) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media task limit reached active=%u", active); done(arg, -1); return; }
if (active >= MEDIA_ASYNC_MAX_TASKS) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media task limit reached active=%u", active); done(arg, -1); return;
}
struct ma_thread_ctx* job = u_calloc(1, sizeof(*job));
if (!job) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media task allocation failed"); done(arg, -1); return; }
job->owner = ma; job->ua = ua; job->work = work; job->data = data; job->done = done; job->arg = arg;
@@ -103,132 +111,212 @@ void media_async_submit(struct media_async* ma, struct UASYNC* ua,
u_free(job->completion); u_free(job); done(arg, -1); return;
}
job->next = ma->jobs; ma->jobs = job;
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "media task accepted job=%p pending=%u", (void*)job, active + 1);
}
/* ─── примитивы (вызываются из worker thread) ─── */
/* Синхронный SHA256; результат не публикуется при ошибке чтения/close. */
int ma_sha256_file(const char* path, uint8_t hash_out[32]) {
FILE* f = fopen(path, "rb");
if (!f) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_sha256_file: cannot open %s", path); return -1; }
if (!hash_out) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SHA256 requires output"); return -1; }
memset(hash_out, 0, 32);
FILE* file = file_io_open_read(path);
if (!file) return -1;
SHA256_CTX ctx;
SHA256_Init(&ctx);
uint8_t buf[65536];
size_t rd;
while ((rd = fread(buf, 1, sizeof(buf), f)) > 0) SHA256_Update(&ctx, buf, rd);
int error = ferror(f);
if (fclose(f)) error = 1;
if (error) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_sha256_file: read/close failed path=%s", path); return -1; }
SHA256_Final(hash_out, &ctx);
int error = SHA256_Init(&ctx) != 1;
uint8_t buffer[65536]; size_t count;
while (!error && (count = fread(buffer, 1, sizeof(buffer), file)) > 0)
if (SHA256_Update(&ctx, buffer, count) != 1) error = 1;
if (ferror(file)) error = 1;
if (fclose(file)) error = 1;
if (!error && SHA256_Final(hash_out, &ctx) != 1) error = 1;
if (error) {
memset(hash_out, 0, 32);
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "SHA256 read/digest/close failed path=%s errno=%d", path, errno); return -1;
}
return 0;
}
int ma_sign_block(const uint8_t* ed25519_privkey, const uint8_t* data, size_t len, uint8_t sig_out[64]) {
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, ed25519_privkey, 32);
if (!pkey) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_sign_block: EVP_PKEY_new_raw_private_key failed"); return -1; }
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
if (!mdctx) { EVP_PKEY_free(pkey); return -1; }
if (EVP_DigestSignInit(mdctx, NULL, NULL, NULL, pkey) != 1) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_sign_block: EVP_DigestSignInit failed");
EVP_MD_CTX_free(mdctx); EVP_PKEY_free(pkey); return -1;
/* Подпись публикуется только после успешного EVP_DigestSign. */
int ma_sign_block(const uint8_t* key, const uint8_t* data, size_t len, uint8_t signature[64]) {
if (signature) memset(signature, 0, 64);
if (!key || (!data && len) || !signature) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Ed25519 invalid key/data/output length=%zu", len); return -1;
}
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, key, 32);
if (!pkey) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Ed25519 key allocation failed"); return -1; }
EVP_MD_CTX* ctx = EVP_MD_CTX_new();
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Ed25519 context allocation failed"); EVP_PKEY_free(pkey); return -1; }
uint8_t result[64]; size_t size = sizeof(result);
int ok = EVP_DigestSignInit(ctx, NULL, NULL, NULL, pkey) == 1 && EVP_DigestSign(ctx, result, &size, data, len) == 1;
EVP_MD_CTX_free(ctx); EVP_PKEY_free(pkey);
if (!ok || size != sizeof(result)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "Ed25519 signing failed length=%zu", size); return -1;
}
memcpy(signature, result, sizeof(result));
return 0;
}
size_t siglen = 64;
int rc = EVP_DigestSign(mdctx, sig_out, &siglen, data, len);
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(pkey);
if (rc != 1 || siglen != 64) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_sign_block: EVP_DigestSign failed rc=%d len=%zu", rc, siglen);
/* Источник открыт до сравнения идентичности; назначение меняется только после полной записи и закрытия. */
int ma_copy_file(const char* source, const char* destination) {
FILE* input = file_io_open_read(source);
if (!input) return -1;
struct file_io_temp output = {0};
const char* phase = "validate paths";
int status = file_io_paths(source, destination);
uint64_t total = 0;
if (status == 0) phase = "create temporary";
if (status == 0 && file_io_temp_open(&output, destination)) status = -1;
if (status == 0) {
uint8_t buffer[65536]; size_t count;
while (1) {
phase = "read source"; count = fread(buffer, 1, sizeof(buffer), input);
if (ferror(input)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "copy read failed path=%s errno=%d", source, errno); status = -1; break;
}
if (!count) break;
phase = "write temporary";
if (fwrite(buffer, 1, count, output.file) != count) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "copy write failed path=%s requested=%zu errno=%d", output.temporary, count, errno);
status = -1; break;
}
total += count;
}
}
if (fclose(input)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "copy source close failed path=%s errno=%d", source, errno);
phase = "close source"; status = -1;
}
if (status == 0) phase = "flush/close temporary";
if (status == 0 && file_io_temp_finish(&output)) status = -1;
if (status == 0) phase = "publish";
if (status == 0 && file_io_temp_publish(&output, source, destination)) status = -1;
file_io_temp_discard(&output);
if (status < 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "copy failed phase=%s source=%s destination=%s bytes=%llu",
phase, source, destination ? destination : "(null)", (unsigned long long)total);
return -1;
}
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "copy complete source=%s destination=%s bytes=%llu same_file=%d",
source, destination, (unsigned long long)total, status == 1);
return 0;
}
int ma_copy_file(const char* src, const char* dst) {
if (!src || !dst) return -1;
if (strcmp(src, dst) == 0) return 0;
FILE* s = fopen(src, "rb");
if (!s) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_copy_file: cannot open src %s", src); return -1; }
FILE* d = fopen(dst, "wb");
if (!d) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_copy_file: cannot create dst %s", dst); fclose(s); return -1; }
uint8_t buf[65536];
size_t rd;
while ((rd = fread(buf, 1, sizeof(buf), s)) > 0) {
if (fwrite(buf, 1, rd, d) != rd) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_copy_file: write error at %s", dst);
fclose(s); fclose(d); return -1;
}
int64_t ma_file_size(const char* path) {
struct file_io_info info;
int status = file_io_inspect(path, 1, &info);
if (status == 1) { DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "file size missing path=%s", path); return -1; }
if (status < 0) return -1;
if (info.type != FILE_IO_REGULAR) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "file size requires regular file path=%s type=%d", path, info.type); return -1;
}
int error = ferror(s);
if (fclose(s)) error = 1;
if (fclose(d)) error = 1;
if (error) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "ma_copy_file: read/close failed src=%s dst=%s", src, dst);
return error ? -1 : 0;
return info.size;
}
int ma_file_size(const char* path) {
struct stat st;
if (stat(path, &st) != 0) return -1;
return (int)(st.st_size > 0 ? st.st_size : 0);
int ma_uuid(uint8_t uuid_out[16]) {
if (!uuid_out) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "UUID requires output"); return -1; }
int rc = RAND_bytes(uuid_out, 16);
if (rc != 1) {
memset(uuid_out, 0, 16);
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "UUID RNG failed rc=%d", rc); return -1;
}
return 0;
}
void ma_uuid(uint8_t uuid_out[16]) {
RAND_bytes(uuid_out, 16);
}
/* ─── Рекурсивный обход каталога (кроссплатформенный) ─── */
struct ma_walk_ctx { ma_dir_walk_cb cb; void* arg; };
static int ma_walk_recursive(const char* directory, struct ma_walk_ctx* ctx, unsigned depth);
#ifndef _WIN32
static void ma_walk_recursive(const char* dir, struct ma_walk_ctx* wc) {
DIR* d = opendir(dir);
if (!d) return;
struct dirent* e;
while ((e = readdir(d)) != NULL) {
const char* n = e->d_name;
if (n[0] == '.' && (n[1] == '\0' || (n[1] == '.' && n[2] == '\0'))) continue;
char full[4096];
snprintf(full, sizeof(full), "%s/%s", dir, n);
struct stat st;
if (lstat(full, &st) != 0) continue;
if (S_ISDIR(st.st_mode)) ma_walk_recursive(full, wc);
else if (S_ISREG(st.st_mode)) wc->cb(wc->arg, full, (int64_t)st.st_size);
/* Одна запись: ссылки/устройства пропускаются, ошибки делают обход неполным. */
static int ma_walk_entry(const char* directory, const char* name, struct ma_walk_ctx* ctx, unsigned depth) {
if (!strcmp(name, ".") || !strcmp(name, "..")) return 0;
size_t length = strlen(directory) + strlen(name) + 2;
if (length > FILE_IO_PATH_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory entry path too long directory=%s name=%s", directory, name); return -1;
}
closedir(d);
char* full = u_malloc((uint32_t)length);
if (!full) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory entry allocation failed bytes=%zu", length); return -1; }
snprintf(full, length, "%s/%s", directory, name);
struct file_io_info info;
if (file_io_inspect(full, 0, &info)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory entry inspection failed path=%s", full); u_free(full); return -1;
}
int status = 0;
if (info.type == FILE_IO_DIRECTORY) status = ma_walk_recursive(full, ctx, depth + 1);
if (info.type == FILE_IO_REGULAR) ctx->cb(ctx->arg, full, info.size);
u_free(full); return status;
}
#else
static void ma_walk_recursive(const char* dir, struct ma_walk_ctx* wc) {
char pattern[4096];
snprintf(pattern, sizeof(pattern), "%s/*", dir);
WIN32_FIND_DATAA fd;
HANDLE h = FindFirstFileA(pattern, &fd);
if (h == INVALID_HANDLE_VALUE) return;
do {
const char* n = fd.cFileName;
if (n[0] == '.' && (n[1] == '\0' || (n[1] == '.' && n[2] == '\0'))) continue;
char full[4096];
snprintf(full, sizeof(full), "%s/%s", dir, n);
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ma_walk_recursive(full, wc);
else {
LARGE_INTEGER sz; sz.HighPart = fd.nFileSizeHigh; sz.LowPart = fd.nFileSizeLow;
wc->cb(wc->arg, full, (int64_t)sz.QuadPart);
}
} while (FindNextFileA(h, &fd));
FindClose(h);
}
#endif
int ma_dir_walk(const char* dir, ma_dir_walk_cb cb, void* arg) {
if (!dir || !dir[0] || !cb) return -1;
struct ma_walk_ctx wc; wc.cb = cb; wc.arg = arg;
ma_walk_recursive(dir, &wc);
return 0;
/* Ограниченный по глубине обход без перехода по конечным ссылкам. */
static int ma_walk_recursive(const char* directory, struct ma_walk_ctx* ctx, unsigned depth) {
if (depth > MA_WALK_MAX_DEPTH) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory depth limit path=%s depth=%u", directory, depth); return -1;
}
struct file_io_info info;
if (file_io_inspect(directory, 0, &info)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory inspection failed path=%s", directory); return -1;
}
if (info.type == FILE_IO_LINK) return 0;
if (info.type != FILE_IO_DIRECTORY) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory walk requires directory path=%s type=%d", directory, info.type); return -1;
}
int status = 0;
#ifdef _WIN32
struct ma_search { wchar_t pattern[FILE_IO_PATH_MAX]; char name[FILE_IO_PATH_MAX]; WIN32_FIND_DATAW data; };
struct ma_search* search = u_calloc(1, sizeof(*search));
if (!search) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory search allocation failed"); return -1; }
if (file_io_wide(directory, search->pattern)) { u_free(search); return -1; }
size_t length = wcslen(search->pattern);
if (length + 3 > FILE_IO_PATH_MAX) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory search path too long path=%s", directory); u_free(search); return -1;
}
memcpy(search->pattern + length, L"/*", 3 * sizeof(wchar_t));
HANDLE find = FindFirstFileW(search->pattern, &search->data);
if (find == INVALID_HANDLE_VALUE) {
DWORD error = GetLastError();
u_free(search);
if (error == ERROR_FILE_NOT_FOUND) return 0; // Открытый выше каталог пуст.
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory open failed path=%s winerr=%lu", directory, error); return -1;
}
do {
if (!WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, search->data.cFileName, -1,
search->name, sizeof(search->name), NULL, NULL)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory entry UTF-8 conversion failed path=%s winerr=%lu", directory, GetLastError());
status = -1; break;
}
if (ma_walk_entry(directory, search->name, ctx, depth)) { status = -1; break; }
if (!FindNextFileW(find, &search->data)) {
DWORD error = GetLastError();
if (error != ERROR_NO_MORE_FILES) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory read failed path=%s winerr=%lu", directory, error); status = -1;
}
break;
}
} while (1);
if (!FindClose(find)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory close failed path=%s winerr=%lu", directory, GetLastError()); status = -1;
}
u_free(search);
#else
DIR* dir = opendir(directory);
if (!dir) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory open failed path=%s errno=%d", directory, errno); return -1; }
while (1) {
errno = 0;
struct dirent* entry = readdir(dir);
if (!entry) {
if (errno) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory read failed path=%s errno=%d", directory, errno); status = -1; }
break;
}
if (ma_walk_entry(directory, entry->d_name, ctx, depth)) { status = -1; break; }
}
if (closedir(dir)) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory close failed path=%s errno=%d", directory, errno); status = -1; }
#endif
return status;
}
int ma_dir_walk(const char* directory, ma_dir_walk_cb callback, void* arg) {
if (!directory || !*directory || strlen(directory) >= FILE_IO_PATH_MAX || !callback) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "directory walk invalid path/callback"); return -1;
}
struct ma_walk_ctx ctx = {.cb = callback, .arg = arg};
int result = ma_walk_recursive(directory, &ctx, 0);
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "directory walk finished path=%s result=%d", directory, result);
return result;
}
+28 -13
View File
@@ -1,7 +1,8 @@
/* media_async — владелец фоновых задач обработки файлов и криптографии.
* Каждая work выполняется в отдельном pthread; done — в потоке uasync.
* create/submit/destroy вызываются из этого потока. destroy ждёт workers и
* завершает ожидающие done с CANCELLED, пока данные вызывающего ещё существуют. */
* Каждая work выполняется в отдельном pthread; done — в потоке владельца uasync.
* create/submit/destroy вызываются из этого потока; ua живёт до окончания destroy.
* destroy ждёт workers и завершает ожидающие done с CANCELLED, пока их данные ещё существуют.
* Отмена не прерывает work и не откатывает файловые изменения. Примитивы ниже синхронные, подходят для workers. */
#ifndef MEDIA_ASYNC_H
#define MEDIA_ASYNC_H
@@ -17,33 +18,47 @@ struct UASYNC;
struct media_async;
typedef void (*ma_work_fn)(void* data); // рабочий поток; не обращаться к состоянию uasync
typedef void (*ma_done_fn)(void* arg, int err); // 0=work завершён, -1=ошибка запуска, -2=отмена
typedef void (*ma_done_fn)(void* arg, int err); // 0=work завершён, -1=отказ запуска/аргументы, -2=остановка владельца
/* Создать владельца задач; NULL при ошибке выделения памяти. */
struct media_async* media_async_create(void);
#define MEDIA_ASYNC_CANCELLED (-2)
#define MEDIA_ASYNC_MAX_TASKS 16
#define MA_WALK_MAX_DEPTH 128
/* Вызывать вне callbacks uasync. Ждёт работы, отменяет posted completion,
* вызывает ожидающие done и освобождает ma; NULL допустим. */
* вызывает ожидающие done и освобождает ma; NULL допустим. Повторный вход во время destroy отклоняется.
* После возврата ma недействителен. Не вызывать из work или callbacks uasync, в том числе обычного done. */
void media_async_destroy(struct media_async* ma);
/* Передать задачу (не более 16 активных); data/arg остаются у вызывающего до done. При ошибке done
* может быть вызван синхронно. Ошибку самой work передают через data, а не err. */
/* Передать задачу: ma/ua/work/done обязательны, data/arg могут быть NULL и остаются у вызывающего до done.
* Лимит 16 включает завершённые workers до обработки done. При отказе done вызывается синхронно ровно один раз;
* при done=NULL только логируется ошибка. У closing-владельца возврат через done с CANCELLED.
* err=0 означает завершение work, её собственную ошибку передают через data. После принятия done — ровно один раз. */
void media_async_submit(struct media_async* ma, struct UASYNC* ua,
ma_work_fn work, void* data,
ma_done_fn done, void* arg);
/* Синхронные примитивы для worker: SHA256 файла, Ed25519-подпись, копирование. 0/-1. */
/* SHA256 обычного файла path (UTF-8); hash_out — 32 байта, обнуляется при ошибке. 0/-1. */
int ma_sha256_file(const char* path, uint8_t hash_out[32]);
/* Подписать len байт Ed25519 ключом 32 байта; sig_out — 64 байта, обнуляется при ошибке.
* data=NULL допустим только при len=0. 0/-1. */
int ma_sign_block(const uint8_t* ed25519_privkey,
const uint8_t* data, size_t len, uint8_t sig_out[64]);
/* Копировать regular src в отсутствующий/regular dst через соседний temp; 0/-1. Пути UTF-8, <4096 байт,
* причём dst вместе с суффиксом .tmp.<32 hex> и NUL должен помещаться в 4096 байт.
* Один существующий файл (включая ./ и hardlink) — успешный no-op. Конечные ссылки назначения отклоняются.
* Ошибка сохраняет src и прежний dst; src/каталоги не менять во время вызова. POSIX результат имеет права 0600.
* Публикация происходит внутри вызова; последующая отмена media_async не откатывает копирование. */
int ma_copy_file(const char* src, const char* dst);
/* Размер файла в байтах (int; для файлов, помещающихся в этот тип), -1 при ошибке stat. */
int ma_file_size(const char* path);
/* Заполнить 16 случайных байт идентификатора. */
void ma_uuid(uint8_t uuid_out[16]);
/* Размер regular-файла path (UTF-8): 0..INT64_MAX байт; -1 при отсутствии/ошибке/другом типе. */
int64_t ma_file_size(const char* path);
/* Заполнить 16 криптографических случайных байт; 0/-1. При отказе выход обнуляется; NULL — ошибка. */
int ma_uuid(uint8_t uuid_out[16]);
/* Рекурсивный обход каталога: для каждого regular-файла вызывается cb(full_path, size).
* Пути строятся с '/' (принимается и POSIX, и Win32 API). 0 = ok, -1 = неверные аргументы. */
* Пути UTF-8 с '/', <4096 байт; глубина не более MA_WALK_MAX_DEPTH. Ссылки/reparse points пропускаются,
* включая корень. 0 — полный обход, -1 — неверные аргументы, отсутствие корня, I/O или превышение лимита.
* Callback синхронный, full_path заимствован до его возврата. При ошибке прежние callbacks не откатываются. */
typedef void (*ma_dir_walk_cb)(void* arg, const char* full_path, int64_t size);
int ma_dir_walk(const char* dir, ma_dir_walk_cb cb, void* arg);
+3 -2
View File
@@ -20,7 +20,8 @@ typedef int (*file_transfer_lookup_fn)(void* arg, uint64_t group, uint64_t peer,
typedef void (*file_transfer_done_fn)(void* arg, int error);
/* 0 в done означает точный приём size байт и закрытие файла, а не custody ACK.
* Возврат NULL означает отказ запуска, done не вызывается. cancel вызывает done(-2).
* Возврат NULL означает отказ запуска, done не вызывается.
* cancel синхронно вызывает done(-2); ошибка fclose заменяет результат на -1, в том числе при отмене.
* Handle перестаёт существовать перед вызовом done, его нужно обнулить в callback. */
struct file_transfer* file_transfer_create(struct UTUN_INSTANCE* inst, file_transfer_lookup_fn lookup, void* arg);
void file_transfer_destroy(struct file_transfer* ft);
@@ -28,7 +29,7 @@ struct file_transfer_op* file_transfer_pull(struct file_transfer* ft, uint64_t g
const uint8_t object[16], uint64_t size, const char* destination,
file_transfer_done_fn done, void* arg);
void file_transfer_cancel(struct file_transfer_op* op);
/* Отозвать все операции объекта (TTL/delete); callbacks приёма вызываются с -2. */
/* Отозвать все операции объекта (TTL/delete); callbacks приёма вызываются синхронно с -2, при ошибке fclose — с -1. */
void file_transfer_cancel_object(struct file_transfer* ft, const uint8_t object[16]);
#endif
+31 -10
View File
@@ -1,8 +1,8 @@
/* media_delivery — канальные медиафайлы: поиск держателей блоков, выдача/relay и репликация доступности.
* Суперузлы обмениваются block_availability, держатели отдают подписанные блоки, media_download собирает файл.
* init создаёт per-instance состояние, bind подключает протокол к router; destroy отменяет сетевую работу.
* Все операции — uasync-поток; долгие файловые/crypto-задачи выполняет media_async.
* E2E-файлы личных бесед используют dm_media/file_transfer, а не канальную таблицу доступности. */
/* media_delivery — поиск держателей и доставка блоков файлов CHAT-канала, включая промежуточную раздачу (relay).
* После регистрации файла через media_index автор вызывает announce_media; получатель загружает его через media_download.
* Суперузлы обмениваются сведениями о наличии блоков; держатели выдают подписанные блоки, загрузчик собирает файл.
* Чат-сервис вызывает init и затем destroy; bind — другое имя init, вызывать оба последовательно нельзя.
* Все операции — uasync; долгие файловые/crypto-задачи выполняет media_async. Для файлов DM используется dm_media/file_transfer. */
#ifndef MEDIA_DELIVERY_H
#define MEDIA_DELIVERY_H
@@ -16,7 +16,7 @@ extern "C" {
struct UTUN_INSTANCE;
/* ── константы ── */
// ── константы ──
#define MEDIA_MAX_REPL_INFLIGHT 4
#define MEDIA_REPL_TIMEOUT_TB 20000 // начальный таймаут 2s (0.1ms)
@@ -29,7 +29,7 @@ struct UTUN_INSTANCE;
#define MEDIA_DL_STALL_TIMEOUT_TB 20000 // таймаут простоя загрузки 2s (0.1ms)
#define MEDIA_DL_MAX_ATTEMPTS 3 // попыток на блок до ошибки
/* ── структуры ── */
// ── структуры ──
struct media_super_peer {
struct ll_entry ll; // индекс по peer_node_id (8 байт)
@@ -45,7 +45,7 @@ struct media_super_peer {
struct media_delivery_ctx* md; // обратная ссылка
};
/* per-file download tracking (source node admission control) */
// per-file download tracking (source node admission control)
struct md_file_load {
struct ll_entry ll; // индекс по media_id (16 байт)
@@ -132,12 +132,27 @@ struct media_delivery_ctx {
struct UTUN_INSTANCE* inst;
};
// ── Жизненный цикл и роль узла ──
// Создать таблицы/контекст и подключить router и подписки; 0/-1. Общая БД и группы должны быть готовы.
int media_delivery_init(struct UTUN_INSTANCE* inst);
// Отменить загрузки/выдачи/репликацию и снять подписки; вызывать до удаления групп и общей БД.
void media_delivery_destroy(struct UTUN_INSTANCE* inst);
// Вызывает init целиком; использовать вместо init, а не как следующий этап. 0/-1.
int media_delivery_bind(struct UTUN_INSTANCE* inst);
// Включить/выключить локальную работу суперузла и обмен доступностью блоков.
void media_delivery_set_supernode(struct UTUN_INSTANCE* inst, int is_supernode);
// ── Внутренний учёт выдач ──
// Внутреннее завершение исходящей выдачи: снять единицу active_streams и отметить завершение для диагностики.
void media_delivery_stream_done(struct UTUN_INSTANCE* inst);
// ── Анонсы доступности блоков ──
/* Автор анонсирует свои блоки суперузлам CHAT-группы (HAVE_BLOCK), чтобы первый
* скачивающий мог найти держателя через QUERY. Вызывается после регистрации медиа. */
void media_delivery_announce_media(struct UTUN_INSTANCE* inst, uint64_t group_id,
@@ -154,19 +169,25 @@ void media_delivery_send_have_block(struct UTUN_INSTANCE* inst, uint64_t group_i
* block_availability для уже скачанных ранее медиа. Идемпотентно на суперузле. */
void media_delivery_announce_local_blocks(struct UTUN_INSTANCE* inst);
// ── Отмена работы канала ──
/* Отменить всё активное, что связано с каналом (локальное удаление канала):
* загрузки с group_id, их relay-контексты и super_peers этой группы. */
void media_delivery_cancel_channel(struct UTUN_INSTANCE* inst, uint64_t group_id);
/* relay block context helpers (used by media_download.c) */
// ── Внутренние операции загрузчика: relay ──
struct relay_block_ctx* md_relay_find(struct media_delivery_ctx* md, const uint8_t* block_id);
struct relay_block_ctx* md_relay_add(struct media_delivery_ctx* md,
const uint8_t* block_id, const uint8_t* media_id, uint32_t chunk);
void md_relay_remove(struct media_delivery_ctx* md, const uint8_t* block_id);
/* file_load helpers (per-file source download tracking) */
// ── Внутренний учёт нагрузки на файл ──
struct md_file_load* md_file_load_find(struct media_delivery_ctx* md, const uint8_t* media_id);
int md_file_load_inc(struct media_delivery_ctx* md, const uint8_t* media_id, uint64_t node_id);
void md_file_load_dec(struct media_delivery_ctx* md, const uint8_t* media_id, uint64_t node_id);
#ifdef __cplusplus
+20 -11
View File
@@ -50,6 +50,10 @@ static int md_dl_block_index(struct media_download* dl, const uint8_t* block_id)
return -1;
}
static int md_dl_block_inflight(const struct media_download_block* block) {
return block->state == MD_BLK_CONN || block->state == MD_BLK_REQ || block->state == MD_BLK_RECV;
}
static void md_dl_reset_block_file(struct media_download* dl, int bi) {
char tmp[2048];
snprintf(tmp, sizeof(tmp), "%s.chunk_%d", dl->dest_path, bi);
@@ -60,7 +64,10 @@ static void md_dl_reset_block_file(struct media_download* dl, int bi) {
static struct media_download_peer* md_dl_get_peer(struct media_download* dl, uint64_t node_id) {
for (int pi = 0; pi < dl->num_peers; pi++)
if (dl->peers[pi].node_id == node_id) return &dl->peers[pi];
if (dl->num_peers >= MD_MAX_PEERS) return NULL;
if (dl->num_peers >= MD_MAX_PEERS) {
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "%s: peer limit reached node=%016llx peers=%d", MDL_ID,
(unsigned long long)node_id, dl->num_peers); return NULL;
}
struct media_download_peer* p = &dl->peers[dl->num_peers++];
memset(p, 0, sizeof(*p));
p->node_id = node_id;
@@ -183,11 +190,11 @@ static int md_dl_request_block(struct media_download* dl, int bi) {
dl->inflight++;
struct UTUN_INSTANCE* inst = dl->inst;
if (instance_find_conn(inst, holder)) {
if ((p->cm_handle && p->connected) || instance_find_conn(inst, holder)) {
p->connected = 1;
b->state = MD_BLK_REQ;
md_dl_send_block_req(inst, holder, dl, bi);
} else {
} else if (!p->cm_handle) {
struct TOPO_GROUP* grp = inst->topo_groups ? topo_groups_find(inst->topo_groups, dl->group_id) : NULL;
if (grp && grp->conn_mgr) {
int rc = conn_mgr_open(inst, grp->group_id, holder, md_dl_conn_cb, dl, &p->cm_handle);
@@ -204,7 +211,8 @@ static int md_dl_request_block(struct media_download* dl, int bi) {
b->state = MD_BLK_REQ;
md_dl_send_block_req(inst, holder, dl, bi);
}
}
} else DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "%s: waiting for existing CM request node=%016llx block=%d handle=%p",
MDL_ID, (unsigned long long)holder, bi, (void*)p->cm_handle);
return 0;
}
@@ -359,9 +367,11 @@ void md_dl_conn_cb(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_i
}
}
} else {
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "%s: conn FAIL node=0x%016llx event=%d", MDL_ID, (unsigned long long)node_id, event);
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "%s: conn FAIL node=0x%016llx event=%d closing handle=%p",
MDL_ID, (unsigned long long)node_id, event, (void*)h);
struct media_download_peer* p = md_dl_get_peer(dl, node_id);
if (p) { p->connected = 0; p->cm_handle = NULL; }
conn_mgr_close(h); // DOWN/TIMEOUT не освобождают handle; отменяем в том числе поздний UP.
uint64_t now = get_time_tb();
int failed = 0;
for (int bi = 0; bi < dl->num_blocks; bi++) {
@@ -479,9 +489,9 @@ void media_download_handle_query_resp(struct UTUN_INSTANCE* inst,
dl->last_activity_tb = get_time_tb();
/* сбросить holders у незавершённых блоков (re-QUERY пересобирает список) */
/* QUERY меняет только ожидающие блоки: у активных сохраняем текущего держателя и его индекс. */
for (int bi = 0; bi < dl->num_blocks; bi++) {
if (dl->blocks[bi].state != MD_BLK_DONE) {
if (dl->blocks[bi].state != MD_BLK_DONE && !md_dl_block_inflight(&dl->blocks[bi])) {
dl->blocks[bi].num_holders = 0;
dl->blocks[bi].holder_idx = -1;
}
@@ -492,7 +502,7 @@ void media_download_handle_query_resp(struct UTUN_INSTANCE* inst,
int bi = md_dl_block_index(dl, entries[i].block_id);
if (bi < 0) continue;
struct media_download_block* b = &dl->blocks[bi];
if (b->state == MD_BLK_DONE) continue;
if (b->state == MD_BLK_DONE || md_dl_block_inflight(b)) continue;
int dup = 0;
for (int h = 0; h < b->num_holders; h++) if (b->holders[h] == entries[i].node_id) { dup = 1; break; }
if (!dup && b->num_holders < MD_MAX_HOLDERS_PER_BLOCK)
@@ -502,13 +512,12 @@ void media_download_handle_query_resp(struct UTUN_INSTANCE* inst,
/* fallback: блок без держателя — автор */
for (int bi = 0; bi < dl->num_blocks; bi++) {
struct media_download_block* b = &dl->blocks[bi];
if (b->state == MD_BLK_DONE) continue;
if (b->state == MD_BLK_DONE || md_dl_block_inflight(b)) continue;
if (b->num_holders == 0 && dl->author_node_id && dl->author_node_id != inst->node_id)
b->holders[b->num_holders++] = dl->author_node_id;
}
/* пересобрать peers (уникальные node_id всех держателей) */
dl->num_peers = 0;
/* Реестр peers живёт до finish: повторный QUERY не должен терять ожидающие CM handles. */
for (int bi = 0; bi < dl->num_blocks; bi++) {
struct media_download_block* b = &dl->blocks[bi];
for (int h = 0; h < b->num_holders; h++)
+5 -3
View File
@@ -1,5 +1,6 @@
/* media_download — загрузка канального файла по media_index_result: QUERY держателей → блоки → проверка/сборка.
* Состояние хранит inst->media_delivery; каждый подключённый держатель удерживается через CM handle.
* Состояние хранит inst->md. При открытии подключения через conn_mgr загрузчик получает CM handle.
* Найденный ETCP_CONN используется без нового handle; при отсутствии conn_mgr запрос отправляется best-effort без handle.
* Старт копирует метаданные result; callbacks и обработчики — uasync-поток, файловая проверка — media_async.
* Failover/таймер простоя ограничивают повторы; done сообщает результат, а не гарантирует доставку другим участникам. */
#ifndef MEDIA_DOWNLOAD_H
@@ -51,7 +52,7 @@ struct media_download_block {
struct media_download_peer {
uint64_t node_id;
uint8_t connected;
struct CONN_MGR_HANDLE* cm_handle;
struct CONN_MGR_HANDLE* cm_handle; /* Один запрос на держателя; закрывается после DOWN/TIMEOUT или завершения загрузки. */
};
/* ── внутреннее состояние загрузки (доступно для тестов) ── */
@@ -118,7 +119,8 @@ void md_dl_conn_cb(struct CONN_MGR_HANDLE* h, uint64_t node_id, uint64_t group_i
/* ── тестовые хуки (для детерминированных юнит-тестов с «фейковым» временем) ──
* md_dl_check_stall — проверка простоя: возвращает 0 (активна) / -1 (провал, нужно finish).
* md_dl_failover_block — фейловер одного блока на следующий держатель: 0 / -1 (попытки исчерпаны).
* md_dl_failover_block — переход блока к следующему держателю: 0, включая DONE/FAILED и ожидание новых держателей;
* -1 — исчерпаны попытки или не удалось начать запрос (лимит peers, ошибка conn_mgr_open).
* md_dl_dump — дамп состояния загрузки в лог (DEBUG_CATEGORY_MEDIA). */
int md_dl_check_stall(struct media_download* dl, uint64_t now_tb);
int md_dl_failover_block(struct media_download* dl, int bi, uint64_t now_tb);
+74 -49
View File
@@ -4,13 +4,16 @@
#include "../../lib/u_async.h"
#include "../../lib/mem.h"
#include "../../lib/debug_config.h"
#include "../../lib/file_io.h"
#include "../../lib/platform_compat.h"
#include <openssl/evp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>
#define MEDIA_BLOCK_MIN (10 * 1024 * 1024)
#define MEDIA_BLOCK_MAX (25 * 1024 * 1024)
@@ -105,7 +108,7 @@ int media_index_init(sqlite3* db) {
int media_calc_block_size(uint64_t file_size) {
if (file_size == 0) return 0;
uint64_t target = (file_size + MEDIA_BLOCK_TARGET - 1) / MEDIA_BLOCK_TARGET;
uint64_t target = (file_size - 1) / MEDIA_BLOCK_TARGET + 1;
if (target < MEDIA_BLOCK_MIN) target = MEDIA_BLOCK_MIN;
if (target > MEDIA_BLOCK_MAX) target = MEDIA_BLOCK_MAX;
return (int)target;
@@ -151,8 +154,7 @@ int media_index_delete_channel(sqlite3* db, const char* chat_id, const char* med
}
int media_index_generate_uuid(uint8_t uuid_out[16]) {
ma_uuid(uuid_out);
return 0;
return ma_uuid(uuid_out);
}
void media_index_result_free(struct media_index_result* result) {
@@ -243,54 +245,71 @@ struct mi_reg_ctx {
int err;
};
/* Только полная подготовка допускает commit; любая ошибка оставляет result для обычной очистки done. */
static void mi_reg_work(void* data) {
struct mi_reg_ctx* ctx = (struct mi_reg_ctx*)data;
if (ctx->copy_file && ma_copy_file(ctx->src_path, ctx->dest_path) != 0) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index: copy failed %s -> %s", ctx->src_path, ctx->dest_path);
ctx->err = -10; return;
}
int64_t fsize = (int64_t)ma_file_size(ctx->dest_path);
if (fsize <= 0) { ctx->err = -20; return; }
ctx->result.file_size = fsize;
int64_t bs = (int64_t)media_calc_block_size((uint64_t)fsize);
int nb = (int)((fsize + bs - 1) / bs);
ctx->result.block_size = bs;
ctx->result.num_blocks = nb;
if (ma_sha256_file(ctx->dest_path, ctx->result.content_hash) != 0) {
ctx->err = -30; return;
}
ma_uuid(ctx->result.media_id);
ctx->result.block_ids = u_malloc((size_t)nb * 16);
ctx->result.block_sigs = u_malloc((size_t)nb * 64);
if (!ctx->result.block_ids || !ctx->result.block_sigs) { ctx->err = -40; return; }
FILE* f = fopen(ctx->dest_path, "rb");
if (!f) { ctx->err = -50; return; }
for (int n = 0; n < nb; n++) {
int64_t off = n * bs;
int64_t sz = (n == nb - 1) ? fsize - off : bs;
uint8_t* buf = u_malloc((size_t)sz);
if (!buf) { fclose(f); ctx->err = -60; return; }
fseeko(f, (off_t)off, SEEK_SET);
size_t rd = fread(buf, 1, (size_t)sz, f);
if (rd != (size_t)sz) { u_free(buf); fclose(f); ctx->err = -70; return; }
ma_uuid(ctx->result.block_ids + n * 16);
if (ma_sign_block(ctx->ed25519_privkey, buf, (size_t)sz,
ctx->result.block_sigs + n * 64) != 0) {
u_free(buf); fclose(f); ctx->err = -80; return;
struct mi_reg_ctx* ctx = data;
FILE* file = NULL; uint8_t* buffer = NULL;
const char* phase = "copy";
ctx->err = -10;
if (ctx->copy_file && ma_copy_file(ctx->src_path, ctx->dest_path)) goto done;
phase = "size"; ctx->err = -20;
int64_t size = ma_file_size(ctx->dest_path);
if (size <= 0) goto done;
int64_t block = media_calc_block_size((uint64_t)size);
uint64_t count = (uint64_t)(size / block + (size % block != 0));
phase = "block count"; ctx->err = -21;
if (count > INT_MAX || count > UINT32_MAX / 64) goto done;
ctx->result.file_size = size; ctx->result.block_size = block; ctx->result.num_blocks = (int)count;
phase = "hash"; ctx->err = -30;
if (ma_sha256_file(ctx->dest_path, ctx->result.content_hash)) goto done;
phase = "media UUID"; ctx->err = -31;
if (ma_uuid(ctx->result.media_id)) goto done;
phase = "block arrays"; ctx->err = -40;
ctx->result.block_ids = u_malloc((uint32_t)count * 16);
ctx->result.block_sigs = u_malloc((uint32_t)count * 64);
if (!ctx->result.block_ids || !ctx->result.block_sigs) goto done;
phase = "open"; ctx->err = -50;
file = file_io_open_read(ctx->dest_path);
if (!file) goto done;
for (int n = 0; n < ctx->result.num_blocks; n++) {
int64_t offset = (int64_t)n * block;
size_t length = (size_t)(n == ctx->result.num_blocks - 1 ? size - offset : block);
phase = "block allocation"; ctx->err = -60;
buffer = u_malloc((uint32_t)length);
if (!buffer) goto done;
phase = "seek"; ctx->err = -70;
#ifdef _WIN32
int seek_error = _fseeki64(file, offset, SEEK_SET);
#else
int seek_error = (int64_t)(off_t)offset != offset || fseeko(file, (off_t)offset, SEEK_SET);
#endif
if (seek_error) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index seek failed path=%s offset=%lld errno=%d",
ctx->dest_path, (long long)offset, errno); goto done;
}
u_free(buf);
phase = "read";
size_t received = fread(buffer, 1, length, file);
if (received != length) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index read failed path=%s offset=%lld wanted=%zu received=%zu error=%d",
ctx->dest_path, (long long)offset, length, received, ferror(file)); goto done;
}
phase = "block UUID"; ctx->err = -81;
if (ma_uuid(ctx->result.block_ids + (size_t)n * 16)) goto done;
phase = "block signature"; ctx->err = -80;
if (ma_sign_block(ctx->ed25519_privkey, buffer, length, ctx->result.block_sigs + (size_t)n * 64)) goto done;
u_free(buffer); buffer = NULL;
}
fclose(f);
phase = "close"; ctx->err = -71;
int close_error = fclose(file); file = NULL;
if (close_error) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index close failed path=%s errno=%d", ctx->dest_path, errno); goto done; }
ctx->err = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "media_index prepared path=%s bytes=%lld blocks=%d",
ctx->dest_path, (long long)size, ctx->result.num_blocks);
done:
if (ctx->err) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index preparation failed phase=%s path=%s error=%d",
phase, ctx->dest_path, ctx->err);
u_free(buffer);
if (file && fclose(file)) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media_index cleanup close failed path=%s errno=%d", ctx->dest_path, errno);
}
static void mi_reg_done(void* arg, int err) {
@@ -325,8 +344,14 @@ void media_index_register_async(
return;
}
if (!*src_path || !*dest_path || strlen(src_path) >= sizeof(((struct mi_reg_ctx*)0)->src_path)
|| strlen(dest_path) >= sizeof(((struct mi_reg_ctx*)0)->dest_path)
|| strlen(chat_id) >= sizeof(((struct mi_reg_ctx*)0)->chat_id)
|| strlen(media_base) >= sizeof(((struct mi_reg_ctx*)0)->media_base)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index register oversized/empty path"); cb(cb_arg, -1, NULL); return;
}
struct mi_reg_ctx* ctx = u_calloc(1, sizeof(*ctx));
if (!ctx) { cb(cb_arg, -1, NULL); return; }
if (!ctx) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media_index context allocation failed"); cb(cb_arg, -1, NULL); return; }
ctx->ua = ua;
ctx->db = db;
Loaded 100 of 276 files, more files were not shown because too many files have changed in this diff. Show more