From 6e6645b06b9aa91ddd5176e14581a76d7e72b90a Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Mon, 23 Feb 2026 12:10:09 -0700 Subject: [PATCH 1/8] Add unbounded broflake widget proxy lifecycle manager Adds vpn/unbounded.go which manages the broflake widget proxy lifecycle: start/stop based on local setting and config, emit connection events via the radiance event bus, and wire into radiance startup/shutdown. Includes the UnboundedKey setting and broflake dependency. Co-Authored-By: Claude Opus 4.6 --- common/settings/settings.go | 1 + go.mod | 39 ++++++-- go.sum | 68 +++++++++++--- radiance.go | 2 + vpn/unbounded.go | 176 ++++++++++++++++++++++++++++++++++++ 5 files changed, 264 insertions(+), 22 deletions(-) create mode 100644 vpn/unbounded.go diff --git a/common/settings/settings.go b/common/settings/settings.go index c76e23f6..e5caa052 100644 --- a/common/settings/settings.go +++ b/common/settings/settings.go @@ -37,6 +37,7 @@ const ( LoginResponseKey = "login_response" SmartRoutingKey = "smart_routing" AdBlockKey = "ad_block" + UnboundedKey = "unbounded" filePathKey = "file_path" settingsFileName = "local.json" diff --git a/go.mod b/go.mod index 59b61ab7..5e18d9bc 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,13 @@ replace github.com/sagernet/wireguard-go => github.com/getlantern/wireguard-go v replace github.com/tetratelabs/wazero => github.com/refraction-networking/wazero v1.7.1-w -// replace github.com/getlantern/common => ../common +replace github.com/enobufs/go-nats => github.com/noahlevenson/go-nats v0.0.0-20230720174341-49df1f749775 + +replace github.com/quic-go/quic-go => github.com/getlantern/quic-go-unbounded-fork v0.51.3-unbounded + +replace github.com/getlantern/common => ../common + +replace github.com/getlantern/broflake => ../unbounded // replace github.com/sagernet/sing => ../sing @@ -20,6 +26,8 @@ replace github.com/tetratelabs/wazero => github.com/refraction-networking/wazero // replace github.com/getlantern/lantern-box => ../lantern-box +require github.com/getlantern/broflake v0.0.0-20260221195823-510790b48bbe + require ( github.com/1Password/srp v0.2.0 github.com/Microsoft/go-winio v0.6.2 @@ -100,6 +108,7 @@ require ( github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect + github.com/enobufs/go-nats v0.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/flynn/noise v1.0.1-0.20220214164934-d803f5c4b0f4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -114,12 +123,14 @@ require ( github.com/go-json-experiment/json v0.0.0-20250103232110-6a9a0fde9288 // indirect github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 // indirect github.com/go-llsqlite/crawshaw v0.4.0 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gobwas/ws v1.4.0 // indirect github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect + github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/websocket v1.5.3 // indirect @@ -144,25 +155,36 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/nwaples/rardecode v1.1.2 // indirect + github.com/onsi/ginkgo/v2 v2.12.0 // indirect github.com/pion/datachannel v1.5.10 // indirect - github.com/pion/dtls/v2 v2.2.7 // indirect + github.com/pion/dtls/v2 v2.2.12 // indirect + github.com/pion/dtls/v3 v3.0.6 // indirect github.com/pion/ice/v2 v2.3.24 // indirect - github.com/pion/interceptor v0.1.37 // indirect - github.com/pion/logging v0.2.3 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/interceptor v0.1.40 // indirect + github.com/pion/logging v0.2.4 // indirect github.com/pion/mdns v0.0.12 // indirect + github.com/pion/mdns/v2 v2.0.7 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/rtcp v1.2.15 // indirect - github.com/pion/rtp v1.8.12 // indirect - github.com/pion/sctp v1.8.37 // indirect - github.com/pion/sdp/v3 v3.0.11 // indirect + github.com/pion/rtp v1.8.19 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/sdp/v3 v3.0.14 // indirect github.com/pion/srtp/v2 v2.0.18 // indirect + github.com/pion/srtp/v3 v3.0.6 // indirect github.com/pion/stun v0.6.1 // indirect - github.com/pion/transport/v2 v2.2.4 // indirect + github.com/pion/stun/v3 v3.0.0 // indirect + github.com/pion/transport v0.14.1 // indirect + github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/turn v1.3.7 // indirect github.com/pion/turn/v2 v2.1.3 // indirect + github.com/pion/turn/v4 v4.0.2 // indirect github.com/pion/webrtc/v3 v3.2.40 // indirect + github.com/pion/webrtc/v4 v4.1.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus-community/pro-bing v0.4.0 // indirect + github.com/quic-go/quic-go v0.51.0 // indirect github.com/refraction-networking/utls v1.8.2 // indirect github.com/refraction-networking/water v0.7.1-alpha // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect @@ -185,6 +207,7 @@ require ( github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tkuchiki/go-timezone v0.2.0 // indirect github.com/ulikunitz/xz v0.5.10 // indirect + github.com/wlynxg/anet v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xtaci/kcp-go/v5 v5.6.20 // indirect diff --git a/go.sum b/go.sum index 84fed3f1..69284d38 100644 --- a/go.sum +++ b/go.sum @@ -192,8 +192,6 @@ github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f h1:NLGftemDrbGf7Wce github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f/go.mod h1:qnMv9szb8JK3kA9W4N2FlYUMj1GkA0x7QEUEPD7tk4o= github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01 h1:Mmeh4/DA1OKN9tVWRAvTL5efFx4c7v9/55hoK17NclA= github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01/go.mod h1:3vR6+jQdWfWojZ77w+htCqEF5MO/Y2twJOpAvFuM9po= -github.com/getlantern/common v1.2.1-0.20260121160752-d8ee5791108f h1:EqRKCaOBuvVkFsIjeWUYluE4s4TZtVQSClfIWFqcSks= -github.com/getlantern/common v1.2.1-0.20260121160752-d8ee5791108f/go.mod h1:eSSuV4bMPgQJnczBw+KWWqWNo1itzmVxC++qUBPRTt0= github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA= github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo= @@ -239,6 +237,8 @@ github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175 h1:JWH5BB2o0e github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175/go.mod h1:h3S9LBmmzN/xM+lwYZHE4abzTtCTtidKtG+nxZcCZX0= github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 h1:rtDmW8YLAuT8r51ApR5z0d8/qjhHu3TW+divQ2C98Ac= github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535/go.mod h1:WKJEdjMOD4IuTRYwjQHjT4bmqDl5J82RShMLxPAvi0Q= +github.com/getlantern/quic-go-unbounded-fork v0.51.3-unbounded h1:qA1oi5so1/C6psHLPlyPGyq6JhZsPvA4EutsNhjzodc= +github.com/getlantern/quic-go-unbounded-fork v0.51.3-unbounded/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= github.com/getlantern/samizdat v0.0.2 h1:PkMu6jsfUz7DLZUH2xh548XfzgPASmq5CajZyUKj/9Y= github.com/getlantern/samizdat v0.0.2/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0= github.com/getlantern/sing v0.7.18-lantern h1:QKGgIUA3LwmKYP/7JlQTRkxj9jnP4cX2Q/B+nd8XEjo= @@ -292,6 +292,8 @@ github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -352,6 +354,8 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= +github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ= +github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -482,12 +486,18 @@ github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOl github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/noahlevenson/go-nats v0.0.0-20230720174341-49df1f749775 h1:CVBqDCqhtrS2etCKGuwruUkwg3f/axVpa2Il5IQQtEs= +github.com/noahlevenson/go-nats v0.0.0-20230720174341-49df1f749775/go.mod h1:dXVvPZcJIwdWDH5ZXQ8oVA7dz+Ecu9TPF+6biaMl1dY= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.2 h1:Cj0yZY6T1Zx1R7AhTbyGSALm44/Mmq+BAPc4B/p/d3M= github.com/nwaples/rardecode v1.1.2/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI= +github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= @@ -500,45 +510,68 @@ github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= -github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= +github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= github.com/pion/ice/v2 v2.3.24 h1:RYgzhH/u5lH0XO+ABatVKCtRd+4U1GEaCXSMjNr13tI= github.com/pion/ice/v2 v2.3.24/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw= -github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= -github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= -github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/rtp v1.8.12 h1:nsKs8Wi0jQyBFHU3qmn/OvtZrhktVfJY0vRxwACsL5U= -github.com/pion/rtp v1.8.12/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= -github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs= -github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= -github.com/pion/sdp/v3 v3.0.11 h1:VhgVSopdsBKwhCFoyyPmT1fKMeV9nLMrEKxNOdy3IVI= -github.com/pion/sdp/v3 v3.0.11/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= +github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.14 h1:1h7gBr9FhOWH5GjWWY5lcw/U85MtdcibTyt/o6RxRUI= +github.com/pion/sdp/v3 v3.0.14/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo= github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= +github.com/pion/stun v0.3.2/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M= +github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M= github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= +github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= +github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/transport v0.8.8/go.mod h1:lpeSM6KJFejVtZf8k0fgeN7zE73APQpTF83WvA1FVP8= +github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= +github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc= github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= -github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo= github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/turn v1.3.7 h1:/nyM2XrlZILD7KKfnh0oYEBTRG5JlbH21ibjluRoCeo= +github.com/pion/turn v1.3.7/go.mod h1:js0LBFqMcKAlaWAXoYqNjefGI7kfJCrkCBfHGuTToXE= github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA= github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= +github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= +github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= github.com/pion/webrtc/v3 v3.2.40 h1:Wtfi6AZMQg+624cvCXUuSmrKWepSB7zfgYDOYqsSOVU= github.com/pion/webrtc/v3 v3.2.40/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY= +github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= +github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -699,6 +732,9 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= @@ -820,6 +856,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= @@ -868,6 +905,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -882,6 +920,7 @@ golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= @@ -897,6 +936,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= diff --git a/radiance.go b/radiance.go index 521068e2..811524ae 100644 --- a/radiance.go +++ b/radiance.go @@ -146,7 +146,9 @@ func NewRadiance(opts Options) (*Radiance, error) { } }) registerPreStartTest(dataDir) + vpn.InitUnboundedSubscription() r.confHandler = config.NewConfigHandler(cOpts) + r.addShutdownFunc(vpn.StopUnbounded) r.addShutdownFunc(telemetry.Close, kindling.Close) return r, nil } diff --git a/vpn/unbounded.go b/vpn/unbounded.go new file mode 100644 index 00000000..93008023 --- /dev/null +++ b/vpn/unbounded.go @@ -0,0 +1,176 @@ +package vpn + +import ( + "context" + "log/slog" + "net" + "sync" + + C "github.com/getlantern/common" + + "github.com/getlantern/broflake/clientcore" + + "github.com/getlantern/radiance/common/settings" + "github.com/getlantern/radiance/config" + "github.com/getlantern/radiance/events" +) + +// UnboundedConnectionEvent is emitted when a consumer connection changes state +// in the broflake widget proxy. State: 1 = connected, -1 = disconnected. +type UnboundedConnectionEvent struct { + events.Event + State int `json:"state"` + WorkerIdx int `json:"workerIdx"` + Addr string `json:"addr"` +} + +var unbounded = &unboundedManager{} + +type unboundedManager struct { + mu sync.Mutex + cancel context.CancelFunc +} + +func UnboundedEnabled() bool { + return settings.GetBool(settings.UnboundedKey) +} + +func SetUnbounded(enable bool) error { + if UnboundedEnabled() == enable { + return nil + } + if err := settings.Set(settings.UnboundedKey, enable); err != nil { + return err + } + slog.Info("Updated Unbounded widget proxy", "enabled", enable) + if enable { + unbounded.start(nil) + } else { + unbounded.stop() + } + return nil +} + +// InitUnboundedSubscription subscribes to config changes and starts/stops the +// broflake widget proxy based on three conditions: +// 1. settings.UnboundedKey is true (local opt-in) +// 2. cfg.Features["unbounded"] is true (server says run it) +// 3. cfg.Unbounded != nil (server provided discovery/egress URLs) +func InitUnboundedSubscription() { + events.Subscribe(func(evt config.NewConfigEvent) { + if evt.New == nil { + return + } + cfg := evt.New.ConfigResponse + shouldRun := shouldRunUnbounded(cfg) + unbounded.mu.Lock() + running := unbounded.cancel != nil + unbounded.mu.Unlock() + + if shouldRun && !running { + unbounded.start(cfg.Unbounded) + } else if !shouldRun && running { + unbounded.stop() + } + }) +} + +func shouldRunUnbounded(cfg C.ConfigResponse) bool { + if !settings.GetBool(settings.UnboundedKey) { + return false + } + // When server-side config is available, also check: + // cfg.Features[C.UNBOUNDED] && cfg.Unbounded != nil + // For now, only require the local setting so we can test without + // lantern-cloud sending the config. + return true +} + +func (m *unboundedManager) start(ucfg *C.UnboundedConfig) { + m.mu.Lock() + defer m.mu.Unlock() + if m.cancel != nil { + return // already running + } + + ctx, cancel := context.WithCancel(context.Background()) + m.cancel = cancel + + go func() { + slog.Info("Unbounded: starting broflake widget proxy") + + bfOpt := clientcore.NewDefaultBroflakeOptions() + bfOpt.ClientType = "widget" + if ucfg != nil { + if ucfg.CTableSize > 0 { + bfOpt.CTableSize = ucfg.CTableSize + } + if ucfg.PTableSize > 0 { + bfOpt.PTableSize = ucfg.PTableSize + } + } + + // Wire up connection change callback to emit radiance events + bfOpt.OnConnectionChangeFunc = func(state int, workerIdx int, addr net.IP) { + addrStr := "" + if addr != nil { + addrStr = addr.String() + } + slog.Debug("Unbounded: consumer connection change", "state", state, "workerIdx", workerIdx, "addr", addrStr) + events.Emit(UnboundedConnectionEvent{ + State: state, + WorkerIdx: workerIdx, + Addr: addrStr, + }) + } + + rtcOpt := clientcore.NewDefaultWebRTCOptions() + if ucfg != nil { + if ucfg.DiscoverySrv != "" { + rtcOpt.DiscoverySrv = ucfg.DiscoverySrv + } + if ucfg.DiscoveryEndpoint != "" { + rtcOpt.Endpoint = ucfg.DiscoveryEndpoint + } + } + + egOpt := clientcore.NewDefaultEgressOptions() + if ucfg != nil { + if ucfg.EgressAddr != "" { + egOpt.Addr = ucfg.EgressAddr + } + if ucfg.EgressEndpoint != "" { + egOpt.Endpoint = ucfg.EgressEndpoint + } + } + + _, ui, err := clientcore.NewBroflake(bfOpt, rtcOpt, egOpt) + if err != nil { + slog.Error("Unbounded: failed to create broflake widget", "error", err) + m.mu.Lock() + m.cancel = nil + m.mu.Unlock() + return + } + + slog.Info("Unbounded: broflake widget proxy started") + <-ctx.Done() + slog.Info("Unbounded: stopping broflake widget proxy") + ui.Stop() + slog.Info("Unbounded: broflake widget proxy stopped") + }() +} + +func (m *unboundedManager) stop() { + m.mu.Lock() + defer m.mu.Unlock() + if m.cancel != nil { + m.cancel() + m.cancel = nil + } +} + +func StopUnbounded(_ context.Context) error { + unbounded.stop() + return nil +} From af7a0f05ebfa092fa6b0108b049651dd6297033a Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Mon, 23 Feb 2026 12:25:42 -0700 Subject: [PATCH 2/8] Point to merged common on main, drop local replace Co-Authored-By: Claude Opus 4.6 --- go.mod | 4 +--- go.sum | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5e18d9bc..1b3387cb 100644 --- a/go.mod +++ b/go.mod @@ -14,8 +14,6 @@ replace github.com/enobufs/go-nats => github.com/noahlevenson/go-nats v0.0.0-202 replace github.com/quic-go/quic-go => github.com/getlantern/quic-go-unbounded-fork v0.51.3-unbounded -replace github.com/getlantern/common => ../common - replace github.com/getlantern/broflake => ../unbounded // replace github.com/sagernet/sing => ../sing @@ -34,7 +32,7 @@ require ( github.com/alitto/pond v1.9.2 github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01 - github.com/getlantern/common v1.2.1-0.20260121160752-d8ee5791108f + github.com/getlantern/common v1.2.1-0.20260223192400-cc00002ef6c7 github.com/getlantern/dnstt v0.0.0-20260112160750-05100563bd0d github.com/getlantern/fronted v0.0.0-20260219001615-7eabaa834efe github.com/getlantern/keepcurrent v0.0.0-20240126172110-2e0264ca385d diff --git a/go.sum b/go.sum index 69284d38..3cca3552 100644 --- a/go.sum +++ b/go.sum @@ -192,6 +192,8 @@ github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f h1:NLGftemDrbGf7Wce github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f/go.mod h1:qnMv9szb8JK3kA9W4N2FlYUMj1GkA0x7QEUEPD7tk4o= github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01 h1:Mmeh4/DA1OKN9tVWRAvTL5efFx4c7v9/55hoK17NclA= github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01/go.mod h1:3vR6+jQdWfWojZ77w+htCqEF5MO/Y2twJOpAvFuM9po= +github.com/getlantern/common v1.2.1-0.20260223192400-cc00002ef6c7 h1:WhfyxuQ6cf7Ia9NF5PNkhCyfY2nhogXx/CCRRqy7DhI= +github.com/getlantern/common v1.2.1-0.20260223192400-cc00002ef6c7/go.mod h1:eSSuV4bMPgQJnczBw+KWWqWNo1itzmVxC++qUBPRTt0= github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA= github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo= From cee8e174ece2eb980ef0e3032418f923f04af388 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Mon, 23 Feb 2026 12:52:06 -0700 Subject: [PATCH 3/8] Point to merged broflake on main, drop local replace Co-Authored-By: Claude Opus 4.6 --- go.mod | 4 +--- go.sum | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1b3387cb..608a1830 100644 --- a/go.mod +++ b/go.mod @@ -14,8 +14,6 @@ replace github.com/enobufs/go-nats => github.com/noahlevenson/go-nats v0.0.0-202 replace github.com/quic-go/quic-go => github.com/getlantern/quic-go-unbounded-fork v0.51.3-unbounded -replace github.com/getlantern/broflake => ../unbounded - // replace github.com/sagernet/sing => ../sing // replace github.com/sagernet/sing-box => ../sing-box-minimal @@ -24,7 +22,7 @@ replace github.com/getlantern/broflake => ../unbounded // replace github.com/getlantern/lantern-box => ../lantern-box -require github.com/getlantern/broflake v0.0.0-20260221195823-510790b48bbe +require github.com/getlantern/broflake v0.0.0-20260223195036-4065257e0911 require ( github.com/1Password/srp v0.2.0 diff --git a/go.sum b/go.sum index 3cca3552..2f829205 100644 --- a/go.sum +++ b/go.sum @@ -192,6 +192,8 @@ github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f h1:NLGftemDrbGf7Wce github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f/go.mod h1:qnMv9szb8JK3kA9W4N2FlYUMj1GkA0x7QEUEPD7tk4o= github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01 h1:Mmeh4/DA1OKN9tVWRAvTL5efFx4c7v9/55hoK17NclA= github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01/go.mod h1:3vR6+jQdWfWojZ77w+htCqEF5MO/Y2twJOpAvFuM9po= +github.com/getlantern/broflake v0.0.0-20260223195036-4065257e0911 h1:HmTK1otLrb3zOi3b+7VEODbKdMKKxs0Ka6Ux17PjgLc= +github.com/getlantern/broflake v0.0.0-20260223195036-4065257e0911/go.mod h1:b40tuN7lS/j6OXOlUyo4m/CY+JbZ/dj1SBOtGfqPUKI= github.com/getlantern/common v1.2.1-0.20260223192400-cc00002ef6c7 h1:WhfyxuQ6cf7Ia9NF5PNkhCyfY2nhogXx/CCRRqy7DhI= github.com/getlantern/common v1.2.1-0.20260223192400-cc00002ef6c7/go.mod h1:eSSuV4bMPgQJnczBw+KWWqWNo1itzmVxC++qUBPRTt0= github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= From 8232d7859b4635be29fc93acacac2ca7fdca600d Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Mon, 23 Feb 2026 12:58:33 -0700 Subject: [PATCH 4/8] Restore server-side checks in shouldRunUnbounded Require all three conditions: local opt-in, server feature flag, and server-provided unbounded config. Cache lastCfg from config events so SetUnbounded can start immediately when the user toggles on and config is already available. Co-Authored-By: Claude Opus 4.6 --- vpn/unbounded.go | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/vpn/unbounded.go b/vpn/unbounded.go index 93008023..a883cd0a 100644 --- a/vpn/unbounded.go +++ b/vpn/unbounded.go @@ -27,8 +27,9 @@ type UnboundedConnectionEvent struct { var unbounded = &unboundedManager{} type unboundedManager struct { - mu sync.Mutex - cancel context.CancelFunc + mu sync.Mutex + cancel context.CancelFunc + lastCfg *C.UnboundedConfig // most recent config from server } func UnboundedEnabled() bool { @@ -44,7 +45,14 @@ func SetUnbounded(enable bool) error { } slog.Info("Updated Unbounded widget proxy", "enabled", enable) if enable { - unbounded.start(nil) + unbounded.mu.Lock() + cfg := unbounded.lastCfg + unbounded.mu.Unlock() + if cfg != nil { + unbounded.start(cfg) + } else { + slog.Info("Unbounded: enabled locally, will start when server config arrives") + } } else { unbounded.stop() } @@ -62,11 +70,14 @@ func InitUnboundedSubscription() { return } cfg := evt.New.ConfigResponse - shouldRun := shouldRunUnbounded(cfg) + + // Always store the latest unbounded config for use by SetUnbounded unbounded.mu.Lock() + unbounded.lastCfg = cfg.Unbounded running := unbounded.cancel != nil unbounded.mu.Unlock() + shouldRun := shouldRunUnbounded(cfg) if shouldRun && !running { unbounded.start(cfg.Unbounded) } else if !shouldRun && running { @@ -79,10 +90,12 @@ func shouldRunUnbounded(cfg C.ConfigResponse) bool { if !settings.GetBool(settings.UnboundedKey) { return false } - // When server-side config is available, also check: - // cfg.Features[C.UNBOUNDED] && cfg.Unbounded != nil - // For now, only require the local setting so we can test without - // lantern-cloud sending the config. + if !cfg.Features[C.UNBOUNDED] { + return false + } + if cfg.Unbounded == nil { + return false + } return true } From 21ef833630bedbf10e868115152000a85311c957 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Mon, 23 Feb 2026 13:15:30 -0700 Subject: [PATCH 5/8] Address PR review: add doc comments, cancel cleanup, and tests - Add doc comments on UnboundedEnabled, SetUnbounded, StopUnbounded - Nil out cancel func after ui.Stop() in goroutine to prevent stale ref - Add idempotency comment on stop() call in config subscription - Add unbounded_test.go with tests for shouldRunUnbounded, toggle, stop-when-not-running, and start/stop lifecycle Co-Authored-By: Claude Opus 4.6 --- vpn/unbounded.go | 9 +++++ vpn/unbounded_test.go | 87 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 vpn/unbounded_test.go diff --git a/vpn/unbounded.go b/vpn/unbounded.go index a883cd0a..39440587 100644 --- a/vpn/unbounded.go +++ b/vpn/unbounded.go @@ -32,10 +32,14 @@ type unboundedManager struct { lastCfg *C.UnboundedConfig // most recent config from server } +// UnboundedEnabled reports whether the Unbounded widget proxy is enabled in local settings. func UnboundedEnabled() bool { return settings.GetBool(settings.UnboundedKey) } +// SetUnbounded enables or disables the Unbounded widget proxy. When enabling, +// the proxy starts immediately if server config is already available; otherwise +// it will start on the next config event. When disabling, the proxy stops. func SetUnbounded(enable bool) error { if UnboundedEnabled() == enable { return nil @@ -81,6 +85,7 @@ func InitUnboundedSubscription() { if shouldRun && !running { unbounded.start(cfg.Unbounded) } else if !shouldRun && running { + // stop() is internally guarded and idempotent unbounded.stop() } }) @@ -170,6 +175,9 @@ func (m *unboundedManager) start(ucfg *C.UnboundedConfig) { <-ctx.Done() slog.Info("Unbounded: stopping broflake widget proxy") ui.Stop() + m.mu.Lock() + m.cancel = nil + m.mu.Unlock() slog.Info("Unbounded: broflake widget proxy stopped") }() } @@ -183,6 +191,7 @@ func (m *unboundedManager) stop() { } } +// StopUnbounded stops the Unbounded widget proxy. Used as a shutdown hook. func StopUnbounded(_ context.Context) error { unbounded.stop() return nil diff --git a/vpn/unbounded_test.go b/vpn/unbounded_test.go new file mode 100644 index 00000000..1e492775 --- /dev/null +++ b/vpn/unbounded_test.go @@ -0,0 +1,87 @@ +package vpn + +import ( + "os" + "testing" + + C "github.com/getlantern/common" + "github.com/getlantern/radiance/common/settings" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var testTmpDir string + +func TestMain(m *testing.M) { + tmp, err := os.MkdirTemp("", "unbounded-test-*") + if err != nil { + panic(err) + } + testTmpDir = tmp + if err := settings.InitSettings(tmp); err != nil { + panic(err) + } + code := m.Run() + os.RemoveAll(tmp) + os.Exit(code) +} + +func TestShouldRunUnbounded(t *testing.T) { + settings.Set(settings.UnboundedKey, false) + + cfg := C.ConfigResponse{ + Features: map[string]bool{C.UNBOUNDED: true}, + Unbounded: &C.UnboundedConfig{}, + } + + assert.False(t, shouldRunUnbounded(cfg), "should be false when setting is off") + + settings.Set(settings.UnboundedKey, true) + assert.True(t, shouldRunUnbounded(cfg), "should be true when all conditions met") + + // Missing feature flag + cfg.Features[C.UNBOUNDED] = false + assert.False(t, shouldRunUnbounded(cfg), "should be false when feature flag is off") + cfg.Features[C.UNBOUNDED] = true + + // Missing config + cfg.Unbounded = nil + assert.False(t, shouldRunUnbounded(cfg), "should be false when config is nil") + + settings.Set(settings.UnboundedKey, false) +} + +func TestSetUnboundedToggle(t *testing.T) { + settings.Set(settings.UnboundedKey, false) + + require.NoError(t, SetUnbounded(true)) + assert.True(t, UnboundedEnabled()) + + require.NoError(t, SetUnbounded(false)) + assert.False(t, UnboundedEnabled()) + + // Idempotent + require.NoError(t, SetUnbounded(false)) + assert.False(t, UnboundedEnabled()) +} + +func TestStopWhenNotRunning(t *testing.T) { + unbounded.stop() + assert.Nil(t, unbounded.cancel) +} + +func TestStartStopLifecycle(t *testing.T) { + unbounded.mu.Lock() + unbounded.cancel = nil + unbounded.mu.Unlock() + + unbounded.mu.Lock() + assert.Nil(t, unbounded.cancel) + unbounded.mu.Unlock() + + // stop is safe when already stopped + unbounded.stop() + unbounded.mu.Lock() + assert.Nil(t, unbounded.cancel) + unbounded.mu.Unlock() +} From 098367b50f866d9fb89c94cc51388a1b62ac1d5a Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Mon, 23 Feb 2026 13:21:49 -0700 Subject: [PATCH 6/8] Address remaining PR comments: race condition docs, ignored return value - Document that start() and stop() are internally guarded and idempotent, making the TOCTOU race between reading running state and acting harmless - Add comment explaining why BroflakeConn return value is unused (widget proxies donate bandwidth, they don't route traffic through the conn) Co-Authored-By: Claude Opus 4.6 --- vpn/unbounded.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vpn/unbounded.go b/vpn/unbounded.go index 39440587..f465e27a 100644 --- a/vpn/unbounded.go +++ b/vpn/unbounded.go @@ -83,9 +83,11 @@ func InitUnboundedSubscription() { shouldRun := shouldRunUnbounded(cfg) if shouldRun && !running { + // start() is internally guarded against being called when already running. unbounded.start(cfg.Unbounded) } else if !shouldRun && running { - // stop() is internally guarded and idempotent + // stop() is internally guarded and idempotent; safe to call even + // if another goroutine changed the running state since we read it. unbounded.stop() } }) @@ -162,6 +164,8 @@ func (m *unboundedManager) start(ucfg *C.UnboundedConfig) { } } + // BroflakeConn is for clients routing traffic through the mesh; + // a widget proxy only donates bandwidth, so the conn is unused. _, ui, err := clientcore.NewBroflake(bfOpt, rtcOpt, egOpt) if err != nil { slog.Error("Unbounded: failed to create broflake widget", "error", err) From 48046679d031c75a680d97ba8f5cb290c58dd2e4 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Mon, 23 Feb 2026 13:26:18 -0700 Subject: [PATCH 7/8] Fix TestSetUnboundedToggle failing in CI Use per-test settings init with t.Cleanup(settings.Reset) instead of TestMain. The boxoptions tests call settings.Reset in their cleanup, which wipes the settings singleton (including file path) between tests. With TestMain the settings were initialized once but then reset by other tests running in between, causing "settings file path is not set". Co-Authored-By: Claude Opus 4.6 --- vpn/unbounded_test.go | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/vpn/unbounded_test.go b/vpn/unbounded_test.go index 1e492775..69a0622b 100644 --- a/vpn/unbounded_test.go +++ b/vpn/unbounded_test.go @@ -1,7 +1,6 @@ package vpn import ( - "os" "testing" C "github.com/getlantern/common" @@ -10,23 +9,15 @@ import ( "github.com/stretchr/testify/require" ) -var testTmpDir string - -func TestMain(m *testing.M) { - tmp, err := os.MkdirTemp("", "unbounded-test-*") - if err != nil { - panic(err) - } - testTmpDir = tmp - if err := settings.InitSettings(tmp); err != nil { - panic(err) - } - code := m.Run() - os.RemoveAll(tmp) - os.Exit(code) +func initTestSettings(t *testing.T) { + t.Helper() + tmp := t.TempDir() + require.NoError(t, settings.InitSettings(tmp)) + t.Cleanup(settings.Reset) } func TestShouldRunUnbounded(t *testing.T) { + initTestSettings(t) settings.Set(settings.UnboundedKey, false) cfg := C.ConfigResponse{ @@ -47,11 +38,10 @@ func TestShouldRunUnbounded(t *testing.T) { // Missing config cfg.Unbounded = nil assert.False(t, shouldRunUnbounded(cfg), "should be false when config is nil") - - settings.Set(settings.UnboundedKey, false) } func TestSetUnboundedToggle(t *testing.T) { + initTestSettings(t) settings.Set(settings.UnboundedKey, false) require.NoError(t, SetUnbounded(true)) From 7520e330886d96109008b7855e3c0fb49e4d3f41 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Tue, 24 Feb 2026 13:29:13 -0700 Subject: [PATCH 8/8] Address PR review comments: fix context leak and test races MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cancel context on NewBroflake error to prevent leak - Add mutex around unbounded.cancel reads in tests to prevent data races - Improve TestStartStopLifecycle to test simulated running→stop transition Co-Authored-By: Claude Opus 4.6 --- vpn/unbounded.go | 1 + vpn/unbounded_test.go | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/vpn/unbounded.go b/vpn/unbounded.go index f465e27a..0cbdd6e2 100644 --- a/vpn/unbounded.go +++ b/vpn/unbounded.go @@ -169,6 +169,7 @@ func (m *unboundedManager) start(ucfg *C.UnboundedConfig) { _, ui, err := clientcore.NewBroflake(bfOpt, rtcOpt, egOpt) if err != nil { slog.Error("Unbounded: failed to create broflake widget", "error", err) + cancel() // cancel the context to avoid a leak m.mu.Lock() m.cancel = nil m.mu.Unlock() diff --git a/vpn/unbounded_test.go b/vpn/unbounded_test.go index 69a0622b..73abb20d 100644 --- a/vpn/unbounded_test.go +++ b/vpn/unbounded_test.go @@ -57,21 +57,35 @@ func TestSetUnboundedToggle(t *testing.T) { func TestStopWhenNotRunning(t *testing.T) { unbounded.stop() + unbounded.mu.Lock() assert.Nil(t, unbounded.cancel) + unbounded.mu.Unlock() } func TestStartStopLifecycle(t *testing.T) { + // Ensure we start from a stopped state. unbounded.mu.Lock() unbounded.cancel = nil unbounded.mu.Unlock() + // stop is safe when already stopped + unbounded.stop() unbounded.mu.Lock() - assert.Nil(t, unbounded.cancel) + assert.Nil(t, unbounded.cancel, "cancel should remain nil when stopping an already stopped unbounded") unbounded.mu.Unlock() - // stop is safe when already stopped + // Simulate a running state by setting a non-nil cancel function. + unbounded.mu.Lock() + unbounded.cancel = func() {} + unbounded.mu.Unlock() + + unbounded.mu.Lock() + assert.NotNil(t, unbounded.cancel, "cancel should be non-nil in simulated running state") + unbounded.mu.Unlock() + + // Now stopping should clear the cancel function. unbounded.stop() unbounded.mu.Lock() - assert.Nil(t, unbounded.cancel) + assert.Nil(t, unbounded.cancel, "cancel should be nil after stopping from a running state") unbounded.mu.Unlock() }