Repository navigation
feat(tracker): BEP 7 multi-homed announce from every eligible local address - #36
Merged
Merged
Conversation
…i-homing) HTTP and UDP tracker announces now leave from every eligible local address per family, each on a source-bound socket, instead of one primary IPv4 and one primary IPv6 address. Eligible: the first usable address of each other interface that is up (not loopback, link-local, ULA/site-local or IPv4-mapped), at most 4 per family, read from the IpCache snapshot. - Single-homed hosts keep the exact old path (sequential, same binds, same unbound retry, same error preference). - Added sources run through Tracker.Fanout: at most 4 concurrent, and a secondary still running 15 s after the first success is abandoned; primaries always run to their own end. - Each distinct source carries the event once; duplicates and sources that would leave from the primary's address are dropped. Announce still sees one merged, peer-deduplicated result per tracker. - Added UDP sources send ip_field=0; BEP 15 connection_ids are cached per local address and port. - Self-connection checks know every local IPv4 address. - Config: :tracker multihomed_announce, max_sources_per_family, max_concurrent_source_announces, source_grace_ms. - Debug log line [tracker_announce] source_result per source. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7n1Zz63wKTZ8xWKnbaAvT
The source_result debug line now carries event=started|completed|stopped|none, so a local run can confirm each source sent the event once. The event is worked out lazily, only when debug logging is on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J7n1Zz63wKTZ8xWKnbaAvT
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
daniboybye
marked this pull request as ready for review
October 25, 2026 15:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Какво прави
BEP 7 — пълен multi-homed announce. HTTP и UDP announce към tracker-ите вече излиза от всеки подходящ локален адрес на семейството, всеки със собствен source-bound сокет, вместо само от един основен IPv4 и един основен IPv6 адрес. Tracker-ът вижда отделен peer запис за всеки адрес на машината и връща peers, достижими през всеки от тях.
Acceptor.announce_sources/0,Acceptor.announce_sources_from/3): от snapshot-а наIpCache(getifaddrs, опреснява се на 30 s). Първо основният адрес (непроменен), после първият подходящ адрес на всеки друг интерфейс, който е:up. Изключени са loopback, link-local, multicast/reserved, а за добавените IPv6 адреси и ULA (fc00::/7), site-local (fec0::/10) и IPv4-mapped/compatible. Лимит: 4 на семейство.Tracker.Fanout, нов модул): заявките вървят паралелно, най-много 4 едновременно. Основната заявка (:primary) винаги стига до края си със старите timeout-и и retry-и. Добавена заявка (:secondary), която не е отговорила 15 s след първия успешен отговор, се прекъсва (:source_abandoned). Ако заявка гръмне, грешката остава само за нея.started/completed/stopped) веднъж. Дублиран адрес или адрес, от който основната заявка вече излиза (включително route-избраният адрес на unbound заявка), се пропуска.PeerDiscovery.Announceпак получава един слят резултат на tracker URL, така че event bookkeeping-ът (StartedAnnounces,Model.update_event), retry/cooldown и tier логиката работят веднъж на tracker, както досега.{ip, port}(съществуващиятmerge_http_announces/1). Ако всичко се провали, се връща грешката на основната заявка (тя е реалният отговор на tracker-а).Странични поправки, нужни за коректността:
connection_idкешът е по{tracker, local_ip, local_port}вместо{tracker, local_port}. Два сокета, вързани към различни адреси, могат да получат един и същи ephemeral порт, а tracker-ите вързват id-то за адреса на клиента.ip_field = 0(tracker-ът взема адреса от пакета). Основният адрес в полето би накарал tracker-а да запише основния адрес и за тази заявка.Handshakes.global_ip_at_listen_port?/1) знае всички локални IPv4 адреси, не само основния. Tracker-ите вече ще ни връщат и вторичните адреси.Конфигурация (
config :elixir_torrent, :tracker, ...):multihomed_announce(по подразбиранеtrue;falseвръща старото поведение),max_sources_per_family(4),max_concurrent_source_announces(4),source_grace_ms(15 000). При невалидни стойности се ползват тези по подразбиране.Защо
Това беше точката „BEP 7 — full multi-homed announce“ (Medium) в PROTOCOL.md § Not yet implemented. Хост с VPN, второ NIC или tethering досега беше видим за tracker-ите само през един адрес на семейство.
Ключови решения (избрани без питане)
announce_source_ip/3(swap към routed адреса при VPN) и unbound retry-я приeaddrnotavail. Добавените адреси нямат unbound retry: unbound означава маршрута на основния адрес, а той вече е анонсиран, така че retry би дублирал event-а.Acceptor.source_reaches?/3: bound UDPconnect, без пакет на жицата). HTTP tracker, чието име не сме resolve-нали сами, остава само с основния адрес.stoppedсамо от основния би оставил другите записи да висят до изтичане при tracker-а. Ако основната заявка се провали, а вторична мине,Announceотбелязваstartedкато доставен. Следващият announce от основния адрес ще е без event, което tracker-ите приемат като implicit start.multihomed_announce: falseго изключва глобално.:telemetryи не исках да я добавям само за това. Всеки announce пише по един ред на source адрес:[tracker_announce] source_result hash=… tracker=udp://host:port event=started dest=1.2.3.4 family=inet role=secondary source=10.8.0.2 result=ok peers=37feat/bep7-multihomed-announce, както поиска.Как е тествано
test/bep7_multihomed_announce_test.exs(18 теста, без реални интерфейси и без мрежа):announce_sources_from/3върху синтетичниgetifaddrsсписъци: един NIC с няколко IPv6 адреса дава само основния; две NIC + VPN дават по един адрес на интерфейс, основният е първи; ULA/site-local/mapped не се добавят; down/loopback интерфейси се прескачат; приgetifaddrsгрешка списъкът е празен.announce_sources/0с инжектиран snapshot: лимит,multihomed_announce: false, смяна на snapshot-а (махнат интерфейс), стар snapshot без новите полета.source_reaches?/3за адрес, който хостът не притежава, за family mismatch и за loopback дестинация.Tracker.Fanout: ред на резултатите, изолиран crash, лимит на конкурентност, прекъсване на висящ secondary и непрекъсване на висящ primary, без прекъсване преди първия успех.127.0.0.1/.2/.3(инжектирани презannounce_sources:в request opts). Проверяват: всеки различен адрес анонсира точно веднъж и с event-а; дубликатите не добавят заявки; peers са слети и дедупликирани;ip_field = 0за добавените; грешка от един адрес не губи peers-ите на другите; black-holed адрес не задържа отговора; при пълен провал се връща грешката на основния; невалиден config не блокира; private-tracker query (passkey) се пази.sleep. Ако127.0.0.2не може да се bind-не (macOS безlo0alias), интеграционните блокове се skip-ват.mix format --check-formatted,compile --warnings-as-errors(dev и test),scripts/compile-property-deps.sh,mix dialyzer,mix credo --all,mix sobelowиtimeout 60s mix coveralls.jsonминават. 15 последователни пускания на tracker тестовете са стабилни.DHTIPv6RoutingTest, IPv6 тестовете вHTTPTrackerIPv6Test/TrackerUDPLoopbackTest/SourceRouteBindTest/AcceptorDialHandshakeCoverageBatchTest) падат там и на чистmaster. Наборът провали преди и след промяната е идентичен. GitHub CI е истинската проверка за тях.ConnectionIds.Какво не е проверено
eaddrnotavail/няма маршрут и се пропуска, без retry.SO_BINDTODEVICE). На Linux/macOS без source-based routing пакетите му може да излязат през default route и да бъдат изхвърлени upstream. Тогава тази заявка изтича или бива прекъсната от grace прозореца, а останалите не страдат. Описано е в PROTOCOL.md.stoppedприstop_and_serialize/1остава best-effort в съществуващия 15 s бюджет наPeerDiscovery.stopped_announce/1. Таймаутът му се хваща, така че няма срив, ноstoppedот вторичен адрес, който отговаря бавно, може да не стигне до tracker-а. Ще изтече при него.Локална валидация
0. Подготовка
Подготви един реален публичен торент с HTTP и UDP tracker-и. Например официалният Ubuntu/Debian ISO
.torrent(HTTP tracker с IPv4 и IPv6) или magnet сtr=udp://tracker.opentrackr.org:1337/announce. Нужни саsha256sum/shasum -a 256и по желаниеtcpdump.1. Кои адреси ще анонсират
Очаквано: основният адрес е първи. Има по един адрес за всеки друг интерфейс, който е up и има подходящ адрес. Няма loopback,
fe80::,fd..::(ULA), няма втори временен IPv6 от същата карта.2. Debug лог и старт на торент
В друг терминал:
grep -E "\[tracker_announce\] (source_result|http_endpoints)" et-run.logВсеки ред
source_resultпоказваtracker,event,dest,family,role,source(auto= unbound, т.е. маршрутът избира) иresult=ok peers=Nилиresult=error reason=….3. Машина с един адрес на семейство (регресия)
Изключи VPN и второто NIC, остави един IPv4 (и евентуално един IPv6).
Acceptor.announce_sources()дава най-много по 1 адрес на семейство.role=primary, по един ред на семейство (family=inetи, ако tracker-ът има AAAA и машината има IPv6,family=inet6).role=secondary. Първият announce еevent=started, следващите саevent=none.master. Може да сравниш същата сесия сmaster: там няма редовеsource_result, но имаhttp_endpointsиudp family=….4. Няколко адреса (IPv4+IPv6, VPN, второ NIC)
Включи VPN (WireGuard/OpenVPN) или свържи едновременно Ethernet и Wi-Fi. Изчакай до 30 s (
IpCacherefresh) и пусни отновоAcceptor.announce_sources(): новият адрес трябва да е в списъка.Рестартирай торента, за да има свеж
started:Очаквано за всеки tracker адрес:
role=primaryплюс по единrole=secondary source=<адрес на VPN/второто NIC>на семейство;sourceсе среща точно веднъж сevent=startedв рунда (никакви дубли от един адрес);result=errorилиreason=:source_abandonedнай-много ~15 s след първия успех, а торентът продължава с peers от другите.Потвърждение на жицата (по избор):
sudo tcpdump -ni any 'udp port 1337 or tcp port 80 or tcp port 443 or udp port 6969'Трябва да видиш announce пакети/SYN-ове с различни source адреси към един и същи tracker.
Брой peers: сравни
peers=по source редове. Обединеният списък ([peer_dial] … discovered=) трябва да е поне колкото най-големия единичен.5. Смяна на интерфейсите по време на сесия
Докато торентът тече, изключи VPN (или извади кабела), изчакай 30 s и го включи пак.
Process.alive?(Process.whereis(Acceptor.IpCache))даваtrue,ElixirTorrent.stats(pid)отговаря.source_route_probe_failed/udp_open_failedна debug ниво за изчезналия адрес. Няма crash и няма[error].Acceptor.announce_sources().6. Обикновено сваляне и seed (регресия)
sha256sum /tmp/et-bep7/ubuntu-*.iso).event=completedпо веднъж на source към всеки tracker.ElixirTorrent.stats(pid, [:speed, :downloaded])трябва да показва upload > 0 (при достатъчно голям swarm).ElixirTorrent.stop_and_serialize(hash)трябва да дадеevent=stoppedпо веднъж на source.ElixirTorrent.download_magnet/2) с UDP tracker.7. Изключване (fallback)
След рестарт на торента трябва да има само
role=primaryредове, т.е. старото поведение и на multi-homed машината. Ако имаш private tracker, провери там, че не отчита проблем с множество IP-та. Ако отчита, това е аргумент за изключване по подразбиране за private torrents (виж решение 6).Generated by Claude Code