Merge pull request #6693 from urbit/philip/ping

ping: Rewrite to support non-NAT mode
This commit is contained in:
Pyry Kovanen 2023-06-28 15:42:03 +03:00 committed by GitHub
commit a1bfce1d01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 323 additions and 156 deletions

View File

@ -13,129 +13,46 @@
=* point point:kale
::
|%
:: How often to ping our sponsor when we might be behind a NAT.
::
:: NAT timeouts are often pretty short for UDP entries. 5 minutes is
:: a common value. We use 25 seconds, same as Wireguard.
::
++ nat-timeout ~s25
::
:: How often to check our IP when we know we're not behind a NAT.
::
++ ip-timeout ~m5
::
:: Chosen because it's run by Cloudflare, and others I tried were
:: inconsistently slow.
::
++ ip-reflector 'https://icanhazip.com'
::
+$ card card:agent:gall
+$ ship-state
$% [%idle ~]
[%poking ~]
[%http until=@da]
[%waiting until=@da]
==
+$ state-1
$: %1
ships=(set ship)
nonce=@ud
$= plan
$~ [%nat ~]
$% [%nat ~]
[%pub ip=(unit @t)]
==
==
--
::
%- agent:dbug
::
=| state=[%0 ships=(map ship [=rift =ship-state])]
=| state=state-1
=> |%
:: +print-error: maybe +slog
::
++ print-error
|= [=tape error=(unit tang)]
^+ same
?~ error same
%- (slog leaf+tape u.error) same
:: +set-timer: send a card to behn to set a timer
::
++ set-timer
|= [now=@da =ship]
^- (quip card _state)
=/ s (~(get by ships.state) ship)
?~ s
`state
?. ?=(%poking -.ship-state.u.s)
%- (slog leaf+"ping: strange state {<ship s>}" ~)
`state
:: NAT timeouts are often pretty short for UDP entries. 5
:: minutes is a common value. We use 25 seconds, same as Wireguard.
::
=/ until (add ~s25 now)
=. ships.state
(~(put by ships.state) ship u.s(ship-state [%waiting until]))
:_ state
=/ =wire /ping-wait/(scot %p ship)/(scot %da until)
[%pass wire %arvo %b %wait `@da`until]~
:: +send-ping: poke their %ping app
::
++ send-ping
|= [our=@p now=@da =ship]
^- (quip card _state)
::
?: =(our ship)
`state
=/ s (~(get by ships.state) ship)
?~ s
`state
?. ?=(%idle -.ship-state.u.s)
`state
:_ state(ships (~(put by ships.state) ship u.s(ship-state [%poking ~])))
[%pass /ping-send/(scot %p ship) %agent [ship %ping] %poke %noun !>(~)]~
:: +stop-ping-ship: stop listening to jael if not sponsor or old rift
::
++ stop-ping-ship
|= [our=@p now=@da =ship =old=rift =ship-state]
^- (quip card _state)
=+ .^(=new=rift %j /(scot %p our)/rift/(scot %da now)/(scot %p ship))
:: if nothing's changed about us, don't cancel
::
?: ?& =(old-rift new-rift)
(~(has in (silt (saxo:title our now our))) ship)
==
`state
:: otherwise, kill jael subscription and timer
::
:_ state(ships (~(del by ships.state) ship))
[%pass /jael/(scot %p ship) %arvo %j %nuke (silt ship ~)]~
:: +start-ping-ship: start listening to jael updates if not already
::
:: While %public-keys is idempotent in most senses, it does
:: trigger a response, and this function is called on that
:: response, so we need a guard to avoid an infinite loop.
::
++ start-ping-ship
|= [our=@p now=@da =ship]
^- (quip card _state)
::
?: (~(has by ships.state) ship)
(send-ping our now ship)
::
;< new-state=_state (rind card state)
=+ .^(=rift %j /(scot %p our)/rift/(scot %da now)/(scot %p ship))
:_ state(ships (~(put by ships.state) ship rift %idle ~))
[%pass /jael/(scot %p ship) %arvo %j %public-keys (silt ship ~)]~
=. state new-state
::
(send-ping our now ship)
:: +kick: idempotent operation to make clean start for all pings
::
++ kick
|= [our=@p now=@da]
^- (quip card _state)
?: =(%czar (clan:title our))
`state
::
=/ old-ships=(list [=ship =rift =ship-state]) ~(tap by ships.state)
|- ^- (quip card _state)
=* loop $
?^ old-ships
;< new-state=_state (rind card state)
(stop-ping-ship our now i.old-ships)
=. state new-state
loop(old-ships t.old-ships)
::
:: NB: !! This includes our own ship, and for moons, this is what
:: has caused Jael to fetch our own rift from our parent. This
:: role may be taken by Ames's subscription to %public-keys, but
:: this must be tested before changing the behavior here.
::
=/ new-ships (saxo:title our now our)
|- ^- (quip card _state)
=* loop $
?^ new-ships
;< new-state=_state (rind card state)
(start-ping-ship our now i.new-ships)
=. state new-state
loop(new-ships t.new-ships)
::
`state
:: +rind: bind for the the writer monad on (quip effect state)
:: Bind for the the writer monad on (quip effect state)
::
++ rind
|* [effect=mold state=*]
@ -148,6 +65,211 @@
=^ effects-2=(list effect) state (fun state)
[(weld effects-1 effects-2) state]
::
++ once
|= =cord
=(cord (scot %uw nonce.state))
::
:: Subsystem to keep track of which ships to ping across breaches
:: and sponsorship changes
::
++ ships
|%
++ rind (^rind card state)
++ kick
|= [our=@p now=@da]
^- (quip card _state)
:: ?: =(%czar (clan:title our))
:: `state
::
:: NB: !! This includes our own ship, and for moons, this is
:: what has caused Jael to fetch our own rift from our parent.
:: This role may be taken by Ames's subscription to
:: %public-keys, but this must be tested before changing the
:: behavior here.
::
=/ new-ships (~(gas in *(set ship)) (saxo:title our now our))
=/ removed (~(dif in ships.state) new-ships)
=/ added (~(dif in new-ships) ships.state)
;< new-state=_state rind
?~ removed `state
[[%pass /jael %arvo %j %nuke removed]~ state]
=. state new-state
::
;< new-state=_state rind
?~ added `state
[[%pass /jael %arvo %j %public-keys added]~ state]
=. state new-state
::
:: Kick even if ships weren't added or removed
::
(kick-pings our now new-ships)
::
:: Kick whenever we get a response. We really care about
:: breaches and sponsorship changes.
::
:: Delay until next event in case of breach, so that ames can
:: clear its state.
::
++ take-jael
|= now=@da
^- (quip card _state)
[[%pass /jael/delay %arvo %b %wait now]~ state]
::
++ take-delay kick
--
::
:: Starts pinging a new set of `ships`.
::
++ kick-pings
|= [our=@p now=@da ships=(set ship)]
^- (quip card _state)
=: nonce.state +(nonce.state)
ships.state ships
==
::
?: ?=(%nat -.plan.state)
(kick:nat our)
(kick:pub our now)
::
:: Subsystem for pinging our sponsors when we might be behind a NAT
::
:: Ping each ship every 25 seconds to keep the pinhole open.
:: This is expensive, but if you don't do it and you are behind a
:: NAT, you will stop receiving packets from other ships except
:: during the 30 seconds following each packet you send.
::
++ nat
?> ?=(%nat -.plan.state)
|%
++ rind (^rind card state)
++ kick
|= our=@p
^- (quip card _state)
=/ ships ~(tap in ships.state)
|- ^- (quip card _state)
?~ ships `state
?: =(our i.ships) `state
;< new-state=_state rind (send-ping i.ships)
=. state new-state
$(ships t.ships)
::
++ send-ping
|= =ship
^- (quip card _state)
:_ state
=/ wire /nat/(scot %uw nonce.state)/ping/(scot %p ship)
[%pass wire %agent [ship %ping] %poke %noun !>(~)]~
::
++ take-ping
|= [now=@da =wire error=(unit tang)]
^- (quip card _state)
?. ?=([%nat @ %ping @ ~] wire) `state
?. (once i.t.wire) `state
=/ ship (slav %p i.t.t.t.wire)
%- (slog ?~(error ~ ['ping: got nack' >ship< u.error]))
:_ state
=/ wire /nat/(scot %uw nonce.state)/wait/(scot %p ship)
[%pass wire %arvo %b %wait (add nat-timeout now)]~
::
++ take-wait
|= =wire
^- (quip card _state)
?. ?=([%nat @ %wait @ ~] wire) `state
?. (once i.t.wire) `state
=/ ship (slav %p i.t.t.t.wire)
(send-ping ship)
--
::
:: Subsystem for pinging our sponsors when we know we're not behind a NAT
::
:: Check our IP address every minute, and only if it changes,
:: ping all our sponsors.
::
++ pub
?> ?=(%pub -.plan.state)
|%
++ rind (^rind card state)
++ kick
|= [our=@p now=@da]
^- (quip card _state)
;< new-state=_state rind (send-pings our)
=. state new-state
::
;< new-state=_state rind check-ip
=. state new-state
::
(set-timer now)
::
++ send-pings
|= our=@p
^- (quip card _state)
:_ state
%+ murn ~(tap in ships.state)
|= =ship
?: =(our ship)
~
=/ wire /pub/(scot %uw nonce.state)/ping/(scot %p ship)
`u=[%pass wire %agent [ship %ping] %poke %noun !>(~)]
::
++ take-pings
|= [=wire error=(unit tang)]
^- (quip card _state)
?. ?=([%pub @ %ping @ ~] wire) `state
?. (once i.t.wire) `state
=/ ship (slav %p i.t.t.t.wire)
%- (slog ?~(error ~ ['ping: got nack' >ship< u.error]))
`state
::
++ check-ip
^- (quip card _state)
:_ state
=/ wire /pub/(scot %uw nonce.state)/ip
=/ =request:http [%'GET' ip-reflector ~ ~]
[%pass wire %arvo %i %request request *outbound-config:iris]~
::
++ take-ip
|= [our=@p =wire resp=client-response:iris]
^- (quip card _state)
?. ?=([%pub @ %ip ~] wire) `state
?. (once i.t.wire) `state
::
?. ?=(%finished -.resp) `state :: will retry in a minute
?. ?=(%200 status-code.response-header.resp)
=* s status-code.response-header.resp
%- (slog leaf+"ping: ip check failed: {<s>}" ~)
`state
::
?~ full-file.resp
%- (slog 'ping: ip check body empty' ~)
`state
::
=* body q.data.u.full-file.resp
?~ body
%- (slog 'ping: ip check body empty' ~)
`state
::
=/ ip (end [3 (dec (met 3 body))] body)
?: =(ip.plan.state `ip) `state
::
=. ip.plan.state `ip
(send-pings our)
::
++ set-timer
|= now=@da
^- (quip card _state)
=/ =wire /pub/(scot %uw nonce.state)/wait
[[%pass wire %arvo %b %wait (add ip-timeout now)]~ state]
::
++ take-wait
|= [our=@p now=@da =wire]
^- (quip card _state)
?. ?=([%pub @ %wait ~] wire) `state
?. (once i.t.wire) `state
;< new-state=_state rind check-ip
=. state new-state
::
(set-timer now)
--
--
%+ verb |
^- agent:gall
@ -159,25 +281,47 @@
::
++ on-init
^- [(list card) _this]
=^ cards state (kick our.bowl now.bowl)
=. plan.state [%nat ~]
=^ cards state (kick:ships our.bowl now.bowl)
[cards this]
::
++ on-save !>(state)
++ on-load
|= old=vase
=. state !<(_state old)
(on-poke %noun !>(%kick))
|= old-vase=vase
|^
=/ old !<(state-any old-vase)
=? old ?=(%0 -.old) (state-0-to-1 old)
?> ?=(%1 -.old)
=. state old
=^ cards state (kick:ships our.bowl now.bowl)
[cards this]
::
+$ state-any $%(state-0 state-1)
+$ state-0 [%0 ships=(map ship [=rift =ship-state])]
::
++ state-0-to-1
|= old=state-0
^- state-1
[%1 ~ 0 %nat ~]
--
:: +on-poke: positively acknowledge pokes
::
++ on-poke
|= [=mark =vase]
?: =(q.vase %kick)
=. ships.state
%- ~(run by ships.state)
|= [=rift =ship-state]
[999.999 ship-state]
on-init
`this
?. =(our src):bowl :: don't crash, this is where pings are handled
`this
::
=^ cards state
?: =(q.vase %kick) :: NB: ames calls this on %born
(kick:ships our.bowl now.bowl)
?: =(q.vase %nat)
=. plan.state [%nat ~]
(kick:ships our.bowl now.bowl)
?: =(q.vase %no-nat)
=. plan.state [%pub ~]
(kick:ships our.bowl now.bowl)
`state
[cards this]
::
++ on-watch on-watch:def
++ on-leave on-leave:def
@ -190,46 +334,56 @@
++ on-agent
|= [=wire =sign:agent:gall]
^- [(list card) _this]
?> ?=([%ping-send @ ~] wire)
?> ?=(%poke-ack -.sign)
::
%- (print-error "ping: ack" p.sign)
=^ cards state
(set-timer now.bowl (slav %p i.t.wire))
?+ wire `state
[%nat *]
?. ?=(%nat -.plan.state) `state
?. ?=(%poke-ack -.sign) `state
(take-ping:nat now.bowl wire p.sign)
::
[%pub *]
?. ?=(%pub -.plan.state) `state
?. ?=(%poke-ack -.sign) `state
(take-pings:pub wire p.sign)
==
[cards this]
:: +on-arvo: handle timer firing
::
++ on-arvo
|= [=wire =sign-arvo]
^- [(list card) _this]
?+ wire !!
[%ping-wait @ @ ~]
?> ?=(%wake +<.sign-arvo)
=/ =ship (slav %p i.t.wire)
=/ until=@da (slav %da i.t.t.wire)
=/ s (~(get by ships.state) ship)
?~ s
`this
?. =([%waiting until] ship-state.u.s)
`this
=. ships.state (~(put by ships.state) ship u.s(ship-state [%idle ~]))
%- (print-error "ping: wake" error.sign-arvo)
=^ cards state
(send-ping our.bowl now.bowl ship)
[cards this]
::
[%jael @ ~]
:: whenever we get an update from Jael, kick
=^ cards state
?+ wire `state
[%jael %delay ~]
?> ?=(%wake +<.sign-arvo)
?^ error.sign-arvo
%- (slog 'ping: strange jael wake fail!' u.error.sign-arvo)
`state
(take-delay:ships our.bowl now.bowl)
::
?> ?=(%public-keys +<.sign-arvo)
:_ this
[%pass /delay %arvo %b %wait now.bowl]~
:: Delayed until next event so that ames can clear its state
::
[%delay ~]
?> ?=(%wake +<.sign-arvo)
on-init
==
[%jael ~]
?> ?=(%public-keys +<.sign-arvo)
(take-jael:ships now.bowl)
::
[%nat *]
?. ?=(%nat -.plan.state) `state
?> ?=(%wake +<.sign-arvo)
?^ error.sign-arvo
%- (slog 'ping: strange nat wake fail!' u.error.sign-arvo)
`state
(take-wait:nat wire)
::
[%pub @ %ip *]
?. ?=(%pub -.plan.state) `state
?> ?=(%http-response +<.sign-arvo)
(take-ip:pub our.bowl wire client-response.sign-arvo)
::
[%pub @ %wait *]
?. ?=(%pub -.plan.state) `state
?> ?=(%wake +<.sign-arvo)
(take-wait:pub our.bowl now.bowl wire)
==
[cards this]
::
++ on-fail on-fail:def
--

View File

@ -1127,6 +1127,9 @@
$: %d
$>(%flog task:dill)
==
$: %g
$>(%deal task:gall)
==
$: %j
$> $? %private-keys
%public-keys
@ -1145,6 +1148,9 @@
$% $: %behn
$>(%wake gift:behn)
==
$: %gall
$>(%unto gift:gall)
==
$: %jael
$> $? %private-keys
%public-keys
@ -2607,7 +2613,11 @@
=< q.q %- need %- need
(rof ~ %j `beam`[[our %turf %da now] /])
::
(emit unix-duct.ames-state %give %turf turfs)
=* duct unix-duct.ames-state
%- emil
:~ [duct %give %turf turfs]
[duct %pass /ping %g %deal [our our] %ping %poke %noun !>(%kick)]
==
:: +on-vega: handle kernel reload
::
++ on-vega event-core
@ -4680,6 +4690,9 @@
=/ event-core (ev [now eny rof] duct ames-state)
::
=^ moves ames-state
?: ?=([%gall %unto *] sign)
`ames-state
::
=< abet
?- sign
[@ %done *] (on-take-done:event-core wire error.sign)