From 1965b43fdc5612b604c98f451a82a9c4af2c4174 Mon Sep 17 00:00:00 2001 From: Henk Date: Fri, 14 Feb 2025 15:38:52 +0100 Subject: [PATCH 01/82] Add -r option to performance tests to allow them to be repeated, store each repetition's results in different subdirectory (currently breaks performance test graph generation --- test_suite/performance_test.sh | 87 ++++++++++++++++++++++------------ test_suite/system_test.sh | 17 +++++-- 2 files changed, 70 insertions(+), 34 deletions(-) diff --git a/test_suite/performance_test.sh b/test_suite/performance_test.sh index ffc2737..a5e0de1 100755 --- a/test_suite/performance_test.sh +++ b/test_suite/performance_test.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash usage_str=""" -Usage: ${0} [OPTIONAL ARGUMENTS] +Usage: ${0} [OPTIONAL ARGUMENTS] [OPTIONAL ARGUMENTS]: -b @@ -43,7 +43,7 @@ done shift $((OPTIND-1)) # Make sure all required arguments have been passed -if [[ $# -ne 7 ]]; then +if [[ $# -ne 8 ]]; then exit_with_error "expected 7 positional parameters, but received $#" fi @@ -53,7 +53,8 @@ peer1_ip=$3 performance_test_var=$4 performance_test_values=$5 performance_test_duration=$6 -log_dir=$7 +performance_test_reps=$7 +log_dir=$8 function clean_exit() { exit_code=$1 @@ -68,8 +69,8 @@ function clean_exit() { kill $keep_alive_pid fi - # Undo the restrictive permissions which iperf3 sets on test_dir - chmod --recursive 777 $test_dir + # Undo the restrictive permissions which iperf3 sets on subdirectories of log_dir + chmod --recursive 777 $log_dir exit $exit_code } @@ -175,6 +176,42 @@ function performance_test() { store_delay $delay $log_path } +# Function to do performance tests for all performance test values +function performance_tests() { + performance_test_value_array=$1 + performance_test_dir=$2 + performance_test_rep=$3 + + # String describing the current repetition, empty if only one repetition is performed + if [[ $performance_test_reps -gt 1 ]]; then + rep_description="Repetition $performance_test_rep/$performance_test_reps: " + fi + + # Variables to display a progress bar + n_values=${#performance_test_value_array[@]} + progress=0 + + # Iterate over performance test values + for performance_test_val in ${performance_test_value_array[@]}; do + bar=$(progress_bar $progress $n_values) + echo -ne "\033[2K\t$rep_description$bar Performance testing with $performance_test_var = $performance_test_val\r" # \033[2K = Ctrl+K, clears rest of line from cursor; \r returns to beginning of line + + # Run performance test for eduP2P + performance_test $performance_test_val $performance_test_dir "eduP2P" $peer1_ip + + # If -b is set, the performance test is repeated over a direct/WireGuard connection instead of over the eduP2P connection + if [[ $baseline == true ]]; then + performance_test $performance_test_val $performance_test_dir "Direct" $peer1_pub_ip + performance_test $performance_test_val $performance_test_dir "WireGuard" 10.0.0.1 + fi + + let "progress++" + done + + bar=$(progress_bar $n_values $n_values) + echo -e "\t$rep_description$bar Performance testing with $performance_test_var finished" +} + # Set up WireGuard connection between the peers (for performance test baseline) function wg_setup() { # Counter for virtual IP addresses @@ -207,17 +244,6 @@ function wg_setup() { ip netns exec $peer2 wg set wg_$peer2 peer $pub1 allowed-ips 10.0.0.1/32 endpoint 192.168.1.254:$port1 } -# Directory to store performance test results -performance_test_dir=$log_dir/performance_tests_$performance_test_var - -# Replace commas by spaces to convert string to array -performance_test_values=$(echo $performance_test_values | tr ',' ' ') -performance_test_values=($performance_test_values) - -# Variables to display a progress bar -n_values=${#performance_test_values[@]} -progress=0 - # For the baseline comparison, we need the peers' public IPs, which are also needed to setup a WireGuard connection between them if [[ $baseline == true ]]; then peer1_pub_ip=$(ip netns exec $peer1 ip address | grep -Eo "inet 192.168.[0-9.]+" | cut -d ' ' -f2) @@ -234,24 +260,23 @@ if [[ $baseline == true ]]; then keep_alive_pid=$! fi -# Iterate over performance test values -for performance_test_val in ${performance_test_values[@]}; do - bar=$(progress_bar $progress $n_values) - echo -ne "\033[2K\t$bar Performance testing with $performance_test_var = $performance_test_val\r" # \033[2K = Ctrl+K, clears rest of line from cursor; \r returns to beginning of line +# Replace commas by spaces to convert string to array +performance_test_value_array=$(echo $performance_test_values | tr ',' ' ') +performance_test_value_array=($performance_test_value_array) - # Run performance test for eduP2P - performance_test $performance_test_val $performance_test_dir "eduP2P" $peer1_ip +if [[ $performance_test_reps -gt 1 ]]; then + for ((i=1;i<=$performance_test_reps;i++)); do + # Directory to store performance test results for this repetition + performance_test_dir=$log_dir/performance_tests_$performance_test_var/repetition$i - # If -b is set, the performance test is repeated over a direct/WireGuard connection instead of over the eduP2P connection - if [[ $baseline == true ]]; then - performance_test $performance_test_val $performance_test_dir "Direct" $peer1_pub_ip - performance_test $performance_test_val $performance_test_dir "WireGuard" 10.0.0.1 - fi + performance_tests $performance_test_value_array $performance_test_dir $i + done +else + # No unnecessary subdirectories if test is run only once + performance_test_dir=$log_dir/performance_tests_$performance_test_var - let "progress++" -done + performance_tests $performance_test_value_array $performance_test_dir $i +fi -bar=$(progress_bar $n_values $n_values) -echo -e "\t$bar Performance testing with $performance_test_var finished" clean_exit 0 diff --git a/test_suite/system_test.sh b/test_suite/system_test.sh index 6e473e2..c4d8137 100755 --- a/test_suite/system_test.sh +++ b/test_suite/system_test.sh @@ -12,6 +12,7 @@ Usage: ${0} [OPTIONAL ARGUMENTS] [NAT CO -k -v -d + -r -b With this flag, eduP2P's performance is compared to the performance of a direct connection, and a connection using only WireGuard This flag should only be used when both peers reside in the 'public' network @@ -38,10 +39,11 @@ If [WIREGUARD INTERFACE 1] or [WIREGUARD INTERFACE 2] is not provided, the corre # Use functions and constants from util.sh . ./util.sh -performance_test_duration=0 # Default value in case -d is not used +performance_test_duration=5 # Default value in case -d is not used +performance_test_reps=1 # Default value in case -r is not used # Validate optional arguments -while getopts ":k:v:d:bh" opt; do +while getopts ":k:v:d:r:bh" opt; do case $opt in k) performance_test_var=$OPTARG @@ -61,6 +63,15 @@ while getopts ":k:v:d:bh" opt; do performance_test_duration=$OPTARG validate_str $performance_test_duration "^[0-9]+$" ;; + r) + performance_test_reps=$OPTARG + validate_str $performance_test_duration "^[0-9]+$" + + if [[ $performance_test_reps -eq 0 ]]; then + exit_with_error "value of -r should be at least 1" + fi + ;; + b) baseline="-b" ;; @@ -300,7 +311,7 @@ if [[ -n $performance_test_var ]]; then peer1_ns=${peer_ns_list[0]} peer1_ip=$(extract_ipv4 $peer1_ns $peer1_interface) - sudo ./performance_test.sh $baseline $peer1_ns ${peer_ns_list[1]} $peer1_ip $performance_test_var $performance_test_values $performance_test_duration $log_dir + sudo ./performance_test.sh $baseline $peer1_ns ${peer_ns_list[1]} $peer1_ip $performance_test_var $performance_test_values $performance_test_duration $performance_test_reps $log_dir if [[ $? -ne 0 ]]; then clean_exit 1 From f465a1c087b678a936188bf6c2c47cb299da5c69 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 12:38:05 +0100 Subject: [PATCH 02/82] golangci-lint run Ran with golangci-lint run ./... -E stylecheck,revive,gocritic also with additional commented out code being removed --- cmd/control_server/main.go | 55 +++--- cmd/dev_client/main.go | 156 +++++------------- cmd/relay_server/main.go | 32 ++-- {ext_wg => extwg}/wgctrl.go | 4 +- test_suite/control_server/main.go | 48 +++--- test_suite/relay_server/main.go | 33 ++-- test_suite/test_client/main.go | 34 ++-- toversok/actors/a_conn.go | 2 +- toversok/actors/a_conn_test.go | 4 +- toversok/actors/a_direct.go | 35 +--- toversok/actors/a_direct_test.go | 9 +- toversok/actors/a_eman.go | 92 +---------- toversok/actors/a_eman_test.go | 2 +- toversok/actors/a_relay.go | 2 +- toversok/actors/a_relay_test.go | 10 +- toversok/actors/a_sman.go | 16 +- toversok/actors/a_sman_test.go | 9 +- toversok/actors/a_sockrecv.go | 5 +- toversok/actors/a_tman.go | 25 ++- toversok/actors/common.go | 2 +- .../{peer_state => peerstate}/common.go | 11 +- .../{peer_state => peerstate}/e_half.go | 46 +++--- .../{peer_state => peerstate}/e_rendez.go | 44 +++-- .../e_t_finalising.go | 14 +- .../{peer_state => peerstate}/e_t_half.go | 14 +- .../e_t_pretransmit.go | 14 +- .../{peer_state => peerstate}/e_t_rendez.go | 14 +- .../e_transmitting.go | 45 +++-- .../actors/{peer_state => peerstate}/iface.go | 6 +- .../{peer_state => peerstate}/peer_state.go | 2 +- .../peer_state.mermaid | 0 .../s_established.go | 59 ++++--- .../{peer_state => peerstate}/s_inactive.go | 44 ++--- .../{peer_state => peerstate}/s_t_booting.go | 10 +- .../{peer_state => peerstate}/s_t_teardown.go | 24 +-- .../{peer_state => peerstate}/s_trying.go | 45 +++-- .../{peer_state => peerstate}/s_waiting.go | 14 +- .../actors/{peer_state => peerstate}/util.go | 22 +-- toversok/actors/rehearsal_test.go | 1 + toversok/actors/stage.go | 64 +------ toversok/actors/util.go | 3 +- toversok/actors/util_test.go | 12 +- toversok/control_conn.go | 5 +- toversok/engine.go | 145 +++------------- toversok/session.go | 4 +- types/control/client.go | 78 +-------- types/control/conn.go | 4 - types/control/controlhttp/http_client.go | 2 +- types/control/graph.go | 8 +- types/control/iface.go | 4 +- types/control/logic.go | 9 +- types/control/server.go | 97 +++++------ types/control/server_session.go | 93 ++++------- types/dial/tcp.go | 2 +- types/key/bson.go | 50 ------ types/key/iface.go | 11 +- types/misc.go | 12 +- types/msgactor/notif.go | 3 + types/relay/serverclient.go | 4 +- usrwg/bind.go | 26 +-- usrwg/channel_conn.go | 4 +- usrwg/router/router_windows.go | 6 +- usrwg/router/util.go | 12 +- usrwg/wgusp.go | 61 ++----- 64 files changed, 604 insertions(+), 1119 deletions(-) rename {ext_wg => extwg}/wgctrl.go (99%) rename toversok/actors/{peer_state => peerstate}/common.go (89%) rename toversok/actors/{peer_state => peerstate}/e_half.go (54%) rename toversok/actors/{peer_state => peerstate}/e_rendez.go (57%) rename toversok/actors/{peer_state => peerstate}/e_t_finalising.go (67%) rename toversok/actors/{peer_state => peerstate}/e_t_half.go (70%) rename toversok/actors/{peer_state => peerstate}/e_t_pretransmit.go (71%) rename toversok/actors/{peer_state => peerstate}/e_t_rendez.go (76%) rename toversok/actors/{peer_state => peerstate}/e_transmitting.go (70%) rename toversok/actors/{peer_state => peerstate}/iface.go (79%) rename toversok/actors/{peer_state => peerstate}/peer_state.go (96%) rename toversok/actors/{peer_state => peerstate}/peer_state.mermaid (100%) rename toversok/actors/{peer_state => peerstate}/s_established.go (75%) rename toversok/actors/{peer_state => peerstate}/s_inactive.go (56%) rename toversok/actors/{peer_state => peerstate}/s_t_booting.go (81%) rename toversok/actors/{peer_state => peerstate}/s_t_teardown.go (66%) rename toversok/actors/{peer_state => peerstate}/s_trying.go (54%) rename toversok/actors/{peer_state => peerstate}/s_waiting.go (73%) rename toversok/actors/{peer_state => peerstate}/util.go (74%) delete mode 100644 types/key/bson.go diff --git a/cmd/control_server/main.go b/cmd/control_server/main.go index 92a544a..7b85708 100644 --- a/cmd/control_server/main.go +++ b/cmd/control_server/main.go @@ -26,10 +26,8 @@ import ( ) var ( - //dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)") addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.") configPath = flag.String("c", "", "config file path") - //stunPort = flag.Int("stun-port", stunserver.DefaultPort, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.") publicFacingBaseString = flag.String("u", "", "public facing base URL (required)") publicFacingBase *url.URL @@ -92,9 +90,11 @@ func main() { mux.Handle("/", handleStaticHTML(ToverSokControlDefaultHTML)) - mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { browserHeaders(w) - io.WriteString(w, "User-agent: *\nDisallow: /\n") + if _, err := io.WriteString(w, "User-agent: *\nDisallow: /\n"); err != nil { + slog.Error("could not write robots.txt", "err", err) + } })) mux.Handle("/generate_204", http.HandlerFunc(serverCaptivePortalBuster)) @@ -125,7 +125,9 @@ func main() { go func() { <-ctx.Done() - httpsrv.Shutdown(ctx) + if err := httpsrv.Shutdown(ctx); err != nil { + slog.Error("could not shutdown control server", "err", err) + } }() // TODO setup TLS with autocert? @@ -134,7 +136,7 @@ func main() { err = httpsrv.ListenAndServe() if err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatalf("control: error %s", err) + log.Fatalf("control: error %s", err) //nolint:gocritic } } @@ -149,7 +151,7 @@ type ControlServer struct { func (cs *ControlServer) HandleAuthRequest(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { - http.Redirect(w, r, "/auth/land", 302) + http.Redirect(w, r, "/auth/land", http.StatusFound) return } @@ -165,16 +167,16 @@ func (cs *ControlServer) HandleAuthRequest(w http.ResponseWriter, r *http.Reques return } - http.Redirect(w, r, "/auth/success", 302) + http.Redirect(w, r, "/auth/success", http.StatusFound) } else { // Fail - http.Redirect(w, r, "/auth/fail", 302) + http.Redirect(w, r, "/auth/fail", http.StatusFound) } } func (cs *ControlServer) OnSessionCreate(id control.SessID, cid control.ClientID) { - println("OnSessionCreate") + slog.Info("OnSessionCreate", "id", id, "cid", cid) if cs.isKnown(key.NodePublic(cid)) { go func() { @@ -186,31 +188,28 @@ func (cs *ControlServer) OnSessionCreate(id control.SessID, cid control.ClientID return } - url, _ := url.Parse(string("/auth/land?session=" + id)) - if err := cs.server.SendAuthURL(id, publicFacingBase.ResolveReference(url).String()); err != nil { + redirectURL, _ := url.Parse(string("/auth/land?session=" + id)) + if err := cs.server.SendAuthURL(id, publicFacingBase.ResolveReference(redirectURL).String()); err != nil { slog.Error("error sending auth URL", "id", id, "err", err) } } -func (cs *ControlServer) OnSessionResume(id control.SessID, id2 control.ClientID) { - println("OnSessionResume") - return // noop +func (cs *ControlServer) OnSessionResume(sess control.SessID, cid control.ClientID) { + slog.Info("OnSessionResume", "sess", sess, "cid", cid) } -func (cs *ControlServer) OnDeviceKey(id control.SessID, key string) { - println("OnDeviceKey") - return // noop +func (cs *ControlServer) OnDeviceKey(sess control.SessID, deviceKey string) { + slog.Info("OnDeviceKey", "sess", sess, "deviceKey", deviceKey) } -func (cs *ControlServer) OnSessionFinalize(id control.SessID, id2 control.ClientID) (netip.Prefix, netip.Prefix) { - println("OnSessionFinalize") +func (cs *ControlServer) OnSessionFinalize(sess control.SessID, cid control.ClientID) (netip.Prefix, netip.Prefix) { + slog.Info("OnSessionFinalize", "sess", sess, "cid", cid) - return cs.getIPs(key.NodePublic(id2)) + return cs.getIPs(key.NodePublic(cid)) } -func (cs *ControlServer) OnSessionDestroy(id control.SessID, id2 control.ClientID) { - println("OnSessionDestroy") - return // noop +func (cs *ControlServer) OnSessionDestroy(sess control.SessID, cid control.ClientID) { + slog.Info("OnSessionDestroy", "sess", sess, "cid", cid) } func LoadServer(ctx context.Context) *ControlServer { @@ -371,14 +370,16 @@ func handleStaticHTML(doc string) http.HandlerFunc { } } -func sendStaticHTML(doc string, w http.ResponseWriter, r *http.Request) { +func sendStaticHTML(doc string, w http.ResponseWriter, _ *http.Request) { browserHeaders(w) w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) - io.WriteString(w, doc) + if _, err := io.WriteString(w, doc); err != nil { + slog.Error("failed to write static HTML page", "error", err) + } } const ToverSokControlDefaultHTML = ` diff --git a/cmd/dev_client/main.go b/cmd/dev_client/main.go index e4c1f4b..ecf6b7e 100644 --- a/cmd/dev_client/main.go +++ b/cmd/dev_client/main.go @@ -8,7 +8,7 @@ import ( "flag" "fmt" "github.com/abiosoft/ishell/v2" - "github.com/edup2p/common/ext_wg" + "github.com/edup2p/common/extwg" "github.com/edup2p/common/toversok" "github.com/edup2p/common/toversok/actors" "github.com/edup2p/common/types" @@ -34,7 +34,7 @@ import ( var ( programLevel = new(slog.LevelVar) // Info by default - wgCtrl *ext_wg.WGCtrl + wgCtrl *extwg.WGCtrl usrWg *usrwg.UserSpaceWireGuardHost wg toversok.WireGuardHost @@ -43,9 +43,6 @@ var ( privKey *key.NodePrivate - //ip4 *netip.Prefix - //ip6 *netip.Prefix - fakeControl StokControl properControl toversok.DefaultControlHost usedControl toversok.ControlHost @@ -74,7 +71,9 @@ func main() { if err != nil { log.Fatal(err) } - pprof.StartCPUProfile(f) + if err := pprof.StartCPUProfile(f); err != nil { + panic(err) + } defer pprof.StopCPUProfile() } @@ -97,7 +96,7 @@ func main() { logCmd.AddCmd(&ishell.Cmd{ Name: "info", Help: "set log level to info", - Func: func(c *ishell.Context) { + Func: func(_ *ishell.Context) { programLevel.Set(slog.LevelInfo) }, }) @@ -105,7 +104,7 @@ func main() { logCmd.AddCmd(&ishell.Cmd{ Name: "debug", Help: "set log level to debug", - Func: func(c *ishell.Context) { + Func: func(_ *ishell.Context) { programLevel.Set(slog.LevelDebug) }, }) @@ -113,7 +112,7 @@ func main() { logCmd.AddCmd(&ishell.Cmd{ Name: "trace", Help: "set log level to trace", - Func: func(c *ishell.Context) { + Func: func(_ *ishell.Context) { programLevel.Set(-8) }, }) @@ -126,9 +125,6 @@ func main() { shell.AddCmd(pcCmd()) shell.AddCmd(fcCmd()) - //shell.AddCmd(tsCmd()) - //shell.AddCmd(ctrlCmd()) - shell.Run() if engine != nil { @@ -196,12 +192,13 @@ func keyCmd() *ishell.Cmd { line = c.Args[0] } - if p, err := key.UnmarshalPrivate(line); err != nil { + p, err := key.UnmarshalPrivate(line) + if err != nil { c.Err(err) return - } else { - privKey = p } + + privKey = p }, }) @@ -288,7 +285,7 @@ func pcCmd() *ishell.Cmd { c.AddCmd(&ishell.Cmd{ Name: "use", Help: "start using the proper control", - Func: func(c *ishell.Context) { + Func: func(_ *ishell.Context) { usedControl = &properControl }, }) @@ -305,12 +302,13 @@ func pcCmd() *ishell.Cmd { line = c.Args[0] } - if p, err := key.UnmarshalControlPublic(line); err != nil { + p, err := key.UnmarshalControlPublic(line) + if err != nil { c.Err(err) return - } else { - properControl.Key = *p } + + properControl.Key = *p }, }) @@ -383,15 +381,12 @@ func fcCmd() *ishell.Cmd { c := &ishell.Cmd{ Name: "fc", Help: "fake controlhost variables and handling", - //Func: func(c *ishell.Context) { - // c.Println("fake control:", fakeControl) - //}, } c.AddCmd(&ishell.Cmd{ Name: "use", Help: "start using the proper control", - Func: func(c *ishell.Context) { + Func: func(_ *ishell.Context) { usedControl = &fakeControl }, }) @@ -433,24 +428,22 @@ func fcCmd() *ishell.Cmd { // We (semi-intentionally) break compatibility with any main network because of this. session = [32]byte(*peerKey) - if ip4, err = netip.ParseAddr(c.Args[2]); err != nil { + ip4, err = netip.ParseAddr(c.Args[2]) + + if err != nil { c.Err(err) return - } else { - if !ip4.Is4() { - c.Err(errors.New("ip4 isnt ipv4")) - return - } + } else if !ip4.Is4() { + c.Err(errors.New("ip4 isnt ipv4")) + return } if ip6, err = netip.ParseAddr(c.Args[3]); err != nil { c.Err(err) return - } else { - if !ip6.Is6() { - c.Err(errors.New("ip6 isnt ipv6")) - return - } + } else if !ip6.Is6() { + c.Err(errors.New("ip6 isnt ipv6")) + return } for _, e := range c.Args[4:] { @@ -478,65 +471,6 @@ func fcCmd() *ishell.Cmd { }, }) - //peerCmd.AddCmd(&ishell.Cmd{ - // Name: "update", - // Aliases: []string{"u"}, - // Help: "update a peer: -r [relay] -e [endpoint,...]", - // Func: func(c *ishell.Context) { - // if len(c.Args) == 0 { - // c.Err(errors.New("did not define peer key")) - // return - // } - // - // peerKey, err := key.UnmarshalPublic(c.Args[0]) - // - // if err != nil { - // c.Err(fmt.Errorf("error parsing peer key: %w", err)) - // return - // } - // - // fs := flag.NewFlagSet("peer-update", flag.ContinueOnError) - // - // r := fs.Int64("r", math.MaxInt64, "relay (int64)") - // endpoints := fs.String("e", "", "endpoints (comma-seperated IPs)") - // - // if err := fs.Parse(c.Args[1:]); err != nil { - // c.Err(fmt.Errorf("could not parse flags: %w", err)) - // return - // } - // - // pu := toversok.PeerUpdate{ - // Key: *peerKey, - // } - // - // if *r != math.MaxInt64 { - // pu.HomeRelayId = gonull.NewNullable(*r) - // } - // - // if *endpoints != "" { - // as := *endpoints - // - // aps := make([]netip.AddrPort, 0) - // - // for _, addr := range strings.Split(as, ",") { - // a, err := netip.ParseAddrPort(addr) - // if err != nil { - // c.Err(err) - // return - // } - // - // aps = append(aps, a) - // } - // - // pu.Endpoints = gonull.NewNullable(aps) - // } - // - // if err = engine.Handle(pu); err != nil { - // c.Err(err) - // } - // }, - //}) - peerCmd.AddCmd(&ishell.Cmd{ Name: "delete", Aliases: []string{"del", "d"}, @@ -747,7 +681,7 @@ func wgCmd() *ishell.Cmd { device = names[choice] } - wgCtrl = ext_wg.NewWGCtrl(client, device) + wgCtrl = extwg.NewWGCtrl(client, device) wg = wgCtrl @@ -771,27 +705,25 @@ func wgCmd() *ishell.Cmd { Name: "init", Help: "Perform Init() on the wg configurator. wg init ", Func: func(c *ishell.Context) { - if len(c.Args) < 2 { + switch { + case len(c.Args) < 2: c.Err(errors.New("usage: privkey addr4 addr6")) return - } else if wg == nil { + case wg == nil: c.Err(errors.New("wg not setup")) - } else { + default: privkeyStr := c.Args[0] addr4Str := c.Args[1] addr6Str := c.Args[2] - privkeySlice, err := hex.DecodeString(privkeyStr) if err != nil { c.Err(err) return } else if len(privkeySlice) != key.Len { - c.Err(errors.New(fmt.Sprintf("unexpected key length, expected 32, got %d", len(privkeySlice)))) + c.Err(fmt.Errorf("unexpected key length, expected 32, got %d", len(privkeySlice))) return } - privkey := key.NodePrivateFrom((key.NakedKey)(privkeySlice)) - addr4, err := netip.ParsePrefix(addr4Str) if err != nil { c.Err(err) @@ -800,7 +732,6 @@ func wgCmd() *ishell.Cmd { c.Err(errors.New("first argument is not ipv4 address/cidr")) return } - addr6, err := netip.ParsePrefix(addr6Str) if err != nil { c.Err(err) @@ -809,13 +740,11 @@ func wgCmd() *ishell.Cmd { c.Err(errors.New("second argument is not ipv6 address/cidr")) return } - wgC, err = wg.Controller(privkey, addr4, addr6) if err != nil { c.Err(err) return } - c.Println("wg controller:", wgC) } }, @@ -850,13 +779,15 @@ func enCmd() *ishell.Cmd { Func: func(c *ishell.Context) { var err error - if usedControl == nil { + switch { + case usedControl == nil: err = errors.New("no control host set") - } else if wg == nil { + case wg == nil: err = errors.New("wg is not set") - } else if privKey == nil { + case privKey == nil: err = errors.New("key is not set") } + if err != nil { c.Err(err) return @@ -871,20 +802,13 @@ func enCmd() *ishell.Cmd { } ctx, ccc := context.WithCancelCause(context.Background()) - //opts := toversok.EngineOptions{ - // Ctx: ctx, - // Ccc: ccc, - // PrivKey: key.UnveilPrivate(*privKey), - // ExtBindPort: engineExtPort, - // WG: wg, - // FW: nil, - //} fw := &StokFirewall{} e, err := toversok.NewEngine(ctx, wg, fw, usedControl, engineExtPort, *privKey) if err != nil { c.Err(err) + ccc(err) return } @@ -993,7 +917,7 @@ func (s *StokControl) InstallCallbacks(callbacks ifaces.ControlCallbacks) { } } -func (s *StokControl) CreateClient(parentCtx context.Context, getNode func() *key.NodePrivate, getSess func() *key.SessionPrivate, login types.LogonCallback) (ifaces.ControlSession, error) { +func (s *StokControl) CreateClient(context.Context, func() *key.NodePrivate, func() *key.SessionPrivate, types.LogonCallback) (ifaces.ControlSession, error) { return s, nil } diff --git a/cmd/relay_server/main.go b/cmd/relay_server/main.go index 3338c9a..78c10aa 100644 --- a/cmd/relay_server/main.go +++ b/cmd/relay_server/main.go @@ -45,9 +45,6 @@ const ToverSokRelayDefaultHTML = ` func main() { flag.Parse() - ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer cancel() - if *dev { *addr = "127.0.0.1:3340" log.Printf("Running in dev mode.") @@ -71,8 +68,15 @@ func main() { log.Fatalf("could not parse stun-combined addrport: %v", err) } + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + stunServer := stunserver.NewServer(ctx) - go stunServer.ListenAndServe(ap) + go func() { + if err := stunServer.ListenAndServe(ap); err != nil { + slog.Error("stun server listen error", "err", err) + } + }() // TODO add STUN here @@ -88,19 +92,23 @@ func main() { mux.Handle("/relay", relayhttp.ServerHandler(server)) - mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { browserHeaders(w) w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) - io.WriteString(w, ToverSokRelayDefaultHTML) + if _, err := io.WriteString(w, ToverSokRelayDefaultHTML); err != nil { + slog.Error("failed to write default HTML response", "err", err) + } })) - mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { browserHeaders(w) - io.WriteString(w, "User-agent: *\nDisallow: /\n") + if _, err := io.WriteString(w, "User-agent: *\nDisallow: /\n"); err != nil { + slog.Error("failed to write robots.txt", "err", err) + } })) mux.Handle("/generate_204", http.HandlerFunc(serverCaptivePortalBuster)) @@ -116,7 +124,9 @@ func main() { go func() { <-ctx.Done() - httpsrv.Shutdown(ctx) + if err := httpsrv.Shutdown(ctx); err != nil { + slog.Error("failed to shutdown server", "err", err) + } }() // TODO setup TLS with autocert @@ -125,7 +135,7 @@ func main() { err = httpsrv.ListenAndServe() if err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatalf("relay: error %s", err) + log.Fatalf("relay: error %s", err) //nolint:gocritic } } diff --git a/ext_wg/wgctrl.go b/extwg/wgctrl.go similarity index 99% rename from ext_wg/wgctrl.go rename to extwg/wgctrl.go index 91b152a..935cec2 100644 --- a/ext_wg/wgctrl.go +++ b/extwg/wgctrl.go @@ -1,4 +1,4 @@ -package ext_wg +package extwg import ( "fmt" @@ -255,7 +255,7 @@ func (w *WGCtrl) bindLocal() *mapping { } func (w *WGCtrl) getWGConn(fromPort *uint16) (*net.UDPConn, error) { - var laddr *net.UDPAddr = nil + var laddr *net.UDPAddr if fromPort != nil { laddr = net.UDPAddrFromAddrPort( diff --git a/test_suite/control_server/main.go b/test_suite/control_server/main.go index aa4e62e..f3357b7 100644 --- a/test_suite/control_server/main.go +++ b/test_suite/control_server/main.go @@ -25,10 +25,8 @@ import ( ) var ( - //dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)") addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.") configPath = flag.String("c", "", "config file path") - //stunPort = flag.Int("stun-port", stunserver.DefaultPort, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.") programLevel = new(slog.LevelVar) // Info by default ) @@ -36,7 +34,6 @@ var ( func main() { h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ Level: programLevel, - //AddSource: true, }) slog.SetDefault(slog.New(h)) programLevel.Set(-8) @@ -58,9 +55,11 @@ func main() { mux.Handle("/", handleStaticHTML(ToverSokControlDefaultHTML)) - mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { browserHeaders(w) - io.WriteString(w, "User-agent: *\nDisallow: /\n") + if _, err := io.WriteString(w, "User-agent: *\nDisallow: /\n"); err != nil { + slog.Error("could not write robots.txt", "err", err) + } })) mux.Handle("/generate_204", http.HandlerFunc(serverCaptivePortalBuster)) @@ -76,7 +75,9 @@ func main() { go func() { <-ctx.Done() - httpsrv.Shutdown(ctx) + if err := httpsrv.Shutdown(ctx); err != nil { + slog.Error("control: failed to shutdown control server", "error", err) + } }() // TODO setup TLS with autocert? @@ -85,7 +86,7 @@ func main() { err := httpsrv.ListenAndServe() if err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatalf("control: error %s", err) + log.Fatalf("control: error %s", err) //nolint:gocritic } } @@ -99,7 +100,7 @@ type ControlServer struct { } func (cs *ControlServer) OnSessionCreate(id control.SessID, cid control.ClientID) { - println("OnSessionCreate") + slog.Info("OnSessionCreate", "id", id, "cid", cid) go func() { if err := cs.server.AcceptAuthentication(id); err != nil { @@ -108,25 +109,22 @@ func (cs *ControlServer) OnSessionCreate(id control.SessID, cid control.ClientID }() } -func (cs *ControlServer) OnSessionResume(id control.SessID, id2 control.ClientID) { - println("OnSessionResume") - return // noop +func (cs *ControlServer) OnSessionResume(sess control.SessID, cid control.ClientID) { + slog.Info("OnSessionResume", "sess", sess, "cid", cid) } -func (cs *ControlServer) OnDeviceKey(id control.SessID, key string) { - println("OnDeviceKey") - return // noop +func (cs *ControlServer) OnDeviceKey(sess control.SessID, deviceKey string) { + slog.Info("OnDeviceKey", "sess", sess, "deviceKey", deviceKey) } -func (cs *ControlServer) OnSessionFinalize(id control.SessID, id2 control.ClientID) (netip.Prefix, netip.Prefix) { - println("OnSessionFinalize") +func (cs *ControlServer) OnSessionFinalize(sess control.SessID, cid control.ClientID) (netip.Prefix, netip.Prefix) { + slog.Info("OnSessionFinalize", "sess", sess, "cid", cid) - return cs.getIPs(key.NodePublic(id2)) + return cs.getIPs(key.NodePublic(cid)) } -func (cs *ControlServer) OnSessionDestroy(id control.SessID, id2 control.ClientID) { - println("OnSessionDestroy") - return // noop +func (cs *ControlServer) OnSessionDestroy(sess control.SessID, cid control.ClientID) { + slog.Info("OnSessionDestroy", "sess", sess, "cid", cid) } func LoadServer(ctx context.Context) *ControlServer { @@ -183,7 +181,7 @@ func (cs *ControlServer) addNewNode(node key.NodePublic) { } } -func (cs *ControlServer) isKnown(node key.NodePublic) bool { +func (cs *ControlServer) isKnown(node key.NodePublic) bool { //nolint:unused cs.cfgMu.Lock() defer cs.cfgMu.Unlock() @@ -287,14 +285,16 @@ func handleStaticHTML(doc string) http.HandlerFunc { } } -func sendStaticHTML(doc string, w http.ResponseWriter, r *http.Request) { +func sendStaticHTML(doc string, w http.ResponseWriter, _ *http.Request) { browserHeaders(w) w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) - io.WriteString(w, doc) + if _, err := io.WriteString(w, doc); err != nil { + slog.Error("failed to write static HTML page", "error", err) + } } const ToverSokControlDefaultHTML = ` diff --git a/test_suite/relay_server/main.go b/test_suite/relay_server/main.go index 3338c9a..a07807e 100644 --- a/test_suite/relay_server/main.go +++ b/test_suite/relay_server/main.go @@ -45,9 +45,6 @@ const ToverSokRelayDefaultHTML = ` func main() { flag.Parse() - ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer cancel() - if *dev { *addr = "127.0.0.1:3340" log.Printf("Running in dev mode.") @@ -71,8 +68,16 @@ func main() { log.Fatalf("could not parse stun-combined addrport: %v", err) } + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + stunServer := stunserver.NewServer(ctx) - go stunServer.ListenAndServe(ap) + go func() { + if err := stunServer.ListenAndServe(ap); err != nil { + // This is okay, because running a STUN server is basically also the entire point of the relay server + panic(err) + } + }() // TODO add STUN here @@ -88,19 +93,23 @@ func main() { mux.Handle("/relay", relayhttp.ServerHandler(server)) - mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { browserHeaders(w) w.Header().Set("Content-Type", "text/html; charset=utf-8") - w.WriteHeader(200) + w.WriteHeader(http.StatusOK) - io.WriteString(w, ToverSokRelayDefaultHTML) + if _, err := io.WriteString(w, ToverSokRelayDefaultHTML); err != nil { + slog.Error("Failed to write default HTML response", "err", err) + } })) - mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { browserHeaders(w) - io.WriteString(w, "User-agent: *\nDisallow: /\n") + if _, err := io.WriteString(w, "User-agent: *\nDisallow: /\n"); err != nil { + slog.Error("Failed to write robots.txt response", "err", err) + } })) mux.Handle("/generate_204", http.HandlerFunc(serverCaptivePortalBuster)) @@ -116,7 +125,9 @@ func main() { go func() { <-ctx.Done() - httpsrv.Shutdown(ctx) + if err := httpsrv.Shutdown(ctx); err != nil { + slog.Error("Failed to shutdown server", "err", err) + } }() // TODO setup TLS with autocert @@ -125,7 +136,7 @@ func main() { err = httpsrv.ListenAndServe() if err != nil && !errors.Is(err, http.ErrServerClosed) { - log.Fatalf("relay: error %s", err) + log.Fatalf("relay: error %s", err) //nolint:gocritic } } diff --git a/test_suite/test_client/main.go b/test_suite/test_client/main.go index e33f8c1..c28d1b0 100644 --- a/test_suite/test_client/main.go +++ b/test_suite/test_client/main.go @@ -6,7 +6,7 @@ import ( "errors" "flag" "fmt" - "github.com/edup2p/common/ext_wg" + "github.com/edup2p/common/extwg" "github.com/edup2p/common/toversok" "github.com/edup2p/common/types/dial" "github.com/edup2p/common/types/key" @@ -94,17 +94,17 @@ func main() { if extPort < 0 || extPort > 65535 { slog.Error("external port out of range 0-65535, aborting", "ext-port", extPort) os.Exit(1) - } else { - engineExtPort = uint16(extPort) } + engineExtPort = uint16(extPort) + if controlPort < 0 || controlPort > 65535 { slog.Error("control port out of range 0-65535, aborting", "control-port", controlPort) os.Exit(1) - } else { - controlPort16 = uint16(controlPort) } + controlPort16 = uint16(controlPort) + var err error if parsedControlKey, err = parseControlKey(controlKeyStr); err != nil { @@ -212,13 +212,12 @@ func parseControlKey(str string) (*key.ControlPublic, error) { if controlKeyStr == "" { return nil, nil } - - if p, err := key.UnmarshalControlPublic(str); err != nil { - + p, err := key.UnmarshalControlPublic(str) + if err != nil { return nil, fmt.Errorf("could not parse control key: %w", err) - } else { - return p, nil } + + return p, nil } func normalisePath(file string) (string, error) { @@ -302,17 +301,18 @@ func writeConfig(c *Config, file string) error { func getWireguardHost() (toversok.WireGuardHost, error) { if extWgDevice != "" { - if wg, err := getWgControl(extWgDevice); err != nil { + wg, err := getWgControl(extWgDevice) + if err != nil { return nil, fmt.Errorf("could not initialise external wireguard device: %w", err) - } else { - return wg, nil } - } else { - return usrwg.NewUsrWGHost(), nil + + return wg, nil } + + return usrwg.NewUsrWGHost(), nil } -func getWgControl(device string) (*ext_wg.WGCtrl, error) { +func getWgControl(device string) (*extwg.WGCtrl, error) { client, err := wgctrl.New() if err != nil { return nil, fmt.Errorf("could not initialise wgctrl: %w", err) @@ -322,7 +322,7 @@ func getWgControl(device string) (*ext_wg.WGCtrl, error) { return nil, fmt.Errorf("could not find/initialise wgctrl device: %w", err) } - return ext_wg.NewWGCtrl(client, device), nil + return extwg.NewWGCtrl(client, device), nil } // A dummy firewall diff --git a/toversok/actors/a_conn.go b/toversok/actors/a_conn.go index d4a7c92..695ef9a 100644 --- a/toversok/actors/a_conn.go +++ b/toversok/actors/a_conn.go @@ -40,7 +40,7 @@ func MakeOutConn(udp types.UDPConn, peer key.NodePublic, homeRelay int64, s *Sta return &OutConn{ ActorCommon: common, - sock: MakeSockRecv(udp, common.ctx), + sock: MakeSockRecv(common.ctx, udp), s: s, peer: peer, diff --git a/toversok/actors/a_conn_test.go b/toversok/actors/a_conn_test.go index 6e9dd0d..25aad77 100644 --- a/toversok/actors/a_conn_test.go +++ b/toversok/actors/a_conn_test.go @@ -27,10 +27,10 @@ func TestOutConn(t *testing.T) { wgConn := &MockUDPConn{ writeCh: make(chan []byte), - setReadDeadline: func(t time.Time) error { + setReadDeadline: func(time.Time) error { return nil }, - readFromUDPAddrPort: func(b []byte) (n int, addr netip.AddrPort, err error) { + readFromUDPAddrPort: func([]byte) (n int, addr netip.AddrPort, err error) { return 0, dummyAddrPort, nil }, } diff --git a/toversok/actors/a_direct.go b/toversok/actors/a_direct.go index 04ab40e..9fbad18 100644 --- a/toversok/actors/a_direct.go +++ b/toversok/actors/a_direct.go @@ -31,7 +31,7 @@ func (s *Stage) makeDM(udpSocket types.UDPConn) *DirectManager { c := MakeCommon(s.Ctx, -1) return &DirectManager{ ActorCommon: c, - sock: MakeSockRecv(udpSocket, c.ctx), + sock: MakeSockRecv(c.ctx, udpSocket), s: s, writeCh: make(chan directWriteRequest, DirectManWriteChLen), } @@ -60,13 +60,6 @@ func (dm *DirectManager) Run() { case <-dm.ctx.Done(): dm.Close() return - //case msg := <-dm.inbox: - // switch m := msg.(type) { - // case *DManSetMTU: - // dm.SetMTUFor(m.forAddrPort, m.mtu) - // default: - // dm.logUnknownMessage(m) - // } case req := <-dm.writeCh: L(dm).Log(context.Background(), types.LevelTrace, "direct: writing") _, err := dm.sock.Conn.WriteToUDPAddrPort(req.pkt, req.to) @@ -103,26 +96,6 @@ func (dm *DirectManager) WriteTo(pkt []byte, addr netip.AddrPort) { } } -//// MTUFor gets the MTU for a netip.AddrPort pair, or default. -//func (dm *DirectManager) MTUFor(ap netip.AddrPort) uint16 { -// // TODO(jo): there is a small possibility that internal representation in -// // netip.AddrPort can differ, even though they'd be the same IP+Port pair. -// // I haven't found such a case, but it'S nagging in the back of my mind, -// // which is why this is a separate function, -// // so we can do any canonisation later. -// mtu, ok := dm.mtuFor[ap] -// if !ok { -// return DefaultSafeMTU -// } else { -// return mtu -// } -//} -// -//// SetMTUFor sets the MTU for a netip.AddrPort pair. -//func (dm *DirectManager) SetMTUFor(ap netip.AddrPort, mtu uint16) { -// dm.mtuFor[ap] = mtu -//} - type DirectRouter struct { *ActorCommon @@ -148,9 +121,9 @@ func (s *Stage) makeDR() *DirectRouter { } func (dr *DirectRouter) Push(frame ifaces.DirectedPeerFrame) { - //go func() { + // go func() { dr.frameCh <- frame - //}() + // }() } func (dr *DirectRouter) Run() { @@ -230,7 +203,7 @@ func (dr *DirectRouter) peerAKA(ap netip.AddrPort) (peer key.NodePublic, ok bool peer, ok = dr.aka[nap] - //slog.Debug("dr: peerAKA", "ap", ap.String(), "nap", nap, "ok", ok) + slog.Log(context.Background(), types.LevelTrace, "dr: peerAKA", "ap", ap.String(), "nap", nap, "ok", ok) return } diff --git a/toversok/actors/a_direct_test.go b/toversok/actors/a_direct_test.go index c87c728..a34897c 100644 --- a/toversok/actors/a_direct_test.go +++ b/toversok/actors/a_direct_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/netip" + "slices" "testing" "time" @@ -22,13 +23,13 @@ func TestDirectManager(t *testing.T) { mockUDPConn := &MockUDPConn{ writeCh: make(chan []byte), - setReadDeadline: func(t time.Time) error { + setReadDeadline: func(time.Time) error { return nil }, - readFromUDPAddrPort: func(b []byte) (n int, addr netip.AddrPort, err error) { + readFromUDPAddrPort: func([]byte) (n int, addr netip.AddrPort, err error) { return 0, dummyAddrPort, nil }, - writeToUDPAddrPort: func(b []byte, addr netip.AddrPort) (int, error) { + writeToUDPAddrPort: func([]byte, netip.AddrPort) (int, error) { return 0, nil }, } @@ -111,7 +112,7 @@ func TestDirectRouter(t *testing.T) { assert.Equal(t, msgEM, &msgactor.EManSTUNResponse{Endpoint: frameEndpoint.SrcAddrPort, Packet: frameEndpoint.Pkt}, "EndpointManager did not receive the expected message") // Message that should be sent to SessionManager - sessionPkt := append(msgsess.MagicBytes, zeroBytes(56)...) + sessionPkt := slices.Concat(msgsess.MagicBytes, zeroBytes(56)) frameSession := ifaces.DirectedPeerFrame{ SrcAddrPort: dummyAddrPort, diff --git a/toversok/actors/a_eman.go b/toversok/actors/a_eman.go index 4b5f934..1f5b08f 100644 --- a/toversok/actors/a_eman.go +++ b/toversok/actors/a_eman.go @@ -266,96 +266,6 @@ func (em *EndpointManager) endpointToRelay(ap netip.AddrPort) *int64 { return nil } -//func (em *EndpointManager) updateEndpoints() { -// ep, err := em.doSTUN(EManStunTimeout) -// if err != nil { -// if ep != nil && len(ep) > 1 { -// L(em).Warn("STUN completed with error", "endpoints", ep, "err", err) -// } else { -// L(em).Warn("STUN failed with error", "err", err) -// } -// } -// if ep != nil && len(ep) > 1 { -// em.s.setSTUNEndpoints(ep) -// L(em).Info("STUN completed", "endpoints", ep) -// } else { -// L(em).Warn("STUN completed with no endpoints") -// } -//} - -//// Performs STUN on all known servers, returns all (deduplicated) results, and any error (if there is one). -//func (em *EndpointManager) doSTUN(timeout time.Duration) (responses []netip.AddrPort, err error) { -// var c *net.UDPConn -// -// c, err = net.ListenUDP("udp", nil) -// if err != nil { -// return nil, fmt.Errorf("failed to open UDP socket: %w", err) -// } -// -// requests := make(map[netip.AddrPort]stun.TxID) -// -// for _, ep := range em.collectSTUNEndpoints() { -// txID := stun.NewTxID() -// req := stun.Request(txID) -// -// _, err = c.WriteToUDP(req, net.UDPAddrFromAddrPort(ep)) -// if err != nil { -// return nil, fmt.Errorf("failed to write to %s: %w", ep, err) -// } -// -// requests[ep] = txID -// } -// -// if err := c.SetReadDeadline(time.Now().Add(timeout)); err != nil { -// return nil, fmt.Errorf("failed to set read deadline: %w", err) -// } -// -// var responseMap = make(map[netip.AddrPort]bool) -// -// for { -// if len(requests) == 0 { -// break -// } -// -// var buf [1024]byte -// var n int -// var raddr netip.AddrPort -// -// n, raddr, err = c.ReadFromUDPAddrPort(buf[:]) -// if err != nil { -// break -// } -// -// if raddr.Addr().Is4In6() { -// raddr = netip.AddrPortFrom(netip.AddrFrom4(raddr.Addr().As4()), raddr.Port()) -// } -// -// if _, ok := requests[raddr]; !ok { -// L(em).Warn("got response from unexpected raddr while doing STUN", "raddr", raddr) -// continue -// } -// -// tid, saddr, err := stun.ParseResponse(buf[:n]) -// if err != nil { -// L(em).Warn("got error when parsing STUN response from raddr", "raddr", raddr, "err", err) -// continue -// } -// if tid != requests[raddr] { -// L(em).Warn("received different TXID from raddr than expected", "raddr", raddr, "txid.expected", requests[raddr], "txid.got", tid) -// continue -// } -// -// responseMap[saddr] = true -// delete(requests, raddr) -// } -// -// for ep := range responseMap { -// responses = append(responses, ep) -// } -// -// return responses, err -//} - // Collects STUN endpoints from known relay definitions and Control itself func (em *EndpointManager) collectRelaySTUNEndpoints() map[netip.AddrPort]int64 { relayEndpoints := make(map[netip.AddrPort]int64) @@ -424,6 +334,6 @@ func (em *EndpointManager) collectLocalEndpoints() []netip.Addr { } func (em *EndpointManager) Close() { - //TODO implement me + // TODO implement me panic("implement me") } diff --git a/toversok/actors/a_eman_test.go b/toversok/actors/a_eman_test.go index 957c7b3..bfd667c 100644 --- a/toversok/actors/a_eman_test.go +++ b/toversok/actors/a_eman_test.go @@ -56,7 +56,7 @@ func TestEndpointManager(t *testing.T) { mockControl := &MockControl{ endpoints: make([]netip.AddrPort, 0), - updateEndpoints: func(eps []netip.AddrPort) error { return nil }, + updateEndpoints: func([]netip.AddrPort) error { return nil }, } s.control = mockControl diff --git a/toversok/actors/a_relay.go b/toversok/actors/a_relay.go index b3ba5a6..79efc87 100644 --- a/toversok/actors/a_relay.go +++ b/toversok/actors/a_relay.go @@ -372,7 +372,7 @@ func (rm *RelayManager) Close() { } func (rm *RelayManager) selectRelay(latencies map[int64]time.Duration) int64 { - var srid int64 = 0 + var srid int64 var slat = 60 * time.Second L(rm).Debug("selectRelay: starting latency check") diff --git a/toversok/actors/a_relay_test.go b/toversok/actors/a_relay_test.go index d8e640b..f231b44 100644 --- a/toversok/actors/a_relay_test.go +++ b/toversok/actors/a_relay_test.go @@ -3,6 +3,7 @@ package actors import ( "context" "fmt" + "slices" "testing" "github.com/edup2p/common/types/ifaces" @@ -19,9 +20,10 @@ func TestRelayManager(t *testing.T) { frameCh: make(chan ifaces.RelayedPeerFrame, RelayRouterFrameChLen), } - var relayID int64 = 0 + const RelayID int64 = 0 + homeRelay := &RestartableRelayConn{ - config: relay.Information{ID: relayID}, + config: relay.Information{ID: RelayID}, bufferCh: make(chan relay.SendPacket), } @@ -32,7 +34,7 @@ func TestRelayManager(t *testing.T) { // Make and run RelayManager rm := s.makeRM() - rm.relays[relayID] = homeRelay + rm.relays[RelayID] = homeRelay go rm.Run() // Message that should be sent to the relay @@ -89,7 +91,7 @@ func TestRelayRouter(t *testing.T) { go rr.Run() // Message that should be sent to SessionManager - sessionPkt := append(msgsess.MagicBytes, zeroBytes(56)...) + sessionPkt := slices.Concat(msgsess.MagicBytes, zeroBytes(56)) frameSession := ifaces.RelayedPeerFrame{ SrcRelay: 0, diff --git a/toversok/actors/a_sman.go b/toversok/actors/a_sman.go index 9497164..36ce4d1 100644 --- a/toversok/actors/a_sman.go +++ b/toversok/actors/a_sman.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgactor" - msg2 "github.com/edup2p/common/types/msgsess" + "github.com/edup2p/common/types/msgsess" "slices" ) @@ -102,12 +102,12 @@ func (sm *SessionManager) Handle(msg msgactor.ActorMessage) { } } -func (sm *SessionManager) Unpack(frameWithMagic []byte) (*msg2.ClearMessage, error) { - if string(frameWithMagic[:len(msg2.Magic)]) != msg2.Magic { +func (sm *SessionManager) Unpack(frameWithMagic []byte) (*msgsess.ClearMessage, error) { + if string(frameWithMagic[:len(msgsess.Magic)]) != msgsess.Magic { panic("Somehow received non-session message in unpack") } - b := frameWithMagic[len(msg2.Magic):] + b := frameWithMagic[len(msgsess.Magic):] sessionKey := key.MakeSessionPublic([key.Len]byte(b[:key.Len])) @@ -119,24 +119,24 @@ func (sm *SessionManager) Unpack(frameWithMagic []byte) (*msg2.ClearMessage, err return nil, fmt.Errorf("could not decrypt session message") } - sMsg, err := msg2.ParseSessionMessage(clearBytes) + sMsg, err := msgsess.ParseSessionMessage(clearBytes) if err != nil { return nil, fmt.Errorf("could not parse session message: %s", err) } - return &msg2.ClearMessage{ + return &msgsess.ClearMessage{ Session: sessionKey, Message: sMsg, }, nil } -func (sm *SessionManager) Pack(sMsg msg2.SessionMessage, toSession key.SessionPublic) []byte { +func (sm *SessionManager) Pack(sMsg msgsess.SessionMessage, toSession key.SessionPublic) []byte { clearBytes := sMsg.MarshalSessionMessage() cipherBytes := sm.session().Shared(toSession).Seal(clearBytes) - return slices.Concat(msg2.MagicBytes, sm.session().Public().ToByteSlice(), cipherBytes) + return slices.Concat(msgsess.MagicBytes, sm.session().Public().ToByteSlice(), cipherBytes) } func (sm *SessionManager) Session() key.SessionPublic { diff --git a/toversok/actors/a_sman_test.go b/toversok/actors/a_sman_test.go index 2896361..f15bcc1 100644 --- a/toversok/actors/a_sman_test.go +++ b/toversok/actors/a_sman_test.go @@ -6,7 +6,6 @@ import ( "github.com/edup2p/common/types/msgactor" "github.com/edup2p/common/types/msgsess" - msg2 "github.com/edup2p/common/types/msgsess" "github.com/stretchr/testify/assert" ) @@ -24,7 +23,7 @@ func (m *MockSessionMessage) Debug() string { return m.debug() } -func assertEncryptedPacket(t *testing.T, pkt []byte, sm *SessionManager, expectedDecryption *msg2.ClearMessage, failMsg string) { +func assertEncryptedPacket(t *testing.T, pkt []byte, sm *SessionManager, expectedDecryption *msgsess.ClearMessage, failMsg string) { // We cannot predict the encryption with a random nonce, so we unpack the packet in receivedReq to test if it is correct unpacked, ok := sm.Unpack(pkt) assert.Nil(t, ok, "Decryption of packet in received directWriteRequest failed") @@ -53,14 +52,14 @@ func TestSessionManager(t *testing.T) { // Create a test ping message txID := [12]byte{42} pingBytes := append(txID[:], dummyKey[:]...) - clearBytes := append([]byte{1, 0}, pingBytes[:]...) // 1 is version nr, 0 is Ping message + clearBytes := append([]byte{1, 0}, pingBytes...) // 1 is version nr, 0 is Ping message pingMsg := &msgsess.Ping{ TxID: txID, NodeKey: dummyKey, } - clearMsg := &msg2.ClearMessage{ + clearMsg := &msgsess.ClearMessage{ Session: testPub, Message: pingMsg, } @@ -93,7 +92,7 @@ func TestSessionManager(t *testing.T) { assert.Equal(t, expectedFromRelay, receivedFromRelay, "TrafficManager did not receive expected message when sending frame from an address-port pair to SessionManager") - //Test Handle on frame from addrport + // Test Handle on frame from addrport frameFromAddrPort := &msgactor.SManSessionFrameFromAddrPort{ AddrPort: dummyAddrPort, FrameWithMagic: packedBytes, diff --git a/toversok/actors/a_sockrecv.go b/toversok/actors/a_sockrecv.go index 28379b9..e69fdbb 100644 --- a/toversok/actors/a_sockrecv.go +++ b/toversok/actors/a_sockrecv.go @@ -27,13 +27,12 @@ type SockRecv struct { outCh chan RecvFrame } -func MakeSockRecv(udp types.UDPConn, pCtx context.Context) *SockRecv { - +func MakeSockRecv(ctx context.Context, udp types.UDPConn) *SockRecv { return &SockRecv{ Conn: udp, outCh: make(chan RecvFrame, SockRecvFrameChanBuffer), - ActorCommon: MakeCommon(pCtx, -1), + ActorCommon: MakeCommon(ctx, -1), } } diff --git a/toversok/actors/a_tman.go b/toversok/actors/a_tman.go index e63b2ab..654ee81 100644 --- a/toversok/actors/a_tman.go +++ b/toversok/actors/a_tman.go @@ -1,7 +1,7 @@ package actors import ( - "github.com/edup2p/common/toversok/actors/peer_state" + "github.com/edup2p/common/toversok/actors/peerstate" "github.com/edup2p/common/types" "github.com/edup2p/common/types/ifaces" "github.com/edup2p/common/types/key" @@ -21,7 +21,7 @@ type TrafficManager struct { ticker *time.Ticker // 250ms poke chan interface{} // len 1 - peerState map[key.NodePublic]peer_state.PeerState + peerState map[key.NodePublic]peerstate.PeerState pings map[msgsess.TxID]*stage.SentPing activeOut map[key.NodePublic]bool @@ -38,7 +38,7 @@ func (s *Stage) makeTM() *TrafficManager { ticker: time.NewTicker(TManTickerInterval), poke: make(chan interface{}, 1), - peerState: make(map[key.NodePublic]peer_state.PeerState), + peerState: make(map[key.NodePublic]peerstate.PeerState), pings: make(map[msgsess.TxID]*stage.SentPing), activeOut: make(map[key.NodePublic]bool), @@ -110,7 +110,7 @@ func (tm *TrafficManager) Handle(m msgactor.ActorMessage) { n := tm.NodeForSess(m.Msg.Session) if n != nil { - tm.forState(*n, func(s peer_state.PeerState) peer_state.PeerState { + tm.forState(*n, func(s peerstate.PeerState) peerstate.PeerState { return s.OnDirect(types.NormaliseAddrPort(m.AddrPort), m.Msg) }) } else { @@ -123,7 +123,7 @@ func (tm *TrafficManager) Handle(m msgactor.ActorMessage) { return } - tm.forState(m.Peer, func(s peer_state.PeerState) peer_state.PeerState { + tm.forState(m.Peer, func(s peerstate.PeerState) peerstate.PeerState { return s.OnRelay(m.Relay, m.Peer, m.Msg) }) case *msgactor.SyncPeerInfo: @@ -153,7 +153,7 @@ func (tm *TrafficManager) DoStateTick() { // We explicitly range over a slice of the keys we already got, // since golang likes to complain when we mutate while we iterate. for _, peer := range maps2.Keys(tm.peerState) { - tm.forState(peer, func(s peer_state.PeerState) peer_state.PeerState { + tm.forState(peer, func(s peerstate.PeerState) peerstate.PeerState { return s.OnTick() }) } @@ -192,6 +192,7 @@ func (tm *TrafficManager) Poke() { } } +//nolint:unused func (tm *TrafficManager) isConnActive(peer key.NodePublic) bool { return tm.activeOut[peer] || tm.activeIn[peer] } @@ -222,7 +223,7 @@ func (tm *TrafficManager) ensurePeerState(peer key.NodePublic) { s, ok := tm.peerState[peer] if !ok { - tm.peerState[peer] = peer_state.MakeWaiting(tm, peer) + tm.peerState[peer] = peerstate.MakeWaiting(tm, peer) tm.Poke() return } @@ -230,7 +231,7 @@ func (tm *TrafficManager) ensurePeerState(peer key.NodePublic) { if s == nil { // !! this should never happen, but we recover regardless L(tm).Warn("found nil state for peer, restarting state with Waiting", "peer", peer.Debug()) - tm.peerState[peer] = peer_state.MakeWaiting(tm, peer) + tm.peerState[peer] = peerstate.MakeWaiting(tm, peer) tm.Poke() } } @@ -244,7 +245,7 @@ func (tm *TrafficManager) doPingManagement() { // - expire old pings } -type StateForState func(state peer_state.PeerState) peer_state.PeerState +type StateForState func(state peerstate.PeerState) peerstate.PeerState func (tm *TrafficManager) forState(peer key.NodePublic, fn StateForState) { // A state for a state, perfectly balanced, as all things should be. @@ -270,12 +271,6 @@ func (tm *TrafficManager) forState(peer key.NodePublic, fn StateForState) { } } -// TODO see if these correspond in peer_state package -//const EstablishmentTimeout = time.Second * 10 -//const EstablishmentRetry = time.Second * 40 -// -//const EstablishedPingTimeout = time.Second * 5 - func (tm *TrafficManager) DManClearAKA(peer key.NodePublic) { SendMessage(tm.s.DRouter.Inbox(), &msgactor.DRouterPeerClearKnownAs{ Peer: peer, diff --git a/toversok/actors/common.go b/toversok/actors/common.go index a5cbb19..97340da 100644 --- a/toversok/actors/common.go +++ b/toversok/actors/common.go @@ -16,7 +16,7 @@ type ActorCommon struct { func MakeCommon(pCtx context.Context, chLen int) *ActorCommon { ctx, ctxCan := context.WithCancel(pCtx) - var inbox chan msgactor.ActorMessage = nil + var inbox chan msgactor.ActorMessage if chLen >= 0 { inbox = make(chan msgactor.ActorMessage, chLen) diff --git a/toversok/actors/peer_state/common.go b/toversok/actors/peerstate/common.go similarity index 89% rename from toversok/actors/peer_state/common.go rename to toversok/actors/peerstate/common.go index 8b51f33..54c87bb 100644 --- a/toversok/actors/peer_state/common.go +++ b/toversok/actors/peerstate/common.go @@ -1,4 +1,4 @@ -package peer_state +package peerstate import ( "context" @@ -30,7 +30,7 @@ func (sc *StateCommon) Peer() key.NodePublic { return sc.peer } -func (sc *StateCommon) pingDirectValid(ap netip.AddrPort, sess key.SessionPublic, ping *msgsess.Ping) bool { +func (sc *StateCommon) pingDirectValid(_ netip.AddrPort, sess key.SessionPublic, ping *msgsess.Ping) bool { return sc.tm.ValidKeys(ping.NodeKey, sess) } @@ -41,7 +41,8 @@ func (sc *StateCommon) replyWithPongDirect(ap netip.AddrPort, sess key.SessionPu }) } -func (sc *StateCommon) pingRelayValid(relay int64, node key.NodePublic, sess key.SessionPublic, ping *msgsess.Ping) bool { +//nolint:unused +func (sc *StateCommon) pingRelayValid(_ int64, _ key.NodePublic, sess key.SessionPublic, ping *msgsess.Ping) bool { return sc.tm.ValidKeys(ping.NodeKey, sess) } @@ -52,7 +53,7 @@ func (sc *StateCommon) replyWithPongRelay(relay int64, node key.NodePublic, sess } // TODO add bool here and checks by callers -func (sc *StateCommon) ackPongDirect(ap netip.AddrPort, sess key.SessionPublic, pong *msgsess.Pong) { +func (sc *StateCommon) ackPongDirect(_ netip.AddrPort, sess key.SessionPublic, pong *msgsess.Pong) { sent, ok := sc.tm.Pings()[pong.TxID] if !ok { // TODO log: Got pong for unknown ping @@ -77,7 +78,7 @@ func (sc *StateCommon) ackPongDirect(ap netip.AddrPort, sess key.SessionPublic, } // TODO add bool here and checks by callers -func (sc *StateCommon) ackPongRelay(relay int64, node key.NodePublic, sess key.SessionPublic, pong *msgsess.Pong) { +func (sc *StateCommon) ackPongRelay(_ int64, node key.NodePublic, sess key.SessionPublic, pong *msgsess.Pong) { // Relay pongs should come in response to relay pings, note if it is different. sent, ok := sc.tm.Pings()[pong.TxID] diff --git a/toversok/actors/peer_state/e_half.go b/toversok/actors/peerstate/e_half.go similarity index 54% rename from toversok/actors/peer_state/e_half.go rename to toversok/actors/peerstate/e_half.go index 25b9533..8909628 100644 --- a/toversok/actors/peer_state/e_half.go +++ b/toversok/actors/peerstate/e_half.go @@ -1,8 +1,8 @@ -package peer_state +package peerstate import ( "github.com/edup2p/common/types/key" - msg2 "github.com/edup2p/common/types/msgsess" + "github.com/edup2p/common/types/msgsess" "net/netip" "time" ) @@ -28,64 +28,62 @@ func (e *EstHalf) OnTick() PeerState { return nil } -func (e *EstHalf) OnDirect(ap netip.AddrPort, clear *msg2.ClearMessage) PeerState { - if s := cascadeDirect(e, ap, clear); s != nil { +func (e *EstHalf) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) PeerState { + if s := cascadeDirect(e, ap, clearMsg); s != nil { return s } - LogDirectMessage(e, ap, clear) + LogDirectMessage(e, ap, clearMsg) - switch m := clear.Message.(type) { - case *msg2.Ping: - if !e.pingDirectValid(ap, clear.Session, m) { + switch m := clearMsg.Message.(type) { + case *msgsess.Ping: + if !e.pingDirectValid(ap, clearMsg.Session, m) { L(e).Warn("dropping invalid ping", "ap", ap.String()) return nil } - e.replyWithPongDirect(ap, clear.Session, m) + e.replyWithPongDirect(ap, clearMsg.Session, m) // Send one as a hail-mary, for if another got lost - e.tm.SendPingDirect(ap, e.peer, clear.Session) + e.tm.SendPingDirect(ap, e.peer, clearMsg.Session) e.lastPing = time.Now() return nil - case *msg2.Pong: + case *msgsess.Pong: e.tm.Poke() return LogTransition(e, &Finalizing{ EstablishingCommon: e.EstablishingCommon, ap: ap, - sess: clear.Session, + sess: clearMsg.Session, pong: m, }) - //case *msg.Rendezvous: default: L(e).Warn("ignoring direct session message", "ap", ap, - "session", clear.Session, + "session", clearMsg.Session, "msg", m.Debug()) return nil } } -func (e *EstHalf) OnRelay(relay int64, peer key.NodePublic, clear *msg2.ClearMessage) PeerState { - if s := cascadeRelay(e, relay, peer, clear); s != nil { +func (e *EstHalf) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) PeerState { + if s := cascadeRelay(e, relay, peer, clearMsg); s != nil { return s } - LogRelayMessage(e, relay, peer, clear) + LogRelayMessage(e, relay, peer, clearMsg) - switch m := clear.Message.(type) { - case *msg2.Ping: - e.replyWithPongRelay(relay, peer, clear.Session, m) + switch m := clearMsg.Message.(type) { + case *msgsess.Ping: + e.replyWithPongRelay(relay, peer, clearMsg.Session, m) return nil - case *msg2.Pong: - e.ackPongRelay(relay, peer, clear.Session, m) + case *msgsess.Pong: + e.ackPongRelay(relay, peer, clearMsg.Session, m) return nil - //case *msg.Rendezvous: default: L(e).Warn("ignoring relay session message", "relay", relay, "peer", peer, - "session", clear.Session, + "session", clearMsg.Session, "msg", m.Debug()) return nil } diff --git a/toversok/actors/peer_state/e_rendez.go b/toversok/actors/peerstate/e_rendez.go similarity index 57% rename from toversok/actors/peer_state/e_rendez.go rename to toversok/actors/peerstate/e_rendez.go index 7b08253..acff7f7 100644 --- a/toversok/actors/peer_state/e_rendez.go +++ b/toversok/actors/peerstate/e_rendez.go @@ -1,8 +1,8 @@ -package peer_state +package peerstate import ( "github.com/edup2p/common/types/key" - msg2 "github.com/edup2p/common/types/msgsess" + "github.com/edup2p/common/types/msgsess" "net/netip" ) @@ -27,16 +27,16 @@ func (e *EstRendezAck) OnTick() PeerState { return nil } -func (e *EstRendezAck) OnDirect(ap netip.AddrPort, clear *msg2.ClearMessage) PeerState { - if s := cascadeDirect(e, ap, clear); s != nil { +func (e *EstRendezAck) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) PeerState { + if s := cascadeDirect(e, ap, clearMsg); s != nil { return s } - LogDirectMessage(e, ap, clear) + LogDirectMessage(e, ap, clearMsg) - switch m := clear.Message.(type) { - case *msg2.Ping: - if !e.pingDirectValid(ap, clear.Session, m) { + switch m := clearMsg.Message.(type) { + case *msgsess.Ping: + if !e.pingDirectValid(ap, clearMsg.Session, m) { L(e).Warn("dropping invalid ping", "ap", ap.String()) return nil } @@ -45,47 +45,45 @@ func (e *EstRendezAck) OnDirect(ap netip.AddrPort, clear *msg2.ClearMessage) Pee return LogTransition(e, &EstHalfIng{ EstablishingCommon: e.EstablishingCommon, ap: ap, - sess: clear.Session, + sess: clearMsg.Session, ping: m, }) - case *msg2.Pong: + case *msgsess.Pong: e.tm.Poke() return LogTransition(e, &Finalizing{ EstablishingCommon: e.EstablishingCommon, ap: ap, - sess: clear.Session, + sess: clearMsg.Session, pong: m, }) - //case *msg.Rendezvous: default: L(e).Warn("ignoring direct session message", "ap", ap, - "session", clear.Session, + "session", clearMsg.Session, "msg", m.Debug()) return nil } } -func (e *EstRendezAck) OnRelay(relay int64, peer key.NodePublic, clear *msg2.ClearMessage) PeerState { - if s := cascadeRelay(e, relay, peer, clear); s != nil { +func (e *EstRendezAck) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) PeerState { + if s := cascadeRelay(e, relay, peer, clearMsg); s != nil { return s } - LogRelayMessage(e, relay, peer, clear) + LogRelayMessage(e, relay, peer, clearMsg) - switch m := clear.Message.(type) { - case *msg2.Ping: - e.replyWithPongRelay(relay, peer, clear.Session, m) + switch m := clearMsg.Message.(type) { + case *msgsess.Ping: + e.replyWithPongRelay(relay, peer, clearMsg.Session, m) return nil - case *msg2.Pong: - e.ackPongRelay(relay, peer, clear.Session, m) + case *msgsess.Pong: + e.ackPongRelay(relay, peer, clearMsg.Session, m) return nil - //case *msg.Rendezvous: default: L(e).Warn("ignoring relay session message", "relay", relay, "peer", peer, - "session", clear.Session, + "session", clearMsg.Session, "msg", m.Debug()) return nil } diff --git a/toversok/actors/peer_state/e_t_finalising.go b/toversok/actors/peerstate/e_t_finalising.go similarity index 67% rename from toversok/actors/peer_state/e_t_finalising.go rename to toversok/actors/peerstate/e_t_finalising.go index 9d00764..b3014f6 100644 --- a/toversok/actors/peer_state/e_t_finalising.go +++ b/toversok/actors/peerstate/e_t_finalising.go @@ -1,8 +1,8 @@ -package peer_state +package peerstate import ( "github.com/edup2p/common/types/key" - msg2 "github.com/edup2p/common/types/msgsess" + "github.com/edup2p/common/types/msgsess" "net/netip" ) @@ -11,7 +11,7 @@ type Finalizing struct { ap netip.AddrPort sess key.SessionPublic - pong *msg2.Pong + pong *msgsess.Pong } func (f *Finalizing) Name() string { @@ -27,12 +27,12 @@ func (f *Finalizing) OnTick() PeerState { }) } -func (f *Finalizing) OnDirect(ap netip.AddrPort, clear *msg2.ClearMessage) PeerState { +func (f *Finalizing) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) PeerState { // OnTick will transition into the next state regardless, so just pass it along - return cascadeDirect(f, ap, clear) + return cascadeDirect(f, ap, clearMsg) } -func (f *Finalizing) OnRelay(relay int64, peer key.NodePublic, clear *msg2.ClearMessage) PeerState { +func (f *Finalizing) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) PeerState { // OnTick will transition into the next state regardless, so just pass it along - return cascadeRelay(f, relay, peer, clear) + return cascadeRelay(f, relay, peer, clearMsg) } diff --git a/toversok/actors/peer_state/e_t_half.go b/toversok/actors/peerstate/e_t_half.go similarity index 70% rename from toversok/actors/peer_state/e_t_half.go rename to toversok/actors/peerstate/e_t_half.go index 464771f..606aece 100644 --- a/toversok/actors/peer_state/e_t_half.go +++ b/toversok/actors/peerstate/e_t_half.go @@ -1,8 +1,8 @@ -package peer_state +package peerstate import ( "github.com/edup2p/common/types/key" - msg2 "github.com/edup2p/common/types/msgsess" + "github.com/edup2p/common/types/msgsess" "net/netip" "time" ) @@ -12,7 +12,7 @@ type EstHalfIng struct { ap netip.AddrPort sess key.SessionPublic - ping *msg2.Ping + ping *msgsess.Ping } func (e *EstHalfIng) Name() string { @@ -28,12 +28,12 @@ func (e *EstHalfIng) OnTick() PeerState { return LogTransition(e, &EstHalf{EstablishingCommon: e.EstablishingCommon}) } -func (e *EstHalfIng) OnDirect(ap netip.AddrPort, clear *msg2.ClearMessage) PeerState { +func (e *EstHalfIng) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) PeerState { // OnTick will transition into the next state regardless, so just pass it along - return cascadeDirect(e, ap, clear) + return cascadeDirect(e, ap, clearMsg) } -func (e *EstHalfIng) OnRelay(relay int64, peer key.NodePublic, clear *msg2.ClearMessage) PeerState { +func (e *EstHalfIng) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) PeerState { // OnTick will transition into the next state regardless, so just pass it along - return cascadeRelay(e, relay, peer, clear) + return cascadeRelay(e, relay, peer, clearMsg) } diff --git a/toversok/actors/peer_state/e_t_pretransmit.go b/toversok/actors/peerstate/e_t_pretransmit.go similarity index 71% rename from toversok/actors/peer_state/e_t_pretransmit.go rename to toversok/actors/peerstate/e_t_pretransmit.go index 1ef3481..16a5e29 100644 --- a/toversok/actors/peer_state/e_t_pretransmit.go +++ b/toversok/actors/peerstate/e_t_pretransmit.go @@ -1,8 +1,8 @@ -package peer_state +package peerstate import ( "github.com/edup2p/common/types/key" - msg2 "github.com/edup2p/common/types/msgsess" + "github.com/edup2p/common/types/msgsess" "net/netip" ) @@ -27,19 +27,19 @@ func (e *EstPreTransmit) OnTick() PeerState { if len(endpoints) > 0 { e.tm.SendMsgToRelay( pi.HomeRelay, e.peer, pi.Session, - &msg2.Rendezvous{MyAddresses: endpoints}, + &msgsess.Rendezvous{MyAddresses: endpoints}, ) } return LogTransition(e, &EstTransmitting{EstablishingCommon: e.EstablishingCommon}) } -func (e *EstPreTransmit) OnDirect(ap netip.AddrPort, clear *msg2.ClearMessage) PeerState { +func (e *EstPreTransmit) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) PeerState { // OnTick will transition into the next state regardless, so just pass it along - return cascadeDirect(e, ap, clear) + return cascadeDirect(e, ap, clearMsg) } -func (e *EstPreTransmit) OnRelay(relay int64, peer key.NodePublic, clear *msg2.ClearMessage) PeerState { +func (e *EstPreTransmit) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) PeerState { // OnTick will transition into the next state regardless, so just pass it along - return cascadeRelay(e, relay, peer, clear) + return cascadeRelay(e, relay, peer, clearMsg) } diff --git a/toversok/actors/peer_state/e_t_rendez.go b/toversok/actors/peerstate/e_t_rendez.go similarity index 76% rename from toversok/actors/peer_state/e_t_rendez.go rename to toversok/actors/peerstate/e_t_rendez.go index d7246b8..1dbd65a 100644 --- a/toversok/actors/peer_state/e_t_rendez.go +++ b/toversok/actors/peerstate/e_t_rendez.go @@ -1,9 +1,9 @@ -package peer_state +package peerstate import ( "github.com/edup2p/common/types" "github.com/edup2p/common/types/key" - msg2 "github.com/edup2p/common/types/msgsess" + "github.com/edup2p/common/types/msgsess" "net/netip" "time" ) @@ -12,7 +12,7 @@ import ( type EstRendezGot struct { *EstablishingCommon - m *msg2.Rendezvous + m *msgsess.Rendezvous } func (e *EstRendezGot) Name() string { @@ -39,12 +39,12 @@ func (e *EstRendezGot) OnTick() PeerState { return LogTransition(e, &EstRendezAck{EstablishingCommon: e.EstablishingCommon}) } -func (e *EstRendezGot) OnDirect(ap netip.AddrPort, clear *msg2.ClearMessage) PeerState { +func (e *EstRendezGot) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) PeerState { // OnTick will transition into the next state regardless, so just pass it along - return cascadeDirect(e, ap, clear) + return cascadeDirect(e, ap, clearMsg) } -func (e *EstRendezGot) OnRelay(relay int64, peer key.NodePublic, clear *msg2.ClearMessage) PeerState { +func (e *EstRendezGot) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) PeerState { // OnTick will transition into the next state regardless, so just pass it along - return cascadeRelay(e, relay, peer, clear) + return cascadeRelay(e, relay, peer, clearMsg) } diff --git a/toversok/actors/peer_state/e_transmitting.go b/toversok/actors/peerstate/e_transmitting.go similarity index 70% rename from toversok/actors/peer_state/e_transmitting.go rename to toversok/actors/peerstate/e_transmitting.go index 8df50e6..72dd25a 100644 --- a/toversok/actors/peer_state/e_transmitting.go +++ b/toversok/actors/peerstate/e_transmitting.go @@ -1,8 +1,8 @@ -package peer_state +package peerstate import ( "github.com/edup2p/common/types/key" - msg2 "github.com/edup2p/common/types/msgsess" + "github.com/edup2p/common/types/msgsess" "net/netip" ) @@ -27,16 +27,16 @@ func (e *EstTransmitting) OnTick() PeerState { return nil } -func (e *EstTransmitting) OnDirect(ap netip.AddrPort, clear *msg2.ClearMessage) PeerState { - if s := cascadeDirect(e, ap, clear); s != nil { +func (e *EstTransmitting) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) PeerState { + if s := cascadeDirect(e, ap, clearMsg); s != nil { return s } - LogDirectMessage(e, ap, clear) + LogDirectMessage(e, ap, clearMsg) - switch m := clear.Message.(type) { - case *msg2.Ping: - if !e.pingDirectValid(ap, clear.Session, m) { + switch m := clearMsg.Message.(type) { + case *msgsess.Ping: + if !e.pingDirectValid(ap, clearMsg.Session, m) { L(e).Warn("dropping invalid ping", "ap", ap.String()) return nil } @@ -45,33 +45,32 @@ func (e *EstTransmitting) OnDirect(ap netip.AddrPort, clear *msg2.ClearMessage) return LogTransition(e, &EstHalfIng{ EstablishingCommon: e.EstablishingCommon, ap: ap, - sess: clear.Session, + sess: clearMsg.Session, ping: m, }) - case *msg2.Pong: + case *msgsess.Pong: e.tm.Poke() return LogTransition(e, &Finalizing{ EstablishingCommon: e.EstablishingCommon, ap: ap, - sess: clear.Session, + sess: clearMsg.Session, pong: m, }) - //case *msg.Rendezvous: default: L(e).Warn("ignoring direct session message", "ap", ap, - "session", clear.Session, + "session", clearMsg.Session, "msg", m.Debug()) return nil } } -func (e *EstTransmitting) OnRelay(relay int64, peer key.NodePublic, clear *msg2.ClearMessage) PeerState { - if s := cascadeRelay(e, relay, peer, clear); s != nil { +func (e *EstTransmitting) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) PeerState { + if s := cascadeRelay(e, relay, peer, clearMsg); s != nil { return s } - LogRelayMessage(e, relay, peer, clear) + LogRelayMessage(e, relay, peer, clearMsg) // NOTE: There an edgecase that can happen here: // @@ -93,21 +92,21 @@ func (e *EstTransmitting) OnRelay(relay int64, peer key.NodePublic, clear *msg2. // // This is harmless, as the state diagram permits for it, but its worth noting. - switch m := clear.Message.(type) { - case *msg2.Ping: - e.replyWithPongRelay(relay, peer, clear.Session, m) + switch m := clearMsg.Message.(type) { + case *msgsess.Ping: + e.replyWithPongRelay(relay, peer, clearMsg.Session, m) return nil - case *msg2.Pong: - e.ackPongRelay(relay, peer, clear.Session, m) + case *msgsess.Pong: + e.ackPongRelay(relay, peer, clearMsg.Session, m) return nil - case *msg2.Rendezvous: + case *msgsess.Rendezvous: e.tm.Poke() return LogTransition(e, &EstRendezGot{EstablishingCommon: e.EstablishingCommon, m: m}) default: L(e).Warn("ignoring relay session message", "relay", relay, "peer", peer, - "session", clear.Session, + "session", clearMsg.Session, "msg", m.Debug()) return nil } diff --git a/toversok/actors/peer_state/iface.go b/toversok/actors/peerstate/iface.go similarity index 79% rename from toversok/actors/peer_state/iface.go rename to toversok/actors/peerstate/iface.go index ab2b87c..582a8ec 100644 --- a/toversok/actors/peer_state/iface.go +++ b/toversok/actors/peerstate/iface.go @@ -1,4 +1,4 @@ -package peer_state +package peerstate import ( "github.com/edup2p/common/types/key" @@ -14,8 +14,8 @@ import ( // If it's non-nil, replace the state for the peer with the state returned. type PeerState interface { OnTick() PeerState - OnDirect(ap netip.AddrPort, clear *msgsess.ClearMessage) PeerState - OnRelay(relay int64, peer key.NodePublic, clear *msgsess.ClearMessage) PeerState + OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) PeerState + OnRelay(relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) PeerState // Name returns a lower-case name to be used in logging. Name() string diff --git a/toversok/actors/peer_state/peer_state.go b/toversok/actors/peerstate/peer_state.go similarity index 96% rename from toversok/actors/peer_state/peer_state.go rename to toversok/actors/peerstate/peer_state.go index 63ca4ab..00f3bed 100644 --- a/toversok/actors/peer_state/peer_state.go +++ b/toversok/actors/peerstate/peer_state.go @@ -5,4 +5,4 @@ // pongs haven't been received for 5 seconds (with pings at 2 second intervals). // // See [peer_state.mermaid] for a primary reference of this state machine. -package peer_state +package peerstate diff --git a/toversok/actors/peer_state/peer_state.mermaid b/toversok/actors/peerstate/peer_state.mermaid similarity index 100% rename from toversok/actors/peer_state/peer_state.mermaid rename to toversok/actors/peerstate/peer_state.mermaid diff --git a/toversok/actors/peer_state/s_established.go b/toversok/actors/peerstate/s_established.go similarity index 75% rename from toversok/actors/peer_state/s_established.go rename to toversok/actors/peerstate/s_established.go index 33e526e..6252acf 100644 --- a/toversok/actors/peer_state/s_established.go +++ b/toversok/actors/peerstate/s_established.go @@ -1,10 +1,10 @@ -package peer_state +package peerstate import ( "context" "github.com/edup2p/common/types" "github.com/edup2p/common/types/key" - msg2 "github.com/edup2p/common/types/msgsess" + "github.com/edup2p/common/types/msgsess" "net/netip" "slices" "time" @@ -51,13 +51,11 @@ func (e *Established) OnTick() PeerState { if !e.inactive { e.inactive = true e.inactiveSince = time.Now() - } else { - if time.Now().After(e.inactiveSince.Add(ConnectionInactivityTimeout)) { - return LogTransition(e, &Teardown{ - StateCommon: e.StateCommon, - inactive: true, - }) - } + } else if time.Now().After(e.inactiveSince.Add(ConnectionInactivityTimeout)) { + return LogTransition(e, &Teardown{ + StateCommon: e.StateCommon, + inactive: true, + }) } } @@ -79,12 +77,12 @@ func (e *Established) OnTick() PeerState { return nil } -func (e *Established) OnDirect(ap netip.AddrPort, clear *msg2.ClearMessage) PeerState { - if s := cascadeDirect(e, ap, clear); s != nil { +func (e *Established) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) PeerState { + if s := cascadeDirect(e, ap, clearMsg); s != nil { return s } - LogDirectMessage(e, ap, clear) + LogDirectMessage(e, ap, clearMsg) // TODO check if endpoint is same as current used one // - switch? trusting it blindly is open to replay attacks @@ -95,55 +93,54 @@ func (e *Established) OnDirect(ap netip.AddrPort, clear *msg2.ClearMessage) Peer return nil } - switch m := clear.Message.(type) { - case *msg2.Ping: - if !e.pingDirectValid(ap, clear.Session, m) { + switch m := clearMsg.Message.(type) { + case *msgsess.Ping: + if !e.pingDirectValid(ap, clearMsg.Session, m) { L(e).Warn("dropping invalid ping", "ap", ap.String()) return nil } e.lastPingRecv = time.Now() - e.replyWithPongDirect(ap, clear.Session, m) + e.replyWithPongDirect(ap, clearMsg.Session, m) return nil - case *msg2.Pong: + case *msgsess.Pong: e.lastPongRecv = time.Now() - e.ackPongDirect(ap, clear.Session, m) + e.ackPongDirect(ap, clearMsg.Session, m) return nil - //case *msg.Rendezvous: default: L(e).Debug("ignoring direct session message", "ap", ap, - "session", clear.Session, + "session", clearMsg.Session, "msg", m.Debug()) return nil } } -func (e *Established) OnRelay(relay int64, peer key.NodePublic, clear *msg2.ClearMessage) PeerState { - if s := cascadeRelay(e, relay, peer, clear); s != nil { +func (e *Established) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) PeerState { + if s := cascadeRelay(e, relay, peer, clearMsg); s != nil { return s } - LogRelayMessage(e, relay, peer, clear) + LogRelayMessage(e, relay, peer, clearMsg) - switch m := clear.Message.(type) { - case *msg2.Ping: - e.replyWithPongRelay(relay, peer, clear.Session, m) + switch m := clearMsg.Message.(type) { + case *msgsess.Ping: + e.replyWithPongRelay(relay, peer, clearMsg.Session, m) return nil - case *msg2.Pong: - e.ackPongRelay(relay, peer, clear.Session, m) + case *msgsess.Pong: + e.ackPongRelay(relay, peer, clearMsg.Session, m) return nil - //case *msg.Rendezvous: - // TODO maybe re-establishment logic? + // case *msg.Rendezvous: + // TODO maybe re-establishment logic? default: L(e).Debug("ignoring relay session message", "relay", relay, "peer", peer, - "session", clear.Session, + "session", clearMsg.Session, "msg", m.Debug()) return nil } diff --git a/toversok/actors/peer_state/s_inactive.go b/toversok/actors/peerstate/s_inactive.go similarity index 56% rename from toversok/actors/peer_state/s_inactive.go rename to toversok/actors/peerstate/s_inactive.go index 38402ad..44679ec 100644 --- a/toversok/actors/peer_state/s_inactive.go +++ b/toversok/actors/peerstate/s_inactive.go @@ -1,8 +1,8 @@ -package peer_state +package peerstate import ( "github.com/edup2p/common/types/key" - msg2 "github.com/edup2p/common/types/msgsess" + "github.com/edup2p/common/types/msgsess" "net/netip" ) @@ -25,49 +25,49 @@ func (i *Inactive) OnTick() PeerState { return nil } -func (i *Inactive) OnDirect(ap netip.AddrPort, clear *msg2.ClearMessage) PeerState { - if s := cascadeDirect(i, ap, clear); s != nil { +func (i *Inactive) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) PeerState { + if s := cascadeDirect(i, ap, clearMsg); s != nil { return s } - LogDirectMessage(i, ap, clear) + LogDirectMessage(i, ap, clearMsg) - switch m := clear.Message.(type) { - case *msg2.Ping: - if !i.pingDirectValid(ap, clear.Session, m) { + switch m := clearMsg.Message.(type) { + case *msgsess.Ping: + if !i.pingDirectValid(ap, clearMsg.Session, m) { L(i).Warn("dropping invalid ping", "ap", ap.String()) return nil } - i.replyWithPongDirect(ap, clear.Session, m) + i.replyWithPongDirect(ap, clearMsg.Session, m) return nil - case *msg2.Pong: - i.ackPongDirect(ap, clear.Session, m) + case *msgsess.Pong: + i.ackPongDirect(ap, clearMsg.Session, m) return nil default: L(i).Warn("ignoring direct session message", "ap", ap, - "session", clear.Session, + "session", clearMsg.Session, "msg", m.Debug()) return nil } } -func (i *Inactive) OnRelay(relay int64, peer key.NodePublic, clear *msg2.ClearMessage) PeerState { - if s := cascadeRelay(i, relay, peer, clear); s != nil { +func (i *Inactive) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) PeerState { + if s := cascadeRelay(i, relay, peer, clearMsg); s != nil { return s } - LogRelayMessage(i, relay, peer, clear) + LogRelayMessage(i, relay, peer, clearMsg) - switch m := clear.Message.(type) { - case *msg2.Ping: - i.replyWithPongRelay(relay, peer, clear.Session, m) + switch m := clearMsg.Message.(type) { + case *msgsess.Ping: + i.replyWithPongRelay(relay, peer, clearMsg.Session, m) return nil - case *msg2.Pong: - i.ackPongRelay(relay, peer, clear.Session, m) + case *msgsess.Pong: + i.ackPongRelay(relay, peer, clearMsg.Session, m) return nil - case *msg2.Rendezvous: + case *msgsess.Rendezvous: i.tm.Poke() return LogTransition(i, &EstRendezGot{ EstablishingCommon: mkEstComm(i.StateCommon, 0), @@ -77,7 +77,7 @@ func (i *Inactive) OnRelay(relay int64, peer key.NodePublic, clear *msg2.ClearMe L(i).Warn("ignoring relay session message", "relay", relay, "peer", peer, - "session", clear.Session, + "session", clearMsg.Session, "msg", m.Debug()) return nil } diff --git a/toversok/actors/peer_state/s_t_booting.go b/toversok/actors/peerstate/s_t_booting.go similarity index 81% rename from toversok/actors/peer_state/s_t_booting.go rename to toversok/actors/peerstate/s_t_booting.go index 13beb84..6465bbd 100644 --- a/toversok/actors/peer_state/s_t_booting.go +++ b/toversok/actors/peerstate/s_t_booting.go @@ -1,4 +1,4 @@ -package peer_state +package peerstate import ( "github.com/edup2p/common/types" @@ -35,12 +35,12 @@ func (b *Booting) OnTick() PeerState { }) } -func (b *Booting) OnDirect(ap netip.AddrPort, clear *msgsess.ClearMessage) PeerState { +func (b *Booting) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) PeerState { // OnTick will transition into the next state regardless, so just pass it along - return cascadeDirect(b, ap, clear) + return cascadeDirect(b, ap, clearMsg) } -func (b *Booting) OnRelay(relay int64, peer key.NodePublic, clear *msgsess.ClearMessage) PeerState { +func (b *Booting) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) PeerState { // OnTick will transition into the next state regardless, so just pass it along - return cascadeRelay(b, relay, peer, clear) + return cascadeRelay(b, relay, peer, clearMsg) } diff --git a/toversok/actors/peer_state/s_t_teardown.go b/toversok/actors/peerstate/s_t_teardown.go similarity index 66% rename from toversok/actors/peer_state/s_t_teardown.go rename to toversok/actors/peerstate/s_t_teardown.go index 8f7d11f..a3a72b7 100644 --- a/toversok/actors/peer_state/s_t_teardown.go +++ b/toversok/actors/peerstate/s_t_teardown.go @@ -1,4 +1,4 @@ -package peer_state +package peerstate import ( "github.com/edup2p/common/types/key" @@ -28,22 +28,22 @@ func (t *Teardown) OnTick() PeerState { return LogTransition(t, &Inactive{ StateCommon: t.StateCommon, }) - } else { - L(t).Info("LOST direct peer connection", "peer", t.peer.Debug()) - - return LogTransition(t, &Trying{ - StateCommon: t.StateCommon, - tryAt: time.Now(), - }) } + + L(t).Info("LOST direct peer connection", "peer", t.peer.Debug()) + + return LogTransition(t, &Trying{ + StateCommon: t.StateCommon, + tryAt: time.Now(), + }) } -func (t *Teardown) OnDirect(ap netip.AddrPort, clear *msgsess.ClearMessage) PeerState { +func (t *Teardown) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) PeerState { // OnTick will transition into the next state regardless, so just pass it along - return cascadeDirect(t, ap, clear) + return cascadeDirect(t, ap, clearMsg) } -func (t *Teardown) OnRelay(relay int64, peer key.NodePublic, clear *msgsess.ClearMessage) PeerState { +func (t *Teardown) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) PeerState { // OnTick will transition into the next state regardless, so just pass it along - return cascadeRelay(t, relay, peer, clear) + return cascadeRelay(t, relay, peer, clearMsg) } diff --git a/toversok/actors/peer_state/s_trying.go b/toversok/actors/peerstate/s_trying.go similarity index 54% rename from toversok/actors/peer_state/s_trying.go rename to toversok/actors/peerstate/s_trying.go index 60bbd73..eb11bb4 100644 --- a/toversok/actors/peer_state/s_trying.go +++ b/toversok/actors/peerstate/s_trying.go @@ -1,8 +1,8 @@ -package peer_state +package peerstate import ( "github.com/edup2p/common/types/key" - msg2 "github.com/edup2p/common/types/msgsess" + "github.com/edup2p/common/types/msgsess" "net/netip" "time" ) @@ -28,51 +28,50 @@ func (t *Trying) OnTick() PeerState { return nil } -func (t *Trying) OnDirect(ap netip.AddrPort, clear *msg2.ClearMessage) PeerState { - if s := cascadeDirect(t, ap, clear); s != nil { +func (t *Trying) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) PeerState { + if s := cascadeDirect(t, ap, clearMsg); s != nil { return s } - LogDirectMessage(t, ap, clear) + LogDirectMessage(t, ap, clearMsg) - switch m := clear.Message.(type) { - case *msg2.Ping: - if !t.pingDirectValid(ap, clear.Session, m) { + switch m := clearMsg.Message.(type) { + case *msgsess.Ping: + if !t.pingDirectValid(ap, clearMsg.Session, m) { L(t).Warn("dropping invalid ping", "ap", ap.String()) return nil } // TODO(jo): We could start establishing here, possibly. - t.replyWithPongDirect(ap, clear.Session, m) + t.replyWithPongDirect(ap, clearMsg.Session, m) return nil - case *msg2.Pong: - t.ackPongDirect(ap, clear.Session, m) + case *msgsess.Pong: + t.ackPongDirect(ap, clearMsg.Session, m) return nil - //case *msg.Rendezvous: default: L(t).Warn("ignoring direct session message", "ap", ap, - "session", clear.Session, + "session", clearMsg.Session, "msg", m.Debug()) return nil } } -func (t *Trying) OnRelay(relay int64, peer key.NodePublic, clear *msg2.ClearMessage) PeerState { - if s := cascadeRelay(t, relay, peer, clear); s != nil { +func (t *Trying) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) PeerState { + if s := cascadeRelay(t, relay, peer, clearMsg); s != nil { return s } - LogRelayMessage(t, relay, peer, clear) + LogRelayMessage(t, relay, peer, clearMsg) - switch m := clear.Message.(type) { - case *msg2.Ping: - t.replyWithPongRelay(relay, peer, clear.Session, m) + switch m := clearMsg.Message.(type) { + case *msgsess.Ping: + t.replyWithPongRelay(relay, peer, clearMsg.Session, m) return nil - case *msg2.Pong: - t.ackPongRelay(relay, peer, clear.Session, m) + case *msgsess.Pong: + t.ackPongRelay(relay, peer, clearMsg.Session, m) return nil - case *msg2.Rendezvous: + case *msgsess.Rendezvous: return LogTransition(t, &EstRendezGot{ EstablishingCommon: mkEstComm(t.StateCommon, 0), m: m, @@ -81,7 +80,7 @@ func (t *Trying) OnRelay(relay int64, peer key.NodePublic, clear *msg2.ClearMess L(t).Warn("ignoring relay session message", "relay", relay, "peer", peer, - "session", clear.Session, + "session", clearMsg.Session, "msg", m.Debug()) return nil } diff --git a/toversok/actors/peer_state/s_waiting.go b/toversok/actors/peerstate/s_waiting.go similarity index 73% rename from toversok/actors/peer_state/s_waiting.go rename to toversok/actors/peerstate/s_waiting.go index 0556d9b..b20b124 100644 --- a/toversok/actors/peer_state/s_waiting.go +++ b/toversok/actors/peerstate/s_waiting.go @@ -1,4 +1,4 @@ -package peer_state +package peerstate import ( "github.com/edup2p/common/types/ifaces" @@ -23,23 +23,23 @@ func (w *WaitingForInfo) OnTick() PeerState { return nil } -func (w *WaitingForInfo) OnDirect(ap netip.AddrPort, clear *msgsess.ClearMessage) PeerState { - s := cascadeDirect(w, ap, clear) +func (w *WaitingForInfo) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) PeerState { + s := cascadeDirect(w, ap, clearMsg) if s == nil { // The state did not cascade, so we log here. - LogDirectMessage(w, ap, clear) + LogDirectMessage(w, ap, clearMsg) } return s } -func (w *WaitingForInfo) OnRelay(relay int64, peer key.NodePublic, clear *msgsess.ClearMessage) PeerState { - s := cascadeRelay(w, relay, peer, clear) +func (w *WaitingForInfo) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) PeerState { + s := cascadeRelay(w, relay, peer, clearMsg) if s == nil { // The state did not cascade, so we log here. - LogRelayMessage(w, relay, peer, clear) + LogRelayMessage(w, relay, peer, clearMsg) } return s diff --git a/toversok/actors/peer_state/util.go b/toversok/actors/peerstate/util.go similarity index 74% rename from toversok/actors/peer_state/util.go rename to toversok/actors/peerstate/util.go index 436aaa2..7b8259d 100644 --- a/toversok/actors/peer_state/util.go +++ b/toversok/actors/peerstate/util.go @@ -1,4 +1,4 @@ -package peer_state +package peerstate import ( "context" @@ -12,9 +12,9 @@ import ( // cascadeDirect makes it so that first we call the default "tick" function of a peer's state, // and if that requests a state transition, call a PeerState.OnDirect with the original arguments, // and return the requested state change with that one if it returns one. -func cascadeDirect(so PeerState, ap netip.AddrPort, clear *msgsess.ClearMessage) (s PeerState) { +func cascadeDirect(so PeerState, ap netip.AddrPort, clearMsg *msgsess.ClearMessage) (s PeerState) { if s1 := so.OnTick(); s1 != nil { - if s2 := s1.OnDirect(ap, clear); s2 != nil { + if s2 := s1.OnDirect(ap, clearMsg); s2 != nil { s = s2 } else { s = s1 @@ -27,9 +27,9 @@ func cascadeDirect(so PeerState, ap netip.AddrPort, clear *msgsess.ClearMessage) // cascadeRelay makes it so that first we call the default "tick" function of a peer's state, // and if that requests a state transition, call a PeerState.OnRelay with the original arguments, // and return the requested state change with that one if it returns one. -func cascadeRelay(so PeerState, relay int64, peer key.NodePublic, clear *msgsess.ClearMessage) (s PeerState) { +func cascadeRelay(so PeerState, relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) (s PeerState) { if s1 := so.OnTick(); s1 != nil { - if s2 := s1.OnRelay(relay, peer, clear); s2 != nil { + if s2 := s1.OnRelay(relay, peer, clearMsg); s2 != nil { s = s2 } else { s = s1 @@ -50,21 +50,21 @@ func LogTransition(from PeerState, to PeerState) PeerState { return to } -func LogDirectMessage(s PeerState, ap netip.AddrPort, clear *msgsess.ClearMessage) { +func LogDirectMessage(s PeerState, ap netip.AddrPort, clearMsg *msgsess.ClearMessage) { L(s).Log(context.Background(), types.LevelTrace, "received direct message", slog.Group("from", "addrport", ap, - "session", clear.Session.Debug()), - "msg", clear.Message.Debug(), + "session", clearMsg.Session.Debug()), + "msg", clearMsg.Message.Debug(), ) } -func LogRelayMessage(s PeerState, relay int64, peer key.NodePublic, clear *msgsess.ClearMessage) { +func LogRelayMessage(s PeerState, relay int64, peer key.NodePublic, clearMsg *msgsess.ClearMessage) { L(s).Log(context.Background(), types.LevelTrace, "received relay message", slog.Group("from", "relay", relay, "peer", peer.Debug(), - "session", clear.Session), - "msg", clear.Message.Debug(), + "session", clearMsg.Session), + "msg", clearMsg.Message.Debug(), ) } diff --git a/toversok/actors/rehearsal_test.go b/toversok/actors/rehearsal_test.go index 11dc439..5828849 100644 --- a/toversok/actors/rehearsal_test.go +++ b/toversok/actors/rehearsal_test.go @@ -10,6 +10,7 @@ import ( "github.com/edup2p/common/types/msgactor" ) +//nolint:unused type MockActor struct { ctx context.Context diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index 1169cfd..8cc8080 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -34,11 +34,6 @@ type InConnActor interface { ForwardPacket(pkt []byte) } -//udp, err := net.ListenUDP("udp", net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), localPort))) -//if err != nil { -// panic(fmt.Sprintf("could not create listenUDP: %s", err)) -//} - func MakeStage( pCtx context.Context, @@ -124,10 +119,11 @@ type Stage struct { //// A repeatable function to an outside context to acquire a new UDPconn, //// once a peer conn has died for whatever reason. - //reviveOutConn func(peer key.NodePublic) *net.UDPConn + // TODO rework this? + // reviveOutConn func(peer key.NodePublic) *net.UDPConn // - //makeOutConn func(udp UDPConn, peer key.NodePublic, s *Stage) OutConnActor - //makeInConn func(udp UDPConn, peer key.NodePublic, s *Stage) InConnActor + // makeOutConn func(udp UDPConn, peer key.NodePublic, s *Stage) OutConnActor + // makeInConn func(udp UDPConn, peer key.NodePublic, s *Stage) InConnActor ext types.UDPConn bindLocal func(peer key.NodePublic) types.UDPConn @@ -222,7 +218,7 @@ func (s *Stage) reapableConnsLocked() []key.NodePublic { if !ok { // outconn is gone for some reason, this is fine for now - // TODO log this? + slog.Warn("missing outconn pair to inconn, this is fine, but odd", "peer", peer.Debug()) } else { out.Cancel() } @@ -343,14 +339,6 @@ func (s *Stage) InConnFor(peer key.NodePublic) InConnActor { return s.inConn[peer] } -//// AddConn creates an InConn and OutConn for a specified connection. -//// Starting each Actor'S goroutines as well. It also starts a SockRecv given the -//// udp connection. -//func (s *Stage) AddConn(udp *net.UDPConn, peer key.NodePublic, info *PeerInfo) { -// s.UpdateSessionKey(peer, session) -// s.addConn(udp, peer, homeRelay) -//} - // addConnLocked assumes Stage.connMutex and Stage.peerInfoMutex is held by caller. func (s *Stage) addConnLocked(peer key.NodePublic, udp types.UDPConn) { pi := s.peerInfo[peer] @@ -461,7 +449,7 @@ func (s *Stage) notifyEndpointChanged() { } } -func (s *Stage) AddPeer(peer key.NodePublic, homeRelay int64, endpoints []netip.AddrPort, session key.SessionPublic, _ netip.Addr, _ netip.Addr, prop msgcontrol.Properties) error { +func (s *Stage) AddPeer(peer key.NodePublic, homeRelay int64, endpoints []netip.AddrPort, session key.SessionPublic, _ netip.Addr, _ netip.Addr, _ msgcontrol.Properties) error { s.peerInfoMutex.Lock() defer func() { @@ -487,7 +475,7 @@ func (s *Stage) AddPeer(peer key.NodePublic, homeRelay int64, endpoints []netip. var errNoPeerInfo = errors.New("could not find peer info to update") -func (s *Stage) UpdatePeer(peer key.NodePublic, homeRelay *int64, endpoints []netip.AddrPort, session *key.SessionPublic, prop *msgcontrol.Properties) error { +func (s *Stage) UpdatePeer(peer key.NodePublic, homeRelay *int64, endpoints []netip.AddrPort, session *key.SessionPublic, _ *msgcontrol.Properties) error { return s.updatePeerInfo(peer, func(info *stage.PeerInfo) { if homeRelay != nil { info.HomeRelay = *homeRelay @@ -570,41 +558,3 @@ func (s *Stage) ControlSTUN() []netip.AddrPort { // TODO return []netip.AddrPort{} } - -//func (s *Stage) RemoveConn(peer key.NodePublic) { -// s.connMutex.Lock() -// defer s.connMutex.Unlock() -// -// in, inok := s.inConn[peer] -// out, outok := s.inConn[peer] -// -// if !inok && !outok { -// // both already removed, we're done here -// return -// } -// -// if inok != outok { -// // only one of them removed? -// // we could recover this, but this is a bug, panic. -// panic(fmt.Sprintf("InConn or OutConn presence on stage was disbalanced: in=%t, out=%t", inok, outok)) -// } -// -// // Now we know both exist -// -// delete(s.inConn, peer) -// delete(s.outConn, peer) -// -// in.Cancel() -// out.Cancel() -// -// // OutConn cancel: -// // this closes the outch in SockRecv, -// // sends "outconn goodbye" to traffic manager, -// // -// // InConn cancel: -// // sends "outconn goodbye" to traffic manager. -// -// // When TM has received both goodbyes: -// // removes from internal activity tracking, -// // and removes mapping from direct router. -//} diff --git a/toversok/actors/util.go b/toversok/actors/util.go index 4da567d..a42dff4 100644 --- a/toversok/actors/util.go +++ b/toversok/actors/util.go @@ -3,6 +3,7 @@ package actors import ( "context" "fmt" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/ifaces" "github.com/edup2p/common/types/msgactor" "log/slog" @@ -37,7 +38,7 @@ func L(a ifaces.Actor) *slog.Logger { } func bail(c context.Context, v any) { - maybeCcc := c.Value("ccc") + maybeCcc := c.Value(types.CCC) if maybeCcc == nil { panic(fmt.Errorf("could not bail, cannot find ccc: %s", v)) } diff --git a/toversok/actors/util_test.go b/toversok/actors/util_test.go index 67de85a..b2c6a11 100644 --- a/toversok/actors/util_test.go +++ b/toversok/actors/util_test.go @@ -8,17 +8,17 @@ import ( ) // Test constants -const assertEventuallyTick time.Duration = 1 * time.Millisecond -const assertEventuallyTimeout time.Duration = 10 * assertEventuallyTick +const assertEventuallyTick = 1 * time.Millisecond +const assertEventuallyTimeout = 10 * assertEventuallyTick // Test variables -var dummyAddr netip.Addr = netip.IPv4Unspecified() -var dummyAddrPort netip.AddrPort = netip.AddrPortFrom(dummyAddr, 0) +var dummyAddr = netip.IPv4Unspecified() +var dummyAddrPort = netip.AddrPortFrom(dummyAddr, 0) var dummyKey key.NodePublic = [32]byte{0} // Test session -var testPriv key.SessionPrivate = key.NewSession() -var testPub key.SessionPublic = testPriv.Public() +var testPriv = key.NewSession() +var testPub = testPriv.Public() func getTestPriv() *key.SessionPrivate { return &testPriv diff --git a/toversok/control_conn.go b/toversok/control_conn.go index 87cfb30..eb40566 100644 --- a/toversok/control_conn.go +++ b/toversok/control_conn.go @@ -77,6 +77,7 @@ func CreateControlSession(ctx context.Context, opts dial.Opts, controlKey key.Co clientCtx := context.WithoutCancel(rcsCtx) c, err := controlhttp.Dial(clientCtx, opts, getPriv, getSess, controlKey, nil, logon) if err != nil { + rcsCcc(err) return nil, fmt.Errorf("could not create control session: %w", err) } @@ -211,7 +212,7 @@ func (rcs *ResumableControlSession) Run() { return } - if errors.Is(err, control.NeedsLogonError) { + if errors.Is(err, control.ErrNeedsLogon) { // TODO dead/retry logic, signal that session is dead and needs manual logon panic("not implemented") } @@ -347,7 +348,7 @@ func (rcs *ResumableControlSession) send(msg msgcontrol.ControlMessage) error { return nil } - if !errors.Is(err, control.ClosedErr) { + if !errors.Is(err, control.ErrClosed) { return err } } diff --git a/toversok/engine.go b/toversok/engine.go index 8f7c5be..d0a78ca 100644 --- a/toversok/engine.go +++ b/toversok/engine.go @@ -14,29 +14,6 @@ import ( "time" ) -//type EngineOptions struct { -// //Ctx context.Context -// //Ccc context.CancelCauseFunc -// // -// //PrivKey key.NakedKey -// // -// //Control dial.Opts -// //ControlKey key.ControlPublic -// // -// //// Do not contact control -// //OverrideControl bool -// //OverrideIPv4 netip.Prefix -// //OverrideIPv6 netip.Prefix -// -// WG WireGuardHost -// FW FirewallHost -// Co ControlHost -// -// ExtBindPort uint16 -// -// PrivateKey key.NodePrivate -//} - // Engine is the main and most high-level object for any client implementation. // // It holds the WireGuardHost, FirewallHost, and ControlHost, and utilises these for connectivity @@ -169,7 +146,7 @@ func (e *Engine) installSession(allowLogon bool) error { logon = func(url string, _ chan<- string) error { // TODO register/use device key channel - e.state.currentLoginUrl = url + e.state.currentLoginURL = url e.state.change(CreatingSession, NeedsLogin) return nil } @@ -212,7 +189,7 @@ func newStateObserver() stateObserver { type stateObserver struct { mu sync.Mutex state EngineState - currentLoginUrl string + currentLoginURL string callbacks []func(state EngineState) } @@ -227,21 +204,21 @@ func (s *stateObserver) RegisterStateChangeListener(f func(state EngineState)) { s.callbacks = append(s.callbacks, f) } -var WrongStateErr = errors.New("wrong state") +var ErrWrongState = errors.New("wrong state") func (s *stateObserver) GetNeedsLoginState() (url string, err error) { s.mu.Lock() defer s.mu.Unlock() - if s.state == NeedsLogin { - return s.currentLoginUrl, nil - } else { - return "", WrongStateErr + if s.state != NeedsLogin { + return "", ErrWrongState } + + return s.currentLoginURL, nil } func (s *stateObserver) GetEstablishedState() { - //TODO implement me + // TODO implement me panic("implement me") } @@ -298,18 +275,19 @@ func NewEngine( parentCtx = context.Background() } - ctx, ccc := context.WithCancelCause(parentCtx) - - if wg == nil { + switch { + case wg == nil: return nil, errors.New("cannot initialise toversok engine with nil WireGuardHost") - } else if fw == nil { + case fw == nil: return nil, errors.New("cannot initialise toversok engine with nil FirewallHost") - } else if co == nil { + case co == nil: return nil, errors.New("cannot initialise toversok engine with nil ControlHost") - } else if privateKey.IsZero() { + case privateKey.IsZero(): return nil, errors.New("cannot initialise toversok engine with zero privateKey") } + ctx, ccc := context.WithCancelCause(parentCtx) + e := &Engine{ ctx: ctx, ccc: ccc, @@ -373,94 +351,11 @@ func (e *Engine) Observer() Observer { return &e.state } -func (e *Engine) SupplyDeviceKey(key string) error { +func (e *Engine) SupplyDeviceKey(string) error { // TODO panic("not implemented") } -// -//const WGKeepAlive = time.Second * 20 -// -//func (e *Engine) Handle(ev Event) error { -// switch ev := ev.(type) { -// case PeerAddition: -// return e.AddPeer(ev.Key, ev.HomeRelayId, ev.Endpoints, ev.SessionKey, ev.VIPs.IPv4, ev.VIPs.IPv6) -// case PeerUpdate: -// // FIXME the reason for the panic below is because this function is essentially deprecated, and it still uses -// // gonull, which is a pain -// panic("cannot handle PeerUpdate via handle") -// -// //if ev.Endpoints.Present { -// // if err := e.stage.SetEndpoints(ev.Key, ev.Endpoints.Val); err != nil { -// // return fmt.Errorf("failed to update endpoints: %w", err) -// // } -// //} -// // -// //if ev.SessionKey.Present { -// // if err := e.stage.UpdateSessionKey(ev.Key, ev.SessionKey.Val); err != nil { -// // return fmt.Errorf("failed to update session key: %w", err) -// // } -// //} -// // -// //if ev.HomeRelayId.Present { -// // if err := e.stage.UpdateHomeRelay(ev.Key, ev.HomeRelayId.Val); err != nil { -// // return fmt.Errorf("failed to update home relay: %w", err) -// // } -// //} -// case PeerRemoval: -// return e.RemovePeer(ev.Key) -// case RelayUpdate: -// return e.UpdateRelays(ev.Set) -// default: -// // TODO warn-log about unknown type instead of panic -// panic("Unknown type!") -// } -// -// return nil -//} -// -//func (e *Engine) AddPeer(peer key.NodePublic, homeRelay int64, endpoints []netip.AddrPort, session key.SessionPublic, ip4 netip.Addr, ip6 netip.Addr) error { -// m := e.bindLocal() -// e.localMapping[peer] = m -// -// if err := e.wg.UpdatePeer(peer, PeerCfg{ -// Set: true, -// VIPs: &VirtualIPs{ -// IPv4: ip4, -// IPv6: ip6, -// }, -// KeepAliveInterval: nil, -// LocalEndpointPort: &m.port, -// }); err != nil { -// return fmt.Errorf("failed to update wireguard: %w", err) -// } -// -// if err := e.stage.AddPeer(peer, homeRelay, endpoints, session, ip4, ip6); err != nil { -// return fmt.Errorf("failed to update stage: %w", err) -// } -// return nil -//} -// -//func (e *Engine) UpdatePeer(peer key.NodePublic, homeRelay *int64, endpoints []netip.AddrPort, session *key.SessionPublic) error { -// return e.stage.UpdatePeer(peer, homeRelay, endpoints, session) -//} -// -//func (e *Engine) RemovePeer(peer key.NodePublic) error { -// if err := e.stage.RemovePeer(peer); err != nil { -// return err -// } -// -// if err := e.wg.RemovePeer(peer); err != nil { -// return fmt.Errorf("failed to remove peer from wireguard: %w", err) -// } -// -// return nil -//} -// -//func (e *Engine) UpdateRelays(relay []relay.Information) error { -// return e.stage.UpdateRelays(relay) -//} - type FakeControl struct { controlKey key.ControlPublic ipv4 netip.Prefix @@ -479,12 +374,16 @@ func (f *FakeControl) IPv6() netip.Prefix { return f.ipv6 } -func (f *FakeControl) InstallCallbacks(callbacks ifaces.ControlCallbacks) error { +func (f *FakeControl) InstallCallbacks(ifaces.ControlCallbacks) { + // NOP +} + +func (f *FakeControl) UpdateEndpoints([]netip.AddrPort) error { // NOP return nil } -func (f *FakeControl) UpdateEndpoints(ports []netip.AddrPort) error { +func (f *FakeControl) UpdateHomeRelay(int64) error { // NOP return nil } diff --git a/toversok/session.go b/toversok/session.go index 62ac918..e8a6412 100644 --- a/toversok/session.go +++ b/toversok/session.go @@ -26,8 +26,6 @@ type Session struct { quarantinedPeers map[key.NodePublic]bool peerAddrs map[key.NodePublic][]netip.Addr - //control ifaces.ControlSession - stage ifaces.Stage sessionKey key.SessionPrivate @@ -44,7 +42,7 @@ func SetupSession( ) (*Session, error) { ctx, ccc := context.WithCancelCause(engineCtx) - sCtx := context.WithValue(ctx, "ccc", ccc) + sCtx := context.WithValue(ctx, types.CCC, ccc) sess := &Session{ ctx: sCtx, diff --git a/types/control/client.go b/types/control/client.go index b05711f..55d3831 100644 --- a/types/control/client.go +++ b/types/control/client.go @@ -46,9 +46,9 @@ func EstablishClient(parentCtx context.Context, mc types.MetaConn, brw *bufio.Re if err := c.Handshake(timeout, logon); err != nil { return nil, err - } else { - return c, nil } + + return c, nil } func (c *Client) Handshake(timeout time.Duration, logon types.LogonCallback) error { @@ -85,10 +85,8 @@ func (c *Client) Handshake(timeout time.Duration, logon types.LogonCallback) err if c.ControlKey.IsZero() { c.ControlKey = serverHello.ControlNodePub // TODO log TOFU? - } else { - if serverHello.ControlNodePub != c.ControlKey { - return fmt.Errorf("client-stated control key does not match server-given control key") - } + } else if serverHello.ControlNodePub != c.ControlKey { + return fmt.Errorf("client-stated control key does not match server-given control key") } clearData, ok := c.getPriv().OpenFromControl(c.ControlKey, serverHello.CheckData) @@ -133,9 +131,6 @@ func (c *Client) Handshake(timeout time.Duration, logon types.LogonCallback) err case *msgcontrol.LogonAccept: c.SessionID = &m.SessionID - //c.IPv4 = netip.PrefixFrom(netip.Addr(m.IPv4Addr), int(m.IPv4Mask)) - //c.IPv6 = netip.PrefixFrom(netip.Addr(m.IPv6Addr), int(m.IPv6Mask)) - c.IPv4 = m.IP4 c.IPv6 = m.IP6 @@ -145,69 +140,14 @@ func (c *Client) Handshake(timeout time.Duration, logon types.LogonCallback) err default: return fmt.Errorf("received unknown message type after-logon: %d", m) } - - //switch typ { - //case msgcontrol.LogonAuthenticateType: - // // TODO - // panic("authenticate logic not implemented") - //case msgcontrol.LogonAcceptType: - // accept := new(msgcontrol.LogonAccept) - // if err := ReadMessage(c.reader, msgLen, accept); err != nil { - // return fmt.Errorf("error when reading after-logon reject message: %w", err) - // } - // - // c.SessionID = &accept.SessionID - // c.IPv4 = netip.PrefixFrom(netip.Addr(accept.IPv4Addr), int(accept.IPv4Mask)) - // c.IPv6 = netip.PrefixFrom(netip.Addr(accept.IPv6Addr), int(accept.IPv6Mask)) - // - // return nil - // - //case msgcontrol.LogonRejectType: - // reject := new(msgcontrol.LogonReject) - // if err := ReadMessage(c.reader, msgLen, reject); err != nil { - // return fmt.Errorf("error when reading after-logon reject message: %w", err) - // } - // - // return fmt.Errorf( - // "logon rejected after-logon: %s; retry strategy: %w", - // reject.Reason, - // types.PtrOr(reject.RetryStrategy, msgcontrol.NoRetryStrategy), - // ) - //default: - // return fmt.Errorf("received unknown message type after-logon: %d", typ) - //} - // - //typ, msgLen, err = ReadMessageHeader(c.reader) - //if err != nil { - // return fmt.Errorf("error when receiving after-authenticate message: %w", err) - //} - // - //switch typ { - //case msgcontrol.LogonAcceptType: - // // TODO - // panic("implement me") - //case msgcontrol.LogonRejectType: - // reject := new(msgcontrol.LogonReject) - // if err := ReadMessage(c.reader, msgLen, reject); err != nil { - // return fmt.Errorf("error when reading after-authenticate reject message: %w", err) - // } - // - // return fmt.Errorf( - // "logon rejected after-authenticate: %s; retry strategy: %w", - // reject.Reason, - // types.PtrOr(reject.RetryStrategy, msgcontrol.NoRetryStrategy), - // ) - //default: - // return fmt.Errorf("received unknown message type after-authenticate: %d", typ) - //} } -var NeedsLogonError = errors.New("needs logon callback") +var ErrNeedsLogon = errors.New("needs logon callback") func (c *Client) handleLogon(url string, logon types.LogonCallback) (msgcontrol.ControlMessage, error) { if logon == nil { // No way we can start or create a logon session, abort - return nil, fmt.Errorf("logonauthenticate requested when no interactive logon callback exists, aborting; %w", NeedsLogonError) + return nil, fmt.Errorf("logonauthenticate requested when no interactive logon callback exists, aborting; %w", ErrNeedsLogon) } deviceKeyChan := make(chan string) @@ -257,11 +197,11 @@ func (c *Client) handleLogon(url string, logon types.LogonCallback) (msgcontrol. } } -var ClosedErr = errors.New("client closed") +var ErrClosed = errors.New("client closed") func (c *Client) Send(msg msgcontrol.ControlMessage) error { if types.IsContextDone(c.ctx) { - return ClosedErr + return ErrClosed } return c.cc.Write(msg) @@ -270,7 +210,7 @@ func (c *Client) Send(msg msgcontrol.ControlMessage) error { // Recv blocks until it receives a package, it will return (nil, nil) if timeout func (c *Client) Recv(ttfbTimeout time.Duration) (msgcontrol.ControlMessage, error) { if types.IsContextDone(c.ctx) { - return nil, ClosedErr + return nil, ErrClosed } return c.cc.Read(ttfbTimeout) diff --git a/types/control/conn.go b/types/control/conn.go index 0f3b036..828b318 100644 --- a/types/control/conn.go +++ b/types/control/conn.go @@ -181,10 +181,6 @@ func (c *Conn) Write(obj msgcontrol.ControlMessage) error { c.writeMutex.Lock() defer c.writeMutex.Unlock() - //// FIXME: bson is extremely fucky and will write empty values if it cannot decode something, so be careful with that - //// or replace this with a registry thingie. - //data, err := bson.Marshal(obj) - data, err := json.Marshal(obj) if err != nil { return fmt.Errorf("could not marshal data: %w", err) diff --git a/types/control/controlhttp/http_client.go b/types/control/controlhttp/http_client.go index 7f6a3c2..46e7803 100644 --- a/types/control/controlhttp/http_client.go +++ b/types/control/controlhttp/http_client.go @@ -26,6 +26,6 @@ func Dial(ctx context.Context, opts dial.Opts, getPriv func() *key.NodePrivate, opts.SetDefaults() return dial.HTTP(ctx, opts, makeControlURL(opts), control.UpgradeProtocol, func(parentCtx context.Context, mc types.MetaConn, brw *bufio.ReadWriter, opts dial.Opts) (*control.Client, error) { - return control.EstablishClient(ctx, mc, brw, opts.EstablishTimeout, getPriv, getSess, controlKey, session, logon) + return control.EstablishClient(parentCtx, mc, brw, opts.EstablishTimeout, getPriv, getSess, controlKey, session, logon) }) } diff --git a/types/control/graph.go b/types/control/graph.go index 404e266..ccffa3d 100644 --- a/types/control/graph.go +++ b/types/control/graph.go @@ -133,23 +133,23 @@ func (g *EdgeGraph) GetEdges(node ClientID) map[ClientID]VisibilityPair { return targetMap } -func (g *EdgeGraph) GetEdge(from, to ClientID) *VisibilityPair { +func (g *EdgeGraph) GetEdge(from, to ClientID) (retPair *VisibilityPair) { g.mu.RLock() defer g.mu.RUnlock() targetMap := g.graph[from] if targetMap == nil { - return nil + return } pair := targetMap[to] if pair != nil { - pair = &(*pair) + *retPair = *pair } - return pair + return } type VisibilityPair struct { diff --git a/types/control/iface.go b/types/control/iface.go index edbfc9c..e46f81f 100644 --- a/types/control/iface.go +++ b/types/control/iface.go @@ -9,9 +9,9 @@ import ( type ClientID key.NodePublic type SessID string -var SessionDoesNotExistError = errors.New("session does not exist") +var ErrSessionDoesNotExist = errors.New("session does not exist") -var SessionIsNotAuthenticating = errors.New("session is not authenticating") +var ErrSessionIsNotAuthenticating = errors.New("session is not authenticating") // ServerLogic denotes exposed functions that a control server must provide for any business logic to interface with it. type ServerLogic interface { diff --git a/types/control/logic.go b/types/control/logic.go index b588ec0..644bea8 100644 --- a/types/control/logic.go +++ b/types/control/logic.go @@ -21,11 +21,11 @@ func (s *Server) whenSessAuthenticating(id SessID, f func(*ServerSession) error) sess, ok := s.sessByID[sid] if !ok { - return SessionDoesNotExistError + return ErrSessionDoesNotExist } if sess.state != Authenticate { - return SessionIsNotAuthenticating + return ErrSessionIsNotAuthenticating } return f(sess) @@ -33,7 +33,7 @@ func (s *Server) whenSessAuthenticating(id SessID, f func(*ServerSession) error) func (s *Server) SendAuthURL(id SessID, url string) error { return s.whenSessAuthenticating(id, func(sess *ServerSession) error { - sess.authChan <- AuthUrl{url: url} + sess.authChan <- AuthURL{url: url} return nil }) @@ -68,7 +68,7 @@ func (s *Server) GetClientID(id SessID) (ClientID, error) { sess, ok := s.sessByID[sid] if !ok { - return nilClientID, SessionDoesNotExistError + return nilClientID, ErrSessionDoesNotExist } return ClientID(sess.Peer), nil @@ -218,6 +218,7 @@ func (s *Server) GetVisibilityPairs(id ClientID) (map[ClientID]VisibilityPair, e return pairs, nil } +//nolint:unused func (s *Server) atomicDoVisibilityPairs(id key.NodePublic, f func(map[ClientID]VisibilityPair) error) error { s.sessLock.RLock() defer s.sessLock.RUnlock() diff --git a/types/control/server.go b/types/control/server.go index 17abd1b..423193c 100644 --- a/types/control/server.go +++ b/types/control/server.go @@ -139,14 +139,14 @@ func (s *Server) Logger() *slog.Logger { return slog.With("control", s.privKey.Public().Debug()) } -func (s *Server) Accept(ctx context.Context, mc types.MetaConn, brw *bufio.ReadWriter, remoteAddrPort netip.AddrPort) error { +func (s *Server) Accept(ctx context.Context, mc types.MetaConn, brw *bufio.ReadWriter, _ netip.AddrPort) error { cc := NewConn(ctx, mc, brw) // TODO this logon segment can be in a different function { // TODO set deadline on read - err, clientHello, logon := s.handleLogon(cc) + clientHello, logon, err := s.handleLogon(cc) if err != nil { return fmt.Errorf("handle logon: %w", err) @@ -168,7 +168,6 @@ func (s *Server) Accept(ctx context.Context, mc types.MetaConn, brw *bufio.ReadW if err = sess.AuthAndStart(); err != nil { return err } - //go sess.Run() } // Wait until connection dead @@ -178,14 +177,6 @@ func (s *Server) Accept(ctx context.Context, mc types.MetaConn, brw *bufio.ReadW return sess.Ctx.Err() - //// for now, send a reject - //if err := cc.Write(&msgcontrol.LogonReject{ - // Reason: "dev: reject unambiguously", - // RetryStrategy: 0, - //}); err != nil { - // return fmt.Errorf("error when sending reject: %w", err) - //} - // TODO send authenticate (then wait, or expect devicekey), accept, or reject // TODO resume @@ -204,17 +195,14 @@ func (s *Server) Accept(ctx context.Context, mc types.MetaConn, brw *bufio.ReadW // TODO (here) mark session as latent } - - //TODO implement me - panic("implement me") } -func (s *Server) handleLogon(cc *Conn) (error, *msgcontrol.ClientHello, *msgcontrol.Logon) { +func (s *Server) handleLogon(cc *Conn) (*msgcontrol.ClientHello, *msgcontrol.Logon, error) { // TODO set deadline on read var clientHello = new(msgcontrol.ClientHello) if err := cc.Expect(clientHello, HandshakeReceiveTimeout); err != nil { - return fmt.Errorf("error when receiving clienthello: %w", err), nil, nil + return nil, nil, fmt.Errorf("error when receiving clienthello: %w", err) } data := randData() @@ -223,12 +211,12 @@ func (s *Server) handleLogon(cc *Conn) (error, *msgcontrol.ClientHello, *msgcont ControlNodePub: s.privKey.Public(), CheckData: s.privKey.SealToNode(clientHello.ClientNodePub, data), }); err != nil { - return fmt.Errorf("error when sending serverhello: %w", err), nil, nil + return nil, nil, fmt.Errorf("error when sending serverhello: %w", err) } logon := new(msgcontrol.Logon) if err := cc.Expect(logon, HandshakeReceiveTimeout); err != nil { - return fmt.Errorf("error when receiving logon: %w", err), nil, nil + return nil, nil, fmt.Errorf("error when receiving logon: %w", err) } // Verify logon @@ -237,46 +225,41 @@ func (s *Server) handleLogon(cc *Conn) (error, *msgcontrol.ClientHello, *msgcont var ok bool if nodeData, ok = s.privKey.OpenFromNode(clientHello.ClientNodePub, logon.NodeKeyAttestation); !ok { - return fmt.Errorf("could not open node attestation"), nil, nil + return nil, nil, fmt.Errorf("could not open node attestation") } if sessData, ok = s.privKey.OpenFromSession(logon.SessKey, logon.SessKeyAttestation); !ok { - return fmt.Errorf("could not open session attestation"), nil, nil + return nil, nil, fmt.Errorf("could not open session attestation") } // FIXME: we should probably make the below something like constant time, to prevent timing attacks. // It is not now, for development purposes. if !slices.Equal(data, nodeData) { - return fmt.Errorf("node data not equal"), nil, nil + return nil, nil, fmt.Errorf("node data not equal") } if !slices.Equal(data, sessData) { - return fmt.Errorf("sess data not equal"), nil, nil + return nil, nil, fmt.Errorf("sess data not equal") } } - return nil, clientHello, logon + return clientHello, logon, nil } func (s *Server) doReject(cc *Conn, sess *ServerSession, err error) error { reject := &msgcontrol.LogonReject{} - if errors.Is(err, stillEstablished) { - if errors.Is(err, stillEstablished) { - // TODO we need to replace this with knocking-and-acquiring - - reject.RetryStrategy = msgcontrol.RegenerateSessionKey - reject.RetryAfter = time.Second * 15 - reject.Reason = "other client session still active, please retry" - } else { - reject.Reason = "cannot log in at the moment, please retry in the future" - } - } else if errors.Is(err, sessionIdMismatch) { + switch { + case errors.Is(err, errStillEstablished): + reject.RetryStrategy = msgcontrol.RegenerateSessionKey + reject.RetryAfter = time.Second * 15 + reject.Reason = "other client session still active, please retry" + case errors.Is(err, errSessionIDMismatch): reject.RetryStrategy = msgcontrol.RecreateSession reject.Reason = "session ID mismatch, please try without" - } else { + default: reject.Reason = "could not acquire session" slog.Warn("rejected session with unknown error", "err", err) } @@ -292,9 +275,9 @@ func (s *Server) doReject(cc *Conn, sess *ServerSession, err error) error { if err := cc.Write(reject); err != nil { return fmt.Errorf("error when sending reject: %w", err) - } else { - return nil } + + return nil } func randData() []byte { @@ -353,8 +336,16 @@ func (s *Server) RunAdditionalSTUN(publicIPs []netip.Addr, listenHost string, lo s.stun.lowServer = stunserver.NewServer(s.ctx) s.stun.highServer = stunserver.NewServer(s.ctx) - go s.stun.lowServer.ListenAndServe(lowAp) - go s.stun.highServer.ListenAndServe(highAp) + go func() { + if err := s.stun.lowServer.ListenAndServe(lowAp); err != nil { + slog.Error("low stun server ListenAndServe error", "err", err) + } + }() + go func() { + if err := s.stun.highServer.ListenAndServe(highAp); err != nil { + slog.Error("high stun server ListenAndServe error", "err", err) + } + }() t := true @@ -398,23 +389,23 @@ func (s *Server) relayExists(id int64) bool { } var ( - incorrectState = errors.New("incorrect state, want nil or Dangling") - stillEstablished = errors.New("session is still established or reestablished") - sessionIdMismatch = errors.New("session ID did not match") + errIncorrectState = errors.New("incorrect state, want nil or Dangling") + errStillEstablished = errors.New("session is still established or reestablished") + errSessionIDMismatch = errors.New("session ID did not match") ) -func (s *Server) ReEstablishOrMakeSession(cc *Conn, nodeKey key.NodePublic, sessKey key.SessionPublic, sessId *string) (retSess *ServerSession, resumed bool, err error) { +func (s *Server) ReEstablishOrMakeSession(cc *Conn, nodeKey key.NodePublic, sessKey key.SessionPublic, sessID *string) (retSess *ServerSession, resumed bool, err error) { s.sessLock.Lock() defer s.sessLock.Unlock() sess, ok := s.sessByNode[nodeKey] if !ok { - if sessId != nil { + if sessID != nil { // There's no session ID to match if its empty. // The client requested resume, so we need to tell it to try again without the session ID, // kicking internal logic to regenerate session keys and clearing state. - err = sessionIdMismatch + err = errSessionIDMismatch return } @@ -433,13 +424,13 @@ func (s *Server) ReEstablishOrMakeSession(cc *Conn, nodeKey key.NodePublic, sess // less simple path: we have a session in state for this nodekey if sess.state != Dangling { // We only accept resuming dangling sessions, everything else is incorrect. - err = incorrectState + err = errIncorrectState if sess.state == Established || sess.state == ReEstablishing { // The server may lag behind for a second, so if we wrap this error and return the session, // the caller could knock that session to force it to Dangling. - err = fmt.Errorf("established state (%w): %w", err, stillEstablished) + err = fmt.Errorf("established state (%w): %w", err, errStillEstablished) retSess = sess } @@ -447,10 +438,10 @@ func (s *Server) ReEstablishOrMakeSession(cc *Conn, nodeKey key.NodePublic, sess } // Session is dangling, we can grab it - if sessId != nil && sess.ID != *sessId { + if sessID != nil && sess.ID != *sessID { // Cant resume, the client expects a different session ID - err = sessionIdMismatch + err = errSessionIDMismatch return } @@ -506,10 +497,6 @@ func (s *Server) RemoveSession(sess *ServerSession) { if err != nil { slog.Error("failed to remove sessions", "err", err) } - - //s.ForVisibleLocked(sess, func(session *ServerSession) { - // session.Bye(sess.Peer) - //}) } slog.Info("REMOVE session", "peer", sess.Peer.Debug()) @@ -518,7 +505,7 @@ func (s *Server) RemoveSession(sess *ServerSession) { delete(s.sessByID, sess.ID) } -//func (s *Server) RegisterSession(sess *ServerSession) { +// func (s *Server) RegisterSession(sess *ServerSession) { // // TODO resume support // // s.sessLock.Lock() @@ -531,7 +518,7 @@ func (s *Server) RemoveSession(sess *ServerSession) { // // s.sessByNode[sess.Peer] = sess // s.sessByID[sess.ID] = sess -//} +// } // ForVisible is called by fromSess' Run goroutine, to inform all other sessions it can see of a change (and the likes) func (s *Server) ForVisible(fromSess *ServerSession, f func(session *ServerSession)) { diff --git a/types/control/server_session.go b/types/control/server_session.go index 9924174..d42959d 100644 --- a/types/control/server_session.go +++ b/types/control/server_session.go @@ -102,18 +102,16 @@ func (s *ServerSession) doAuthenticate(resumed bool) error { errChan <- err - return - } else { - msgChan <- msg - return } + + msgChan <- msg } }() wg.Add(1) deviceKeySeen := false - authUrlSent := false + authURLSent := false // TODO build timeout in here somewhere @@ -136,18 +134,18 @@ func (s *ServerSession) doAuthenticate(resumed bool) error { err := s.conn.Write(msg.LogonReject) if err != nil { - return fmt.Errorf("error while writing logon reject: %w, %w", err, LogonRejectedError) + return fmt.Errorf("error while writing logon reject: %w, %w", err, ErrLogonRejected) } - return LogonRejectedError + return ErrLogonRejected case AcceptAuth: return nil - case AuthUrl: - if authUrlSent { + case AuthURL: + if authURLSent { // auth url already sent, this is a business logic error, we should error out return fmt.Errorf("business logic sent auth url twice") } - authUrlSent = true + authURLSent = true err := s.conn.Write(&msgcontrol.LogonAuthenticate{ AuthenticateURL: msg.url, @@ -179,11 +177,11 @@ type RejectAuth struct { type AcceptAuth struct{} -type AuthUrl struct { +type AuthURL struct { url string } -var LogonRejectedError = errors.New("authentication resulted in logon rejected") +var ErrLogonRejected = errors.New("authentication resulted in logon rejected") // Knock asks the session goroutine/connection to "knock" (send ping, await pong) the session, // to make sure it is still alive. @@ -198,7 +196,7 @@ func (s *ServerSession) Knock() (dangling bool) { func (s *ServerSession) Greet(otherSess *ServerSession, prop msgcontrol.Properties) { s.Slog().Debug("Greet", "from", otherSess.Peer.Debug()) - s.conn.Write(&msgcontrol.PeerAddition{ + if err := s.conn.Write(&msgcontrol.PeerAddition{ PubKey: otherSess.Peer, SessKey: otherSess.Sess, IPv4: otherSess.IPv4.Addr(), @@ -206,7 +204,9 @@ func (s *ServerSession) Greet(otherSess *ServerSession, prop msgcontrol.Properti Endpoints: otherSess.CurrentEndpoints, HomeRelay: otherSess.HomeRelay, Properties: prop, - }) + }); err != nil { + slog.Error("error writing peer addition", "err", err) + } s.greetedMu.Lock() defer s.greetedMu.Unlock() @@ -226,10 +226,12 @@ func (s *ServerSession) UpdateEndpoints(peer key.NodePublic, endpoints []netip.A s.Slog().Debug("UpdateEndpoints", "from", peer.Debug(), "endpoints", endpoints) - s.conn.Write(&msgcontrol.PeerUpdate{ + if err := s.conn.Write(&msgcontrol.PeerUpdate{ PubKey: peer, Endpoints: endpoints, - }) + }); err != nil { + slog.Error("error writing endpoints peer update", "err", err) + } } func (s *ServerSession) UpdateSessKey(peer key.NodePublic, sessKey key.SessionPublic) { @@ -237,10 +239,12 @@ func (s *ServerSession) UpdateSessKey(peer key.NodePublic, sessKey key.SessionPu s.Slog().Debug("UpdateSessKey", "from", peer.Debug(), "sess-key", sessKey) - s.conn.Write(&msgcontrol.PeerUpdate{ + if err := s.conn.Write(&msgcontrol.PeerUpdate{ PubKey: peer, SessKey: &sessKey, - }) + }); err != nil { + slog.Error("error writing sess key peer update", "err", err) + } } func (s *ServerSession) UpdateHomeRelay(peer key.NodePublic, homeRelay int64) { @@ -248,28 +252,34 @@ func (s *ServerSession) UpdateHomeRelay(peer key.NodePublic, homeRelay int64) { s.Slog().Debug("UpdateHomeRelay", "from", peer.Debug(), "home-relay", homeRelay) - s.conn.Write(&msgcontrol.PeerUpdate{ + if err := s.conn.Write(&msgcontrol.PeerUpdate{ PubKey: peer, HomeRelay: &homeRelay, - }) + }); err != nil { + slog.Error("error writing home relay peer update", "err", err) + } } func (s *ServerSession) UpdateProperties(peer key.NodePublic, prop msgcontrol.Properties) { s.Slog().Debug("UpdateProperties", "from", peer.Debug(), "prop", prop) - s.conn.Write(&msgcontrol.PeerUpdate{ + if err := s.conn.Write(&msgcontrol.PeerUpdate{ PubKey: peer, Properties: &prop, - }) + }); err != nil { + slog.Error("error writing properties peer update", "err", err) + } } // Bye to another session, send PeerRemove func (s *ServerSession) Bye(peer key.NodePublic) { s.Slog().Debug("Bye", "from", peer.Debug()) - s.conn.Write(&msgcontrol.PeerRemove{ + if err := s.conn.Write(&msgcontrol.PeerRemove{ PubKey: peer, - }) + }); err != nil { + slog.Error("error writing peer remove message", "err", err) + } } // SendRelays sends all relay information to the client. This is not ran on Resume. @@ -279,7 +289,7 @@ func (s *ServerSession) SendRelays() error { return s.conn.Write(&msgcontrol.RelayUpdate{Relays: s.server.relays}) } -func (s *ServerSession) Resume(cc *Conn, sessKey key.SessionPublic) { +func (s *ServerSession) Resume(_ *Conn, _ key.SessionPublic) { // TODO: check sessKey == s.key, else send sesskeyupdate // TODO we send nothing to the client except queued messages, which are backed up. @@ -380,16 +390,6 @@ func (s *ServerSession) Run() { return } - //s.server.ForVisible(s, func(session *ServerSession) { - // // TODO this currently blocks and holds the lock, we should make Greet async as well - // - // // TODO there is no bubbling of errors, ignore? log? - // - // session.Greet(s) - // - // s.Greet(session) - //}) - s.Slog().Info("established session") for { @@ -433,29 +433,6 @@ func (s *ServerSession) Run() { return } } - - time.Sleep(30 * time.Second) - - // TODO make other peers aware - - // for now, send a reject - //if err = s.conn.Write(&msgcontrol.LogonReject{ - // Reason: "dev: reject unambiguously", - // RetryStrategy: 0, - //}); err != nil { - // err = fmt.Errorf("error when sending reject: %w", err) - // return - //} - - return - - // TODO after Accept, we send the client peer and relay definitions, - // but we need to wait for the client to send their home relay and endpoints, - // before we'd (ideally) send a complete peer info to other clients. - // We will wait 10 seconds for this, before timing out and sending incomplete information. - - // TODO - panic("implement me") } func (s *ServerSession) Slog() *slog.Logger { diff --git a/types/dial/tcp.go b/types/dial/tcp.go index ea52654..c1dc77e 100644 --- a/types/dial/tcp.go +++ b/types/dial/tcp.go @@ -45,7 +45,7 @@ func TCP(ctx context.Context, opts Opts) (net.Conn, error) { var err error - if opts.Addrs == nil || len(opts.Addrs) == 0 { + if len(opts.Addrs) == 0 { opts.Addrs, err = net.DefaultResolver.LookupNetIP(ctx, "ip", opts.Domain) if err != nil { return nil, fmt.Errorf("failed to lookup %s: %w", opts.Domain, err) diff --git a/types/key/bson.go b/types/key/bson.go deleted file mode 100644 index 1e204dd..0000000 --- a/types/key/bson.go +++ /dev/null @@ -1,50 +0,0 @@ -package key - -import ( - "encoding" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/bsontype" -) - -func textMarshalBson(val encoding.TextMarshaler) (bsontype.Type, []byte, error) { - textBytes, err := val.MarshalText() - if err != nil { - return 0, nil, err - } - - return bson.MarshalValue(string(textBytes)) -} - -func textUnmarshalBson(val encoding.TextUnmarshaler, b bsontype.Type, bytes []byte) error { - var s = new(string) - - if err := bson.UnmarshalValue(b, bytes, s); err != nil { - return err - } - - return val.UnmarshalText([]byte(*s)) -} - -func (n *NodePublic) MarshalBSONValue() (bsontype.Type, []byte, error) { - return textMarshalBson(n) -} - -func (n *NodePublic) UnmarshalBSONValue(b bsontype.Type, bytes []byte) error { - return textUnmarshalBson(n, b, bytes) -} - -func (s *SessionPublic) MarshalBSONValue() (bsontype.Type, []byte, error) { - return textMarshalBson(s) -} - -func (s *SessionPublic) UnmarshalBSONValue(b bsontype.Type, bytes []byte) error { - return textUnmarshalBson(s, b, bytes) -} - -func (c *ControlPublic) MarshalBSONValue() (bsontype.Type, []byte, error) { - return textMarshalBson(c) -} - -func (c *ControlPublic) UnmarshalBSONValue(b bsontype.Type, bytes []byte) error { - return textUnmarshalBson(c, b, bytes) -} diff --git a/types/key/iface.go b/types/key/iface.go index eb0deb1..58c0fa6 100644 --- a/types/key/iface.go +++ b/types/key/iface.go @@ -9,7 +9,7 @@ type key interface { } type canTextMarshal interface { - // We need text encoding for JSON and BSON (currently) + // We need text encoding for JSON encoding.TextMarshaler encoding.TextUnmarshaler @@ -19,15 +19,6 @@ type canTextMarshal interface { // encoding.BinaryUnmarshaler } -//type canBsonMarshal interface { -// bson.ValueMarshaler -// bson.ValueUnmarshaler -// -// // TODO maybe also allow/support binary marshalling -// // encoding.BinaryMarshaler -// // encoding.BinaryUnmarshaler -//} - type publicKey interface { key diff --git a/types/misc.go b/types/misc.go index 2e0565d..efe8cf1 100644 --- a/types/misc.go +++ b/types/misc.go @@ -72,12 +72,12 @@ func SliceOrEmpty[T any](v []T) []T { } func SliceOrNil[T any](v []T) []T { - if (v != nil && len(v) > 0) || (v == nil) { + if len(v) > 0 { return v - } else { - // len(v) == 0 - return nil } + + // len(v) == 0 + return nil } // IsContextDone does a quick check on a context to see if its dead. @@ -135,3 +135,7 @@ func Map[T, U any](ts []T, f func(T) U) []U { } type LogonCallback func(url string, deviceKey chan<- string) error + +type CCCKEY int + +const CCC CCCKEY = 112 diff --git a/types/msgactor/notif.go b/types/msgactor/notif.go index e41d2b7..54c36a4 100644 --- a/types/msgactor/notif.go +++ b/types/msgactor/notif.go @@ -13,16 +13,19 @@ const ( PeerStateDirect ) +//nolint:unused type PeerConnStateChangeNotification struct { peer key.NodePublic state PeerState } +//nolint:unused type LocalEndpointsChangeNotification struct { endpoints []netip.AddrPort } +//nolint:unused type HomeRelayChangeNotification struct { homeRelay int64 } diff --git a/types/relay/serverclient.go b/types/relay/serverclient.go index 206f8b0..d0a46b5 100644 --- a/types/relay/serverclient.go +++ b/types/relay/serverclient.go @@ -292,7 +292,9 @@ func (sc *ServerClient) RunSender() { } func (sc *ServerClient) setWriteDeadline() { - sc.netConn.SetWriteDeadline(time.Now().Add(ServerClientWriteTimeout)) + if err := sc.netConn.SetWriteDeadline(time.Now().Add(ServerClientWriteTimeout)); err != nil { + slog.Error("setWriteDeadline error", "err", err) + } } // sendKeepAlive sends a keep-alive frame, without flushing. diff --git a/usrwg/bind.go b/usrwg/bind.go index 144b8cb..d7a5642 100644 --- a/usrwg/bind.go +++ b/usrwg/bind.go @@ -1,11 +1,14 @@ package usrwg import ( + "context" "errors" "fmt" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/key" "golang.org/x/exp/maps" "golang.zx2c4.com/wireguard/conn" + "log/slog" "reflect" "runtime" "sync" @@ -94,15 +97,9 @@ fill: sizes[i] = len(p) copy(packets[i], p) - n += 1 + n++ } - //defer func() { - // for i := 0; i < n; i++ { - // slog.Debug("received packet", "hex", hex.EncodeToString(packets[i][:sizes[i]])) - // } - //}() - if n != 0 { // Buffer filled, return early return @@ -144,9 +141,18 @@ func (b *ToverSokBind) waitForValueFromConns() ([]byte, *endpoint) { cases = append(cases, connChangeCase) - choice, recv, _ := reflect.Select(cases) + choice, recv, recvOk := reflect.Select(cases) - //slog.Debug("waitForValueFromConns reflect.Select", "choice", choice, "len", len(cases), "recv", recv, "recvOk", recvOk, "cases", cases) + slog.Log( + context.Background(), + types.LevelTrace, + "waitForValueFromConns reflect.Select", + "choice", choice, + "len", len(cases), + "recv", recv, + "recvOk", recvOk, + "cases", cases, + ) // choice == last index if choice == len(cases)-1 { @@ -188,7 +194,7 @@ func (b *ToverSokBind) createConnChangeSelectCase() reflect.SelectCase { // SetMark is used by wireguard-go to avoid routing loops. // TODO: double-check -func (b *ToverSokBind) SetMark(mark uint32) error { +func (b *ToverSokBind) SetMark(uint32) error { return nil } diff --git a/usrwg/channel_conn.go b/usrwg/channel_conn.go index dbf14b8..b149f26 100644 --- a/usrwg/channel_conn.go +++ b/usrwg/channel_conn.go @@ -64,7 +64,7 @@ func (cc *ChannelConn) Write(b []byte) (int, error) { return len(b), nil } -func (cc *ChannelConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) { +func (cc *ChannelConn) WriteToUDPAddrPort(_ []byte, _ netip.AddrPort) (int, error) { return 0, net.ErrWriteToConnected } @@ -92,7 +92,7 @@ func (cc *ChannelConn) tryGetOut() (pkt []byte) { // Reads a packet from the outgoing channel, and waits. // // Returns nil on timeout. -func (cc *ChannelConn) getOut(d time.Duration) (pkt []byte) { +func (cc *ChannelConn) getOut(d time.Duration) (pkt []byte) { // nolint:unused select { case pkt = <-cc.outgoing: case <-time.After(d): diff --git a/usrwg/router/router_windows.go b/usrwg/router/router_windows.go index 2f9da0a..92ac234 100644 --- a/usrwg/router/router_windows.go +++ b/usrwg/router/router_windows.go @@ -16,7 +16,7 @@ import ( "github.com/dblohm7/wingoes/com" "github.com/edup2p/common/usrwg/router/winnet" - ole "github.com/go-ole/go-ole" + "github.com/go-ole/go-ole" "go4.org/netipx" "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/tun" @@ -870,7 +870,7 @@ func (ft *firewallTweaker) doAsyncSet() { needClear := !ft.known || len(ft.lastLocal) > 0 || len(val) == 0 ft.mu.Unlock() - err := ft.doSet(val, needClear) + err := ft.doSet(val, needclearMsg) if err != nil { slog.Warn("firewall: set failed: %v", err) } @@ -878,7 +878,7 @@ func (ft *firewallTweaker) doAsyncSet() { ft.mu.Lock() ft.lastLocal = val - ft.known = (err == nil) + ft.known = err == nil } } diff --git a/usrwg/router/util.go b/usrwg/router/util.go index 6adfc7a..eae3d63 100644 --- a/usrwg/router/util.go +++ b/usrwg/router/util.go @@ -6,10 +6,10 @@ import ( "os/exec" ) -func prefixesToAdd(new, curr []netip.Prefix) (add []netip.Prefix) { - for _, cur := range new { +func prefixesToAdd(newP, currP []netip.Prefix) (add []netip.Prefix) { + for _, cur := range newP { found := false - for _, v := range curr { + for _, v := range currP { found = v == cur if found { break @@ -22,10 +22,10 @@ func prefixesToAdd(new, curr []netip.Prefix) (add []netip.Prefix) { return } -func prefixesToRemove(new, curr []netip.Prefix) (remove []netip.Prefix) { - for _, cur := range curr { +func prefixesToRemove(newP, currP []netip.Prefix) (remove []netip.Prefix) { + for _, cur := range currP { found := false - for _, v := range new { + for _, v := range newP { found = v == cur if found { break diff --git a/usrwg/wgusp.go b/usrwg/wgusp.go index 9f97916..9ba121e 100644 --- a/usrwg/wgusp.go +++ b/usrwg/wgusp.go @@ -9,8 +9,6 @@ import ( "golang.zx2c4.com/wireguard/device" "log/slog" "net/netip" - "runtime" - "strings" ) func init() { @@ -78,7 +76,9 @@ func (u *UserSpaceWireGuardHost) Controller(privateKey key.NodePrivate, addr4, a nKey := key.UnveilPrivate(privateKey) - wgDev.IpcSet(fmt.Sprintf(WGGOIPCDevSetup, nKey.HexString())) + if err := wgDev.IpcSet(fmt.Sprintf(WGGOIPCDevSetup, nKey.HexString())); err != nil { + return nil, fmt.Errorf("failed to set private key on wireguard device: %w", err) + } if err := wgDev.Up(); err != nil { return nil, fmt.Errorf("failed to bring up wireguard device: %w", err) @@ -106,45 +106,6 @@ func (u *UserSpaceWireGuardHost) Controller(privateKey key.NodePrivate, addr4, a return usrwgc, nil } -func (u *UserSpaceWireGuardHost) tempPrintInstructions(addr4, addr6 netip.Prefix, name string) { - - const sep = "; " - - switch runtime.GOOS { - case "darwin": - const ( - ifconfig4 = "sudo ifconfig %s inet %s/32 %s" - ifconfig6 = "sudo ifconfig %s inet6 %s %s prefixlen 128" - - route4 = "sudo route add -inet %s -iface %s" - route6 = "sudo route add -inet6 %s -iface %s" - ) - - slog.Warn("Please run these lines in a separate terminal:") - slog.Warn( - strings.Join([]string{ - fmt.Sprintf(ifconfig4, name, addr4.Addr().String(), addr4.Addr().String()), - fmt.Sprintf(ifconfig6, name, addr6.Addr().String(), addr6.Addr().String()), - fmt.Sprintf(route4, addr4.String(), name), - fmt.Sprintf(route6, addr6.String(), name), - }, sep), - ) - case "linux": - const ( - ip = "sudo ip address add %s dev %s" - ) - - slog.Warn("Please run these lines in a separate terminal:") - slog.Warn( - strings.Join([]string{ - fmt.Sprintf(ip, addr4.String(), name), - fmt.Sprintf(ip, addr6.String(), name), - }, sep), - ) - } - -} - type UserSpaceWireGuardController struct { wgDev *device.Device bind *ToverSokBind @@ -181,9 +142,8 @@ func (u *UserSpaceWireGuardController) RemovePeer(publicKey key.NodePublic) erro return nil } -func (u *UserSpaceWireGuardController) GetStats(publicKey key.NodePublic) (*toversok.WGStats, error) { - //TODO implement me - //panic("implement me") +func (u *UserSpaceWireGuardController) GetStats(_ key.NodePublic) (*toversok.WGStats, error) { + // TODO implement me return nil, nil } @@ -194,9 +154,10 @@ func (u *UserSpaceWireGuardController) ConnFor(node key.NodePublic) types.UDPCon func (u *UserSpaceWireGuardController) Close() { u.wgDev.Close() - // TODO return or log error - u.bind.Close() - u.router.Close() + if err := u.bind.Close(); err != nil { + slog.Error("Failed to close wireguard bind", "err", err) + } + if err := u.router.Close(); err != nil { + slog.Error("Failed to close router", "err", err) + } } - -//const _ toversok.WireGuardHost = UserspaceWireguardHost{} From 035af4ea06c2b44b40d41b917d44977d723ed698 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 12:51:29 +0100 Subject: [PATCH 03/82] gofumpt run --- cmd/control_server/main.go | 18 +++++------ cmd/dev_client/main.go | 33 ++++++++++---------- cmd/relay_server/main.go | 15 ++++----- cmd/stun_client/main.go | 5 +-- extwg/wgctrl.go | 15 +++++---- test_suite/control_server/main.go | 6 ++-- test_suite/relay_server/main.go | 15 ++++----- test_suite/test_client/main.go | 20 ++++++------ toversok/actors/a_conn.go | 7 +++-- toversok/actors/a_direct.go | 7 +++-- toversok/actors/a_eman.go | 2 +- toversok/actors/a_relay.go | 10 +++--- toversok/actors/a_sman.go | 4 +-- toversok/actors/a_sockrecv.go | 5 +-- toversok/actors/a_tman.go | 10 +++--- toversok/actors/common.go | 3 +- toversok/actors/peerstate/common.go | 9 +++--- toversok/actors/peerstate/e_half.go | 5 +-- toversok/actors/peerstate/e_rendez.go | 3 +- toversok/actors/peerstate/e_t_finalising.go | 3 +- toversok/actors/peerstate/e_t_half.go | 5 +-- toversok/actors/peerstate/e_t_pretransmit.go | 3 +- toversok/actors/peerstate/e_t_rendez.go | 5 +-- toversok/actors/peerstate/e_transmitting.go | 3 +- toversok/actors/peerstate/iface.go | 3 +- toversok/actors/peerstate/s_established.go | 7 +++-- toversok/actors/peerstate/s_inactive.go | 3 +- toversok/actors/peerstate/s_t_booting.go | 5 +-- toversok/actors/peerstate/s_t_teardown.go | 5 +-- toversok/actors/peerstate/s_trying.go | 5 +-- toversok/actors/peerstate/s_waiting.go | 3 +- toversok/actors/peerstate/util.go | 5 +-- toversok/actors/stage.go | 15 ++++----- toversok/actors/util.go | 7 +++-- toversok/actors/util_test.go | 20 +++++++----- toversok/control_conn.go | 18 +++++------ toversok/engine.go | 8 ++--- toversok/events.go | 3 +- toversok/interface.go | 5 +-- toversok/session.go | 7 +++-- types/control/client.go | 8 ++--- types/control/conn.go | 7 ++--- types/control/controlhttp/http_client.go | 1 + types/control/controlhttp/http_server.go | 3 +- types/control/graph.go | 3 +- types/control/iface.go | 9 ++++-- types/control/logic.go | 1 + types/control/server.go | 19 +++++------ types/control/server_session.go | 12 +++---- types/dial/http.go | 4 +-- types/dial/http_server.go | 3 +- types/dial/tcp.go | 1 - types/ifaces/actor.go | 5 +-- types/ifaces/control.go | 3 +- types/ifaces/stage.go | 3 +- types/key/control_priv.go | 1 + types/key/control_pub.go | 3 +- types/key/node_priv.go | 3 +- types/key/node_pub.go | 3 +- types/key/session_priv.go | 1 + types/key/session_pub.go | 1 + types/key/session_shared.go | 1 + types/key/util.go | 5 +-- types/misc.go | 3 +- types/msgactor/msg.go | 5 +-- types/msgactor/notif.go | 3 +- types/msgcontrol/msg.go | 5 +-- types/msgcontrol/msg_iface.go | 14 +++++++++ types/msgsess/parsing.go | 3 +- types/msgsess/ping.go | 3 +- types/msgsess/pong.go | 3 +- types/msgsess/rendezvous.go | 3 +- types/relay/client.go | 10 +++--- types/relay/frame.go | 1 + types/relay/info.go | 3 +- types/relay/relayhttp/http_client.go | 1 + types/relay/relayhttp/http_server.go | 3 +- types/relay/server.go | 9 +++--- types/relay/serverclient.go | 9 +++--- types/stage/stage.go | 3 +- types/stun/response.go | 1 - types/stun/server.go | 1 - usrwg/bind.go | 12 +++---- usrwg/channel_conn.go | 1 - usrwg/endpoint.go | 3 +- usrwg/router/router_bsd.go | 18 ++++++----- usrwg/router/router_linux.go | 4 +-- usrwg/router/router_windows.go | 19 ++++++----- usrwg/router/winnet/winnet.go | 7 +++-- usrwg/wgusp.go | 8 ++--- 90 files changed, 324 insertions(+), 261 deletions(-) diff --git a/cmd/control_server/main.go b/cmd/control_server/main.go index 7b85708..1290f2b 100644 --- a/cmd/control_server/main.go +++ b/cmd/control_server/main.go @@ -6,10 +6,6 @@ import ( "errors" "flag" "fmt" - "github.com/edup2p/common/types/control" - "github.com/edup2p/common/types/control/controlhttp" - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/relay" "io" "log" "log/slog" @@ -23,6 +19,11 @@ import ( "sync" "syscall" "time" + + "github.com/edup2p/common/types/control" + "github.com/edup2p/common/types/control/controlhttp" + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/relay" ) var ( @@ -42,7 +43,7 @@ var ( func main() { h := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ Level: programLevel, - //AddSource: true, + // AddSource: true, }) slog.SetDefault(slog.New(h)) programLevel.Set(-8) @@ -117,7 +118,7 @@ func main() { Addr: *addr, Handler: mux, // TODO - //ErrorLog: slog.NewLogLogger(), + // ErrorLog: slog.NewLogLogger(), ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, @@ -172,7 +173,6 @@ func (cs *ControlServer) HandleAuthRequest(w http.ResponseWriter, r *http.Reques // Fail http.Redirect(w, r, "/auth/fail", http.StatusFound) } - } func (cs *ControlServer) OnSessionCreate(id control.SessID, cid control.ClientID) { @@ -508,14 +508,14 @@ func writeNewConfig() Config { } func writeConfig(cfg Config, path string) { - if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil { + if err := os.MkdirAll(filepath.Dir(path), 0o777); err != nil { log.Fatal(err) } b, err := json.MarshalIndent(cfg, "", "\t") if err != nil { log.Fatal(err) } - if err := os.WriteFile(path, b, 0600); err != nil { + if err := os.WriteFile(path, b, 0o600); err != nil { log.Fatal(err) } } diff --git a/cmd/dev_client/main.go b/cmd/dev_client/main.go index ecf6b7e..a35f7ab 100644 --- a/cmd/dev_client/main.go +++ b/cmd/dev_client/main.go @@ -7,6 +7,17 @@ import ( "errors" "flag" "fmt" + "log" + "log/slog" + "math" + "net/netip" + "os" + "path/filepath" + "runtime/pprof" + "strconv" + "strings" + "sync" + "github.com/abiosoft/ishell/v2" "github.com/edup2p/common/extwg" "github.com/edup2p/common/toversok" @@ -19,16 +30,6 @@ import ( "github.com/edup2p/common/usrwg" "golang.org/x/exp/maps" "golang.zx2c4.com/wireguard/wgctrl" - "log" - "log/slog" - "math" - "net/netip" - "os" - "path/filepath" - "runtime/pprof" - "strconv" - "strings" - "sync" ) var ( @@ -239,7 +240,6 @@ func getOrGenerateKey(file string, c *ishell.Context) (key.NodePrivate, error) { } data, err := os.ReadFile(file) - if err != nil { if os.IsNotExist(err) { c.Println(fmt.Sprintf("%s does not exist, generating new key...", file)) @@ -251,7 +251,7 @@ func getOrGenerateKey(file string, c *ishell.Context) (key.NodePrivate, error) { return k, fmt.Errorf("failed to marshal private key: %w", err) } - if err := os.WriteFile(file, jsonData, 0644); err != nil { + if err := os.WriteFile(file, jsonData, 0o644); err != nil { return k, fmt.Errorf("failed to write private key to file: %w", err) } @@ -536,10 +536,10 @@ func fcCmd() *ishell.Cmd { ID: id, Key: *relayKey, Domain: *domain, - //IPs: gonull.Nullable[[]netip.Addr]{}, - //STUNPort: gonull.Nullable[uint16]{}, - //HTTPSPort: gonull.Nullable[uint16]{}, - //HTTPPort: gonull.Nullable[uint16]{}, + // IPs: gonull.Nullable[[]netip.Addr]{}, + // STUNPort: gonull.Nullable[uint16]{}, + // HTTPSPort: gonull.Nullable[uint16]{}, + // HTTPPort: gonull.Nullable[uint16]{}, IsInsecure: *insecure, } @@ -754,7 +754,6 @@ func wgCmd() *ishell.Cmd { } func enCmd() *ishell.Cmd { - c := &ishell.Cmd{ Name: "en", Help: "toversok engine and subcommands", diff --git a/cmd/relay_server/main.go b/cmd/relay_server/main.go index 78c10aa..837040f 100644 --- a/cmd/relay_server/main.go +++ b/cmd/relay_server/main.go @@ -6,10 +6,6 @@ import ( "errors" "flag" "fmt" - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/relay" - "github.com/edup2p/common/types/relay/relayhttp" - stunserver "github.com/edup2p/common/types/stun" "io" "log" "log/slog" @@ -22,6 +18,11 @@ import ( "strings" "syscall" "time" + + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/relay" + "github.com/edup2p/common/types/relay/relayhttp" + stunserver "github.com/edup2p/common/types/stun" ) var ( @@ -116,7 +117,7 @@ func main() { Addr: *addr, Handler: mux, // TODO - //ErrorLog: slog.NewLogLogger(), + // ErrorLog: slog.NewLogLogger(), ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, @@ -204,7 +205,7 @@ func loadConfig() Config { } func writeNewConfig() Config { - if err := os.MkdirAll(filepath.Dir(*configPath), 0777); err != nil { + if err := os.MkdirAll(filepath.Dir(*configPath), 0o777); err != nil { log.Fatal(err) } cfg := newConfig() @@ -212,7 +213,7 @@ func writeNewConfig() Config { if err != nil { log.Fatal(err) } - if err := os.WriteFile(*configPath, b, 0600); err != nil { + if err := os.WriteFile(*configPath, b, 0o600); err != nil { log.Fatal(err) } return cfg diff --git a/cmd/stun_client/main.go b/cmd/stun_client/main.go index 14fa4ad..9f2bdd9 100644 --- a/cmd/stun_client/main.go +++ b/cmd/stun_client/main.go @@ -1,12 +1,13 @@ package main import ( - "github.com/edup2p/common/types" - "github.com/edup2p/common/types/stun" "log" "net" "net/netip" "os" + + "github.com/edup2p/common/types" + "github.com/edup2p/common/types/stun" ) func main() { diff --git a/extwg/wgctrl.go b/extwg/wgctrl.go index 935cec2..ae3c997 100644 --- a/extwg/wgctrl.go +++ b/extwg/wgctrl.go @@ -2,6 +2,13 @@ package extwg import ( "fmt" + "log/slog" + "net" + "net/netip" + "runtime" + "strings" + "sync" + "github.com/edup2p/common/toversok" "github.com/edup2p/common/types" "github.com/edup2p/common/types/key" @@ -9,12 +16,6 @@ import ( "golang.org/x/exp/maps" "golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" - "log/slog" - "net" - "net/netip" - "runtime" - "strings" - "sync" ) // A wireguard configurator by the help of wgtools shell commands. @@ -130,7 +131,6 @@ func (w *WGCtrl) Controller(privateKey key.NodePrivate, addr4, addr6 netip.Prefi var device *wgtypes.Device device, err = w.client.Device(w.name) - if err != nil { return nil, err } @@ -246,7 +246,6 @@ func (w *WGCtrl) rebindMapping(m *mapping) error { func (w *WGCtrl) bindLocal() *mapping { conn, err := w.getWGConn(nil) - if err != nil { panic(fmt.Sprintf("error when first binding to wgport: %s", err)) } diff --git a/test_suite/control_server/main.go b/test_suite/control_server/main.go index f3357b7..e180a08 100644 --- a/test_suite/control_server/main.go +++ b/test_suite/control_server/main.go @@ -67,7 +67,7 @@ func main() { Addr: *addr, Handler: mux, // TODO - //ErrorLog: slog.NewLogLogger(), + // ErrorLog: slog.NewLogLogger(), ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, @@ -366,14 +366,14 @@ func writeNewConfig() Config { } func writeConfig(cfg Config, path string) { - if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil { + if err := os.MkdirAll(filepath.Dir(path), 0o777); err != nil { log.Fatal(err) } b, err := json.MarshalIndent(cfg, "", "\t") if err != nil { log.Fatal(err) } - if err := os.WriteFile(path, b, 0600); err != nil { + if err := os.WriteFile(path, b, 0o600); err != nil { log.Fatal(err) } } diff --git a/test_suite/relay_server/main.go b/test_suite/relay_server/main.go index a07807e..a0ee18d 100644 --- a/test_suite/relay_server/main.go +++ b/test_suite/relay_server/main.go @@ -6,10 +6,6 @@ import ( "errors" "flag" "fmt" - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/relay" - "github.com/edup2p/common/types/relay/relayhttp" - stunserver "github.com/edup2p/common/types/stun" "io" "log" "log/slog" @@ -22,6 +18,11 @@ import ( "strings" "syscall" "time" + + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/relay" + "github.com/edup2p/common/types/relay/relayhttp" + stunserver "github.com/edup2p/common/types/stun" ) var ( @@ -117,7 +118,7 @@ func main() { Addr: *addr, Handler: mux, // TODO - //ErrorLog: slog.NewLogLogger(), + // ErrorLog: slog.NewLogLogger(), ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, @@ -205,7 +206,7 @@ func loadConfig() Config { } func writeNewConfig() Config { - if err := os.MkdirAll(filepath.Dir(*configPath), 0777); err != nil { + if err := os.MkdirAll(filepath.Dir(*configPath), 0o777); err != nil { log.Fatal(err) } cfg := newConfig() @@ -213,7 +214,7 @@ func writeNewConfig() Config { if err != nil { log.Fatal(err) } - if err := os.WriteFile(*configPath, b, 0600); err != nil { + if err := os.WriteFile(*configPath, b, 0o600); err != nil { log.Fatal(err) } return cfg diff --git a/test_suite/test_client/main.go b/test_suite/test_client/main.go index c28d1b0..344b836 100644 --- a/test_suite/test_client/main.go +++ b/test_suite/test_client/main.go @@ -6,12 +6,6 @@ import ( "errors" "flag" "fmt" - "github.com/edup2p/common/extwg" - "github.com/edup2p/common/toversok" - "github.com/edup2p/common/types/dial" - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/usrwg" - "golang.zx2c4.com/wireguard/wgctrl" "log/slog" "net/netip" "os" @@ -19,6 +13,13 @@ import ( "path/filepath" "strings" "syscall" + + "github.com/edup2p/common/extwg" + "github.com/edup2p/common/toversok" + "github.com/edup2p/common/types/dial" + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/usrwg" + "golang.zx2c4.com/wireguard/wgctrl" ) // Flags @@ -72,7 +73,7 @@ func main() { flag.Parse() - var level = slog.LevelInfo + level := slog.LevelInfo switch logLevel { case "debug": @@ -123,7 +124,7 @@ func main() { } if controlHost != "" || controlPort != 0 || controlKeyStr != "" { - var mustWrite = false + mustWrite := false if config.ControlPort != controlPort16 { slog.Warn("config control port and given control port disagree, overwriting config", "config", config.ControlPort, "cli-given", controlPort16) @@ -248,7 +249,6 @@ func getOrGenerateConfig(file string) (*Config, error) { var c *Config data, err := os.ReadFile(file) - if err != nil { if os.IsNotExist(err) { slog.Info("config file does not exist, generating new config...", "file", file) @@ -292,7 +292,7 @@ func writeConfig(c *Config, file string) error { return fmt.Errorf("failed to marshal config: %w", err) } - if err := os.WriteFile(file, jsonData, 0644); err != nil { + if err := os.WriteFile(file, jsonData, 0o644); err != nil { return fmt.Errorf("failed to write config to file: %w", err) } diff --git a/toversok/actors/a_conn.go b/toversok/actors/a_conn.go index 695ef9a..245d0fb 100644 --- a/toversok/actors/a_conn.go +++ b/toversok/actors/a_conn.go @@ -3,12 +3,13 @@ package actors import ( "context" "errors" - "github.com/edup2p/common/types" - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/msgactor" "net" "net/netip" "time" + + "github.com/edup2p/common/types" + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/msgactor" ) type OutConn struct { diff --git a/toversok/actors/a_direct.go b/toversok/actors/a_direct.go index 9fbad18..e3bea43 100644 --- a/toversok/actors/a_direct.go +++ b/toversok/actors/a_direct.go @@ -2,15 +2,16 @@ package actors import ( "context" + "log/slog" + "net/netip" + "runtime" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/ifaces" "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgactor" "github.com/edup2p/common/types/msgsess" "golang.org/x/exp/maps" - "log/slog" - "net/netip" - "runtime" ) type directWriteRequest struct { diff --git a/toversok/actors/a_eman.go b/toversok/actors/a_eman.go index 1f5b08f..d6b14f2 100644 --- a/toversok/actors/a_eman.go +++ b/toversok/actors/a_eman.go @@ -131,7 +131,7 @@ func (em *EndpointManager) startSTUN() { em.collectedResponse = make([]stunResponse, 0) - var stunReq = &msgactor.DRouterPushSTUN{Packets: make(map[netip.AddrPort][]byte)} + stunReq := &msgactor.DRouterPushSTUN{Packets: make(map[netip.AddrPort][]byte)} em.stunRequests = make(map[netip.AddrPort]stunRequest) diff --git a/toversok/actors/a_relay.go b/toversok/actors/a_relay.go index 79efc87..b210b82 100644 --- a/toversok/actors/a_relay.go +++ b/toversok/actors/a_relay.go @@ -3,6 +3,10 @@ package actors import ( "context" "fmt" + "log/slog" + "runtime" + "time" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/dial" "github.com/edup2p/common/types/ifaces" @@ -11,9 +15,6 @@ import ( "github.com/edup2p/common/types/msgsess" "github.com/edup2p/common/types/relay" "github.com/edup2p/common/types/relay/relayhttp" - "log/slog" - "runtime" - "time" ) // RestartableRelayConn is a Relay connection that will automatically reconnect, @@ -364,7 +365,6 @@ func (rm *RelayManager) Run() { rm.s.RRouter.Push(frame) } } - } func (rm *RelayManager) Close() { @@ -373,7 +373,7 @@ func (rm *RelayManager) Close() { func (rm *RelayManager) selectRelay(latencies map[int64]time.Duration) int64 { var srid int64 - var slat = 60 * time.Second + slat := 60 * time.Second L(rm).Debug("selectRelay: starting latency check") diff --git a/toversok/actors/a_sman.go b/toversok/actors/a_sman.go index 36ce4d1..7249e73 100644 --- a/toversok/actors/a_sman.go +++ b/toversok/actors/a_sman.go @@ -2,10 +2,11 @@ package actors import ( "fmt" + "slices" + "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgactor" "github.com/edup2p/common/types/msgsess" - "slices" ) // SessionManager receives frames from routers and decrypts them, @@ -120,7 +121,6 @@ func (sm *SessionManager) Unpack(frameWithMagic []byte) (*msgsess.ClearMessage, } sMsg, err := msgsess.ParseSessionMessage(clearBytes) - if err != nil { return nil, fmt.Errorf("could not parse session message: %s", err) } diff --git a/toversok/actors/a_sockrecv.go b/toversok/actors/a_sockrecv.go index e69fdbb..6f409a6 100644 --- a/toversok/actors/a_sockrecv.go +++ b/toversok/actors/a_sockrecv.go @@ -4,11 +4,12 @@ import ( "context" "errors" "fmt" - "github.com/edup2p/common/types" "net" "net/netip" "slices" "time" + + "github.com/edup2p/common/types" ) type RecvFrame struct { @@ -50,7 +51,7 @@ func (r *SockRecv) Run() { return } - var buf = make([]byte, 1<<16) + buf := make([]byte, 1<<16) for { if context.Cause(r.ctx) != nil { diff --git a/toversok/actors/a_tman.go b/toversok/actors/a_tman.go index 654ee81..78552a0 100644 --- a/toversok/actors/a_tman.go +++ b/toversok/actors/a_tman.go @@ -1,6 +1,10 @@ package actors import ( + "maps" + "net/netip" + "time" + "github.com/edup2p/common/toversok/actors/peerstate" "github.com/edup2p/common/types" "github.com/edup2p/common/types/ifaces" @@ -8,10 +12,6 @@ import ( "github.com/edup2p/common/types/msgactor" "github.com/edup2p/common/types/msgsess" "github.com/edup2p/common/types/stage" - maps2 "golang.org/x/exp/maps" - "maps" - "net/netip" - "time" ) type TrafficManager struct { @@ -152,7 +152,7 @@ func (tm *TrafficManager) Handle(m msgactor.ActorMessage) { func (tm *TrafficManager) DoStateTick() { // We explicitly range over a slice of the keys we already got, // since golang likes to complain when we mutate while we iterate. - for _, peer := range maps2.Keys(tm.peerState) { + for peer := range maps.Keys(tm.peerState) { tm.forState(peer, func(s peerstate.PeerState) peerstate.PeerState { return s.OnTick() }) diff --git a/toversok/actors/common.go b/toversok/actors/common.go index 97340da..574ec6a 100644 --- a/toversok/actors/common.go +++ b/toversok/actors/common.go @@ -2,8 +2,9 @@ package actors import ( "context" - "github.com/edup2p/common/types/msgactor" "log/slog" + + "github.com/edup2p/common/types/msgactor" ) type ActorCommon struct { diff --git a/toversok/actors/peerstate/common.go b/toversok/actors/peerstate/common.go index 54c87bb..fa49779 100644 --- a/toversok/actors/peerstate/common.go +++ b/toversok/actors/peerstate/common.go @@ -2,14 +2,15 @@ package peerstate import ( "context" + "log/slog" + "net/netip" + "time" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/ifaces" "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgsess" "github.com/edup2p/common/types/stage" - "log/slog" - "net/netip" - "time" ) const ( @@ -79,7 +80,6 @@ func (sc *StateCommon) ackPongDirect(_ netip.AddrPort, sess key.SessionPublic, p // TODO add bool here and checks by callers func (sc *StateCommon) ackPongRelay(_ int64, node key.NodePublic, sess key.SessionPublic, pong *msgsess.Pong) { - // Relay pongs should come in response to relay pings, note if it is different. sent, ok := sc.tm.Pings()[pong.TxID] @@ -108,7 +108,6 @@ func (sc *StateCommon) ackPongRelay(_ int64, node key.NodePublic, sess key.Sessi // TODO more checks? (permissive, but log) delete(sc.tm.Pings(), pong.TxID) - } func (sc *StateCommon) getPeerInfo() *stage.PeerInfo { diff --git a/toversok/actors/peerstate/e_half.go b/toversok/actors/peerstate/e_half.go index 8909628..05675bf 100644 --- a/toversok/actors/peerstate/e_half.go +++ b/toversok/actors/peerstate/e_half.go @@ -1,10 +1,11 @@ package peerstate import ( - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/msgsess" "net/netip" "time" + + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/msgsess" ) type EstHalf struct { diff --git a/toversok/actors/peerstate/e_rendez.go b/toversok/actors/peerstate/e_rendez.go index acff7f7..2ebf737 100644 --- a/toversok/actors/peerstate/e_rendez.go +++ b/toversok/actors/peerstate/e_rendez.go @@ -1,9 +1,10 @@ package peerstate import ( + "net/netip" + "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgsess" - "net/netip" ) type EstRendezAck struct { diff --git a/toversok/actors/peerstate/e_t_finalising.go b/toversok/actors/peerstate/e_t_finalising.go index b3014f6..1df4f01 100644 --- a/toversok/actors/peerstate/e_t_finalising.go +++ b/toversok/actors/peerstate/e_t_finalising.go @@ -1,9 +1,10 @@ package peerstate import ( + "net/netip" + "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgsess" - "net/netip" ) type Finalizing struct { diff --git a/toversok/actors/peerstate/e_t_half.go b/toversok/actors/peerstate/e_t_half.go index 606aece..0b4ef65 100644 --- a/toversok/actors/peerstate/e_t_half.go +++ b/toversok/actors/peerstate/e_t_half.go @@ -1,10 +1,11 @@ package peerstate import ( - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/msgsess" "net/netip" "time" + + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/msgsess" ) type EstHalfIng struct { diff --git a/toversok/actors/peerstate/e_t_pretransmit.go b/toversok/actors/peerstate/e_t_pretransmit.go index 16a5e29..8299701 100644 --- a/toversok/actors/peerstate/e_t_pretransmit.go +++ b/toversok/actors/peerstate/e_t_pretransmit.go @@ -1,9 +1,10 @@ package peerstate import ( + "net/netip" + "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgsess" - "net/netip" ) type EstPreTransmit struct { diff --git a/toversok/actors/peerstate/e_t_rendez.go b/toversok/actors/peerstate/e_t_rendez.go index 1dbd65a..59563ce 100644 --- a/toversok/actors/peerstate/e_t_rendez.go +++ b/toversok/actors/peerstate/e_t_rendez.go @@ -1,11 +1,12 @@ package peerstate import ( + "net/netip" + "time" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgsess" - "net/netip" - "time" ) // EstRendezGot is a transient state that immediately transitions to EstRendezAck after the first OnTick diff --git a/toversok/actors/peerstate/e_transmitting.go b/toversok/actors/peerstate/e_transmitting.go index 72dd25a..6337a76 100644 --- a/toversok/actors/peerstate/e_transmitting.go +++ b/toversok/actors/peerstate/e_transmitting.go @@ -1,9 +1,10 @@ package peerstate import ( + "net/netip" + "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgsess" - "net/netip" ) type EstTransmitting struct { diff --git a/toversok/actors/peerstate/iface.go b/toversok/actors/peerstate/iface.go index 582a8ec..ff74157 100644 --- a/toversok/actors/peerstate/iface.go +++ b/toversok/actors/peerstate/iface.go @@ -1,9 +1,10 @@ package peerstate import ( + "net/netip" + "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgsess" - "net/netip" ) // This state pattern was inspired by https://refactoring.guru/design-patterns/state/go/example diff --git a/toversok/actors/peerstate/s_established.go b/toversok/actors/peerstate/s_established.go index 6252acf..ef5441d 100644 --- a/toversok/actors/peerstate/s_established.go +++ b/toversok/actors/peerstate/s_established.go @@ -2,12 +2,13 @@ package peerstate import ( "context" - "github.com/edup2p/common/types" - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/msgsess" "net/netip" "slices" "time" + + "github.com/edup2p/common/types" + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/msgsess" ) const EstablishedPingInterval = time.Second * 2 diff --git a/toversok/actors/peerstate/s_inactive.go b/toversok/actors/peerstate/s_inactive.go index 44679ec..0f7c2f1 100644 --- a/toversok/actors/peerstate/s_inactive.go +++ b/toversok/actors/peerstate/s_inactive.go @@ -1,9 +1,10 @@ package peerstate import ( + "net/netip" + "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgsess" - "net/netip" ) type Inactive struct { diff --git a/toversok/actors/peerstate/s_t_booting.go b/toversok/actors/peerstate/s_t_booting.go index 6465bbd..68f6519 100644 --- a/toversok/actors/peerstate/s_t_booting.go +++ b/toversok/actors/peerstate/s_t_booting.go @@ -1,11 +1,12 @@ package peerstate import ( + "net/netip" + "time" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgsess" - "net/netip" - "time" ) type Booting struct { diff --git a/toversok/actors/peerstate/s_t_teardown.go b/toversok/actors/peerstate/s_t_teardown.go index a3a72b7..90ddf72 100644 --- a/toversok/actors/peerstate/s_t_teardown.go +++ b/toversok/actors/peerstate/s_t_teardown.go @@ -1,10 +1,11 @@ package peerstate import ( - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/msgsess" "net/netip" "time" + + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/msgsess" ) type Teardown struct { diff --git a/toversok/actors/peerstate/s_trying.go b/toversok/actors/peerstate/s_trying.go index eb11bb4..e2beea5 100644 --- a/toversok/actors/peerstate/s_trying.go +++ b/toversok/actors/peerstate/s_trying.go @@ -1,10 +1,11 @@ package peerstate import ( - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/msgsess" "net/netip" "time" + + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/msgsess" ) type Trying struct { diff --git a/toversok/actors/peerstate/s_waiting.go b/toversok/actors/peerstate/s_waiting.go index b20b124..b4f3cbb 100644 --- a/toversok/actors/peerstate/s_waiting.go +++ b/toversok/actors/peerstate/s_waiting.go @@ -1,10 +1,11 @@ package peerstate import ( + "net/netip" + "github.com/edup2p/common/types/ifaces" "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgsess" - "net/netip" ) type WaitingForInfo struct { diff --git a/toversok/actors/peerstate/util.go b/toversok/actors/peerstate/util.go index 7b8259d..c2a58e0 100644 --- a/toversok/actors/peerstate/util.go +++ b/toversok/actors/peerstate/util.go @@ -2,11 +2,12 @@ package peerstate import ( "context" + "log/slog" + "net/netip" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgsess" - "log/slog" - "net/netip" ) // cascadeDirect makes it so that first we call the default "tick" function of a peer's state, diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index 8cc8080..b6b7906 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -3,6 +3,14 @@ package actors import ( "context" "errors" + "log/slog" + "net" + "net/netip" + "reflect" + "slices" + "sync" + "time" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/ifaces" "github.com/edup2p/common/types/key" @@ -11,13 +19,6 @@ import ( "github.com/edup2p/common/types/relay" "github.com/edup2p/common/types/stage" "golang.org/x/exp/maps" - "log/slog" - "net" - "net/netip" - "reflect" - "slices" - "sync" - "time" ) type OutConnActor interface { diff --git a/toversok/actors/util.go b/toversok/actors/util.go index a42dff4..03e4032 100644 --- a/toversok/actors/util.go +++ b/toversok/actors/util.go @@ -3,13 +3,14 @@ package actors import ( "context" "fmt" - "github.com/edup2p/common/types" - "github.com/edup2p/common/types/ifaces" - "github.com/edup2p/common/types/msgactor" "log/slog" "net/netip" "sort" "sync/atomic" + + "github.com/edup2p/common/types" + "github.com/edup2p/common/types/ifaces" + "github.com/edup2p/common/types/msgactor" ) // RunCheck ensures that only one instance of the actor is running at all times. diff --git a/toversok/actors/util_test.go b/toversok/actors/util_test.go index b2c6a11..473cf6f 100644 --- a/toversok/actors/util_test.go +++ b/toversok/actors/util_test.go @@ -8,17 +8,23 @@ import ( ) // Test constants -const assertEventuallyTick = 1 * time.Millisecond -const assertEventuallyTimeout = 10 * assertEventuallyTick +const ( + assertEventuallyTick = 1 * time.Millisecond + assertEventuallyTimeout = 10 * assertEventuallyTick +) // Test variables -var dummyAddr = netip.IPv4Unspecified() -var dummyAddrPort = netip.AddrPortFrom(dummyAddr, 0) -var dummyKey key.NodePublic = [32]byte{0} +var ( + dummyAddr = netip.IPv4Unspecified() + dummyAddrPort = netip.AddrPortFrom(dummyAddr, 0) + dummyKey key.NodePublic = [32]byte{0} +) // Test session -var testPriv = key.NewSession() -var testPub = testPriv.Public() +var ( + testPriv = key.NewSession() + testPub = testPriv.Public() +) func getTestPriv() *key.SessionPrivate { return &testPriv diff --git a/toversok/control_conn.go b/toversok/control_conn.go index eb40566..f21b5be 100644 --- a/toversok/control_conn.go +++ b/toversok/control_conn.go @@ -4,6 +4,11 @@ import ( "context" "errors" "fmt" + "log/slog" + "net/netip" + "sync" + "time" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/control" "github.com/edup2p/common/types/control/controlhttp" @@ -12,10 +17,6 @@ import ( "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgcontrol" "golang.org/x/exp/maps" - "log/slog" - "net/netip" - "sync" - "time" ) type DefaultControlHost struct { @@ -107,7 +108,6 @@ func CreateControlSession(ctx context.Context, opts dial.Opts, controlKey key.Co } func (rcs *ResumableControlSession) Run() { - go func() { <-rcs.ctx.Done() @@ -130,7 +130,6 @@ func (rcs *ResumableControlSession) Run() { } err := rcs.FlushOut() - if err != nil { slog.Warn("control connection errored while flushing out", "err", err) @@ -176,7 +175,7 @@ func (rcs *ResumableControlSession) Run() { absenceStart := time.Now() - var session = &rcs.session + session := &rcs.session var err error var client *control.Client @@ -193,8 +192,8 @@ func (rcs *ResumableControlSession) Run() { clientCtx, rcs.clientOpts, rcs.getPriv, rcs.getSess, rcs.controlKey, session, nil, ) - var r = msgcontrol.NoRetryStrategy - var retry = &r + r := msgcontrol.NoRetryStrategy + retry := &r if err != nil { if errors.As(err, retry) { @@ -270,7 +269,6 @@ func (rcs *ResumableControlSession) Handle(msg msgcontrol.ControlMessage) error default: return fmt.Errorf("got unexpected message from control: %v", msg) } - } func (rcs *ResumableControlSession) CallbacksReady() bool { diff --git a/toversok/engine.go b/toversok/engine.go index d0a78ca..606af7b 100644 --- a/toversok/engine.go +++ b/toversok/engine.go @@ -4,14 +4,15 @@ import ( "context" "errors" "fmt" - "github.com/edup2p/common/types" - "github.com/edup2p/common/types/ifaces" - "github.com/edup2p/common/types/key" "log/slog" "net" "net/netip" "sync" "time" + + "github.com/edup2p/common/types" + "github.com/edup2p/common/types/ifaces" + "github.com/edup2p/common/types/key" ) // Engine is the main and most high-level object for any client implementation. @@ -325,7 +326,6 @@ func (e *Engine) getNodePriv() *key.NodePrivate { func (e *Engine) getExtConn() types.UDPConn { if e.extBind == nil || e.extBind.Closed { conn, err := e.bindExt() - if err != nil { panic(fmt.Sprintf("could not bind ext: %s", err)) } diff --git a/toversok/events.go b/toversok/events.go index f4f72ed..d5aa008 100644 --- a/toversok/events.go +++ b/toversok/events.go @@ -1,10 +1,11 @@ package toversok import ( + "net/netip" + "github.com/LukaGiorgadze/gonull" "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/relay" - "net/netip" ) // TODO DEPRECATED, should be refactored into using fake control client and such diff --git a/toversok/interface.go b/toversok/interface.go index 3c0e260..8f6168d 100644 --- a/toversok/interface.go +++ b/toversok/interface.go @@ -2,11 +2,12 @@ package toversok import ( "context" + "net/netip" + "time" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/ifaces" "github.com/edup2p/common/types/key" - "net/netip" - "time" ) // PeerCfg isa a peer config update struct, all values are nullable through being pointers. diff --git a/toversok/session.go b/toversok/session.go index e8a6412..5c37f69 100644 --- a/toversok/session.go +++ b/toversok/session.go @@ -3,15 +3,16 @@ package toversok import ( "context" "fmt" + "log/slog" + "net/netip" + "sync" + "github.com/edup2p/common/toversok/actors" "github.com/edup2p/common/types" "github.com/edup2p/common/types/ifaces" "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgcontrol" "github.com/edup2p/common/types/relay" - "log/slog" - "net/netip" - "sync" ) // Session represents one single session; a session key is generated here, and used inside a Stage diff --git a/types/control/client.go b/types/control/client.go index 55d3831..89ef562 100644 --- a/types/control/client.go +++ b/types/control/client.go @@ -5,12 +5,13 @@ import ( "context" "errors" "fmt" - "github.com/edup2p/common/types" - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/msgcontrol" "log/slog" "net/netip" "time" + + "github.com/edup2p/common/types" + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/msgcontrol" ) type Client struct { @@ -52,7 +53,6 @@ func EstablishClient(parentCtx context.Context, mc types.MetaConn, brw *bufio.Re } func (c *Client) Handshake(timeout time.Duration, logon types.LogonCallback) error { - // TODO // 1. send ClientHello // 2. expect ServerHello diff --git a/types/control/conn.go b/types/control/conn.go index 828b318..b339461 100644 --- a/types/control/conn.go +++ b/types/control/conn.go @@ -6,13 +6,14 @@ import ( "encoding/json" "errors" "fmt" - "github.com/edup2p/common/types" - "github.com/edup2p/common/types/msgcontrol" "io" "log/slog" "os" "sync" "time" + + "github.com/edup2p/common/types" + "github.com/edup2p/common/types/msgcontrol" ) type Conn struct { @@ -37,7 +38,6 @@ func NewConn(ctx context.Context, mc types.MetaConn, brw *bufio.ReadWriter) *Con } func (c *Conn) UnmarshalInto(data []byte, to msgcontrol.ControlMessage) error { - if err := json.Unmarshal(data, to); err != nil { return fmt.Errorf("failed to unmarshal data: %w", err) } @@ -50,7 +50,6 @@ func (c *Conn) Expect(to msgcontrol.ControlMessage, ttfbTimeout time.Duration) e defer c.readMutex.Unlock() msgTyp, data, err := c.readRawMessageLocked(ttfbTimeout) - if err != nil { return fmt.Errorf("failed reading message: %w", err) } diff --git a/types/control/controlhttp/http_client.go b/types/control/controlhttp/http_client.go index 46e7803..b1e36c9 100644 --- a/types/control/controlhttp/http_client.go +++ b/types/control/controlhttp/http_client.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "fmt" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/control" "github.com/edup2p/common/types/dial" diff --git a/types/control/controlhttp/http_server.go b/types/control/controlhttp/http_server.go index 833f12c..9930bc3 100644 --- a/types/control/controlhttp/http_server.go +++ b/types/control/controlhttp/http_server.go @@ -1,9 +1,10 @@ package controlhttp import ( + "net/http" + "github.com/edup2p/common/types/control" "github.com/edup2p/common/types/dial" - "net/http" ) func ServerHandler(s *control.Server) http.Handler { diff --git a/types/control/graph.go b/types/control/graph.go index ccffa3d..821378a 100644 --- a/types/control/graph.go +++ b/types/control/graph.go @@ -2,9 +2,10 @@ package control import ( "errors" + "sync" + "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgcontrol" - "sync" ) type EdgeGraph struct { diff --git a/types/control/iface.go b/types/control/iface.go index e46f81f..5fc6b52 100644 --- a/types/control/iface.go +++ b/types/control/iface.go @@ -2,12 +2,15 @@ package control import ( "errors" - "github.com/edup2p/common/types/key" "net/netip" + + "github.com/edup2p/common/types/key" ) -type ClientID key.NodePublic -type SessID string +type ( + ClientID key.NodePublic + SessID string +) var ErrSessionDoesNotExist = errors.New("session does not exist") diff --git a/types/control/logic.go b/types/control/logic.go index 644bea8..0f90a1e 100644 --- a/types/control/logic.go +++ b/types/control/logic.go @@ -2,6 +2,7 @@ package control import ( "errors" + "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgcontrol" ) diff --git a/types/control/server.go b/types/control/server.go index 423193c..6f3129b 100644 --- a/types/control/server.go +++ b/types/control/server.go @@ -6,17 +6,18 @@ import ( "crypto/rand" "errors" "fmt" - "github.com/edup2p/common/types" - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/msgcontrol" - "github.com/edup2p/common/types/relay" - stunserver "github.com/edup2p/common/types/stun" "log/slog" "net" "net/netip" "slices" "sync" "time" + + "github.com/edup2p/common/types" + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/msgcontrol" + "github.com/edup2p/common/types/relay" + stunserver "github.com/edup2p/common/types/stun" ) type Server struct { @@ -147,13 +148,11 @@ func (s *Server) Accept(ctx context.Context, mc types.MetaConn, brw *bufio.ReadW // TODO set deadline on read clientHello, logon, err := s.handleLogon(cc) - if err != nil { return fmt.Errorf("handle logon: %w", err) } sess, resumed, err := s.ReEstablishOrMakeSession(cc, clientHello.ClientNodePub, logon.SessKey, logon.ResumeSessionID) - if err != nil { return s.doReject(cc, sess, err) } @@ -200,7 +199,7 @@ func (s *Server) Accept(ctx context.Context, mc types.MetaConn, brw *bufio.ReadW func (s *Server) handleLogon(cc *Conn) (*msgcontrol.ClientHello, *msgcontrol.Logon, error) { // TODO set deadline on read - var clientHello = new(msgcontrol.ClientHello) + clientHello := new(msgcontrol.ClientHello) if err := cc.Expect(clientHello, HandshakeReceiveTimeout); err != nil { return nil, nil, fmt.Errorf("error when receiving clienthello: %w", err) } @@ -248,7 +247,6 @@ func (s *Server) handleLogon(cc *Conn) (*msgcontrol.ClientHello, *msgcontrol.Log } func (s *Server) doReject(cc *Conn, sess *ServerSession, err error) error { - reject := &msgcontrol.LogonReject{} switch { @@ -301,7 +299,7 @@ func NewServer(privKey key.ControlPrivate, relays []relay.Information) *Server { sessLock: sync.RWMutex{}, sessByNode: make(map[key.NodePublic]*ServerSession), sessByID: make(map[string]*ServerSession), - //getIPs: getIPs, + // getIPs: getIPs, relays: relays, vGraph: NewEdgeGraph(), pendingLock: sync.Mutex{}, @@ -493,7 +491,6 @@ func (s *Server) RemoveSession(sess *ServerSession) { return nil }) - if err != nil { slog.Error("failed to remove sessions", "err", err) } diff --git a/types/control/server_session.go b/types/control/server_session.go index d42959d..e78c402 100644 --- a/types/control/server_session.go +++ b/types/control/server_session.go @@ -4,14 +4,15 @@ import ( "context" "errors" "fmt" - "github.com/edup2p/common/types" - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/msgcontrol" "log/slog" "net/netip" "os" "sync" "time" + + "github.com/edup2p/common/types" + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/msgcontrol" ) type ServerSession struct { @@ -92,7 +93,6 @@ func (s *ServerSession) doAuthenticate(resumed bool) error { for ctx.Err() == nil { msg := msgcontrol.LogonDeviceKey{} err := s.conn.Expect(&msg, time.Millisecond*100) - if err != nil { if errors.Is(err, os.ErrDeadlineExceeded) { continue @@ -132,7 +132,6 @@ func (s *ServerSession) doAuthenticate(resumed bool) error { switch msg := authMsg.(type) { case RejectAuth: err := s.conn.Write(msg.LogonReject) - if err != nil { return fmt.Errorf("error while writing logon reject: %w, %w", err, ErrLogonRejected) } @@ -319,7 +318,6 @@ func (s *ServerSession) AuthAndStart() error { s.IPv4, s.IPv6 = s.server.callbacks.OnSessionFinalize(SessID(s.ID), ClientID(s.Peer)) err := s.AuthenticateAccept() - if err != nil { return fmt.Errorf("error while writing logon accept: %w", err) } @@ -384,7 +382,6 @@ func (s *ServerSession) Run() { return nil }) - if err != nil { err = fmt.Errorf("could not send greets: %w", err) return @@ -396,7 +393,6 @@ func (s *ServerSession) Run() { var m msgcontrol.ControlMessage m, err = s.conn.Read(0) - if err != nil { // TODO this currently removes the session on connection break; no resuming diff --git a/types/dial/http.go b/types/dial/http.go index b94d611..565b02e 100644 --- a/types/dial/http.go +++ b/types/dial/http.go @@ -4,10 +4,11 @@ import ( "bufio" "context" "fmt" - "github.com/edup2p/common/types" "io" "net/http" "time" + + "github.com/edup2p/common/types" ) // getPriv func() *key.NodePrivate, getSess func() *key.SessionPrivate, controlKey key.NodePublic @@ -55,7 +56,6 @@ func HTTP[T any](ctx context.Context, opts Opts, url, protocol string, makeClien // At this point, we're speaking the protocol with the server. c, err := makeClient(ctx, netConn, brw, opts) - if err != nil { netConn.Close() return nil, fmt.Errorf("failed to establish client: %w", err) diff --git a/types/dial/http_server.go b/types/dial/http_server.go index 5c07e6f..d6314e7 100644 --- a/types/dial/http_server.go +++ b/types/dial/http_server.go @@ -4,11 +4,12 @@ import ( "bufio" "context" "fmt" - "github.com/edup2p/common/types" "log/slog" "net/http" "net/netip" "strings" + + "github.com/edup2p/common/types" ) type ProtocolServer interface { diff --git a/types/dial/tcp.go b/types/dial/tcp.go index c1dc77e..d8bc280 100644 --- a/types/dial/tcp.go +++ b/types/dial/tcp.go @@ -13,7 +13,6 @@ import ( // WithTLS does a "full" dial, including TLS wrapping and CN checking func WithTLS(ctx context.Context, opts Opts) (net.Conn, error) { netConn, err := TCP(ctx, opts) - if err != nil { return nil, fmt.Errorf("tcp dial failed: %w", err) } diff --git a/types/ifaces/actor.go b/types/ifaces/actor.go index 9736341..6006c55 100644 --- a/types/ifaces/actor.go +++ b/types/ifaces/actor.go @@ -1,12 +1,13 @@ package ifaces import ( + "net/netip" + "time" + "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgactor" "github.com/edup2p/common/types/msgsess" "github.com/edup2p/common/types/stage" - "net/netip" - "time" ) type Actor interface { diff --git a/types/ifaces/control.go b/types/ifaces/control.go index 81be825..3c43dff 100644 --- a/types/ifaces/control.go +++ b/types/ifaces/control.go @@ -1,10 +1,11 @@ package ifaces import ( + "net/netip" + "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgcontrol" "github.com/edup2p/common/types/relay" - "net/netip" ) // ControlCallbacks are the possible updates that the control server wishes to inform the client about. diff --git a/types/ifaces/stage.go b/types/ifaces/stage.go index 49c6119..1f0268f 100644 --- a/types/ifaces/stage.go +++ b/types/ifaces/stage.go @@ -1,9 +1,10 @@ package ifaces import ( + "net/netip" + "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/stage" - "net/netip" ) // Stage documents/iterates the functions a Stage should expose diff --git a/types/key/control_priv.go b/types/key/control_priv.go index c14cfa1..3dcc5f4 100644 --- a/types/key/control_priv.go +++ b/types/key/control_priv.go @@ -2,6 +2,7 @@ package key import ( "crypto/subtle" + "github.com/edup2p/common/types" "go4.org/mem" "golang.org/x/crypto/curve25519" diff --git a/types/key/control_pub.go b/types/key/control_pub.go index a83a726..19785e4 100644 --- a/types/key/control_pub.go +++ b/types/key/control_pub.go @@ -4,8 +4,9 @@ import ( "encoding/hex" "encoding/json" "fmt" - "go4.org/mem" "strings" + + "go4.org/mem" ) type ControlPublic NakedKey diff --git a/types/key/node_priv.go b/types/key/node_priv.go index 6d10ee4..f958882 100644 --- a/types/key/node_priv.go +++ b/types/key/node_priv.go @@ -4,10 +4,11 @@ import ( "crypto/subtle" "encoding/json" "fmt" + "strings" + "github.com/edup2p/common/types" "go4.org/mem" "golang.org/x/crypto/curve25519" - "strings" ) type NodePrivate struct { diff --git a/types/key/node_pub.go b/types/key/node_pub.go index 9a81a2e..cb9f252 100644 --- a/types/key/node_pub.go +++ b/types/key/node_pub.go @@ -4,8 +4,9 @@ import ( "encoding/hex" "encoding/json" "fmt" - "go4.org/mem" "strings" + + "go4.org/mem" ) type NodePublic NakedKey diff --git a/types/key/session_priv.go b/types/key/session_priv.go index 81d2281..64b1443 100644 --- a/types/key/session_priv.go +++ b/types/key/session_priv.go @@ -2,6 +2,7 @@ package key import ( "crypto/subtle" + "github.com/edup2p/common/types" "golang.org/x/crypto/curve25519" "golang.org/x/crypto/nacl/box" diff --git a/types/key/session_pub.go b/types/key/session_pub.go index 693ecdc..9d18437 100644 --- a/types/key/session_pub.go +++ b/types/key/session_pub.go @@ -3,6 +3,7 @@ package key import ( "encoding/hex" "fmt" + "go4.org/mem" ) diff --git a/types/key/session_shared.go b/types/key/session_shared.go index 6d9d7c1..fae4bee 100644 --- a/types/key/session_shared.go +++ b/types/key/session_shared.go @@ -2,6 +2,7 @@ package key import ( "crypto/subtle" + "github.com/edup2p/common/types" "golang.org/x/crypto/nacl/box" ) diff --git a/types/key/util.go b/types/key/util.go index aa9c724..d101fec 100644 --- a/types/key/util.go +++ b/types/key/util.go @@ -5,10 +5,11 @@ import ( "encoding/hex" "errors" "fmt" - "go4.org/mem" - "golang.org/x/crypto/nacl/box" "io" "slices" + + "go4.org/mem" + "golang.org/x/crypto/nacl/box" ) // rand fills b with cryptographically strong random bytes. Panics if diff --git a/types/misc.go b/types/misc.go index efe8cf1..6899a8c 100644 --- a/types/misc.go +++ b/types/misc.go @@ -6,10 +6,11 @@ import ( "context" "crypto/rand" "encoding/hex" - "golang.org/x/exp/maps" "log/slog" "net/netip" "strings" + + "golang.org/x/exp/maps" ) // Incomparable is a zero-width incomparable type. If added as the diff --git a/types/msgactor/msg.go b/types/msgactor/msg.go index 7d47cc3..5cef33a 100644 --- a/types/msgactor/msg.go +++ b/types/msgactor/msg.go @@ -1,11 +1,12 @@ package msgactor import ( + "net/netip" + "time" + "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgsess" "github.com/edup2p/common/types/relay" - "net/netip" - "time" ) // Messages diff --git a/types/msgactor/notif.go b/types/msgactor/notif.go index 54c36a4..b646fdd 100644 --- a/types/msgactor/notif.go +++ b/types/msgactor/notif.go @@ -1,8 +1,9 @@ package msgactor import ( - "github.com/edup2p/common/types/key" "net/netip" + + "github.com/edup2p/common/types/key" ) type PeerState byte diff --git a/types/msgcontrol/msg.go b/types/msgcontrol/msg.go index 5e0292b..8b13a81 100644 --- a/types/msgcontrol/msg.go +++ b/types/msgcontrol/msg.go @@ -1,10 +1,11 @@ package msgcontrol import ( - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/relay" "net/netip" "time" + + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/relay" ) type ControlMessageType byte diff --git a/types/msgcontrol/msg_iface.go b/types/msgcontrol/msg_iface.go index b429257..b246f00 100644 --- a/types/msgcontrol/msg_iface.go +++ b/types/msgcontrol/msg_iface.go @@ -7,30 +7,39 @@ type ControlMessage interface { func (c *ClientHello) CMsgType() ControlMessageType { return ClientHelloType } + func (c *ServerHello) CMsgType() ControlMessageType { return ServerHelloType } + func (c *Logon) CMsgType() ControlMessageType { return LogonType } + func (c *LogonAuthenticate) CMsgType() ControlMessageType { return LogonAuthenticateType } + func (c *LogonDeviceKey) CMsgType() ControlMessageType { return LogonDeviceKeyType } + func (c *LogonAccept) CMsgType() ControlMessageType { return LogonAcceptType } + func (c *LogonReject) CMsgType() ControlMessageType { return LogonRejectType } + func (c *Logout) CMsgType() ControlMessageType { return LogoutType } + func (c *Ping) CMsgType() ControlMessageType { return PingType } + func (c *Pong) CMsgType() ControlMessageType { return PongType } @@ -38,18 +47,23 @@ func (c *Pong) CMsgType() ControlMessageType { func (c *EndpointUpdate) CMsgType() ControlMessageType { return EndpointUpdateType } + func (c *HomeRelayUpdate) CMsgType() ControlMessageType { return HomeRelayUpdateType } + func (c *PeerAddition) CMsgType() ControlMessageType { return PeerAdditionType } + func (c *PeerUpdate) CMsgType() ControlMessageType { return PeerUpdateType } + func (c *PeerRemove) CMsgType() ControlMessageType { return PeerRemoveType } + func (c *RelayUpdate) CMsgType() ControlMessageType { return RelayUpdateType } diff --git a/types/msgsess/parsing.go b/types/msgsess/parsing.go index 01daa6f..66bf518 100644 --- a/types/msgsess/parsing.go +++ b/types/msgsess/parsing.go @@ -3,9 +3,10 @@ package msgsess import ( "errors" "fmt" + "net/netip" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/key" - "net/netip" ) // Session Wire header: diff --git a/types/msgsess/ping.go b/types/msgsess/ping.go index d28de62..33e9b8b 100644 --- a/types/msgsess/ping.go +++ b/types/msgsess/ping.go @@ -3,8 +3,9 @@ package msgsess import ( crand "crypto/rand" "fmt" - "github.com/edup2p/common/types/key" "slices" + + "github.com/edup2p/common/types/key" ) type TxID [12]byte diff --git a/types/msgsess/pong.go b/types/msgsess/pong.go index b011c33..fa027f4 100644 --- a/types/msgsess/pong.go +++ b/types/msgsess/pong.go @@ -2,9 +2,10 @@ package msgsess import ( "fmt" - "github.com/edup2p/common/types" "net/netip" "slices" + + "github.com/edup2p/common/types" ) type Pong struct { diff --git a/types/msgsess/rendezvous.go b/types/msgsess/rendezvous.go index b4d7d56..e5640f7 100644 --- a/types/msgsess/rendezvous.go +++ b/types/msgsess/rendezvous.go @@ -2,9 +2,10 @@ package msgsess import ( "fmt" - "github.com/edup2p/common/types" "net/netip" "slices" + + "github.com/edup2p/common/types" ) type Rendezvous struct { diff --git a/types/relay/client.go b/types/relay/client.go index 9399aed..35ec18a 100644 --- a/types/relay/client.go +++ b/types/relay/client.go @@ -6,14 +6,15 @@ import ( "encoding/json" "errors" "fmt" - "github.com/edup2p/common/types" - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/msgsess" "io" "log/slog" "slices" "sync" "time" + + "github.com/edup2p/common/types" + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/msgsess" ) const ( @@ -187,7 +188,6 @@ func (c *Client) recvServerKey() error { var buf [32]byte _, err = io.ReadFull(c.reader, buf[:]) - if err != nil { return err } @@ -238,7 +238,7 @@ func (c *Client) recvServerInfo() (*ServerInfo, error) { return nil, errPacketTooLarge } - var msgbox = make([]byte, frLen) + msgbox := make([]byte, frLen) if _, err = io.ReadFull(c.reader, msgbox); err != nil { return nil, err diff --git a/types/relay/frame.go b/types/relay/frame.go index 6410772..43e9510 100644 --- a/types/relay/frame.go +++ b/types/relay/frame.go @@ -2,6 +2,7 @@ package relay import ( "bufio" + "github.com/edup2p/common/types" ) diff --git a/types/relay/info.go b/types/relay/info.go index 3a72e9c..0f29767 100644 --- a/types/relay/info.go +++ b/types/relay/info.go @@ -1,8 +1,9 @@ package relay import ( - "github.com/edup2p/common/types/key" "net/netip" + + "github.com/edup2p/common/types/key" ) type Information struct { diff --git a/types/relay/relayhttp/http_client.go b/types/relay/relayhttp/http_client.go index c2d72e0..5c28d34 100644 --- a/types/relay/relayhttp/http_client.go +++ b/types/relay/relayhttp/http_client.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "fmt" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/dial" "github.com/edup2p/common/types/key" diff --git a/types/relay/relayhttp/http_server.go b/types/relay/relayhttp/http_server.go index d557f47..4389d0e 100644 --- a/types/relay/relayhttp/http_server.go +++ b/types/relay/relayhttp/http_server.go @@ -1,9 +1,10 @@ package relayhttp import ( + "net/http" + "github.com/edup2p/common/types/dial" "github.com/edup2p/common/types/relay" - "net/http" ) func ServerHandler(s *relay.Server) http.Handler { diff --git a/types/relay/server.go b/types/relay/server.go index ef5ddbc..d2d533e 100644 --- a/types/relay/server.go +++ b/types/relay/server.go @@ -6,14 +6,15 @@ import ( "encoding/json" "errors" "fmt" - "github.com/edup2p/common/types" - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/msgsess" "io" "log/slog" "net/netip" "sync" "time" + + "github.com/edup2p/common/types" + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/msgsess" ) type Server struct { @@ -61,7 +62,6 @@ func (s *Server) sendServerKey(writer *bufio.Writer) (err error) { pKey := s.PublicKey() _, err = writer.Write(pKey[:]) - if err != nil { return } @@ -212,7 +212,6 @@ func (s *Server) getClient(peer key.NodePublic) *ServerClient { } func (s *Server) registerClient(client *ServerClient) { - // Check if there's a client active on this key already. if sc := s.getClient(client.nodeKey); sc != nil { // Just cancel the old connected client. diff --git a/types/relay/serverclient.go b/types/relay/serverclient.go index d0a46b5..d9d08c8 100644 --- a/types/relay/serverclient.go +++ b/types/relay/serverclient.go @@ -5,14 +5,15 @@ import ( "context" "errors" "fmt" - "github.com/edup2p/common/types" - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/msgsess" "io" "log/slog" "math/rand" "net/netip" "time" + + "github.com/edup2p/common/types" + "github.com/edup2p/common/types/key" + "github.com/edup2p/common/types/msgsess" ) // ServerPacket is a transient packet type handled by the server @@ -113,7 +114,6 @@ func (sc *ServerClient) RunReceiver() { for { frType, frLen, err := readFrameHeader(sc.buffReader) - if err != nil { if errors.Is(err, io.EOF) { sc.ccc(fmt.Errorf("reader: read EOF")) @@ -147,7 +147,6 @@ func (sc *ServerClient) RunReceiver() { } func (sc *ServerClient) handleSend(frLen uint32) error { - dstKey, contents, err := sc.readSend(frLen) if err != nil { return err diff --git a/types/stage/stage.go b/types/stage/stage.go index b32779e..617d90b 100644 --- a/types/stage/stage.go +++ b/types/stage/stage.go @@ -2,9 +2,10 @@ package stage import ( - "github.com/edup2p/common/types/key" "net/netip" "time" + + "github.com/edup2p/common/types/key" ) type SentPing struct { diff --git a/types/stun/response.go b/types/stun/response.go index 3aad864..5a761ad 100644 --- a/types/stun/response.go +++ b/types/stun/response.go @@ -90,7 +90,6 @@ func ParseResponse(b []byte) (tID TxID, addr netip.AddrPort, err error) { } } return nil - }); err != nil { return TxID{}, netip.AddrPort{}, err } diff --git a/types/stun/server.go b/types/stun/server.go index 44c7e4e..1b05f6f 100644 --- a/types/stun/server.go +++ b/types/stun/server.go @@ -51,7 +51,6 @@ func (s *Server) Serve() error { ) for { n, ua, err = s.pc.ReadFromUDP(buf[:]) - if err != nil { if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { return nil diff --git a/usrwg/bind.go b/usrwg/bind.go index d7a5642..478c09a 100644 --- a/usrwg/bind.go +++ b/usrwg/bind.go @@ -4,15 +4,16 @@ import ( "context" "errors" "fmt" - "github.com/edup2p/common/types" - "github.com/edup2p/common/types/key" - "golang.org/x/exp/maps" - "golang.zx2c4.com/wireguard/conn" "log/slog" "reflect" "runtime" "sync" "time" + + "github.com/edup2p/common/types" + "github.com/edup2p/common/types/key" + "golang.org/x/exp/maps" + "golang.zx2c4.com/wireguard/conn" ) type ToverSokBind struct { @@ -164,7 +165,7 @@ func (b *ToverSokBind) waitForValueFromConns() ([]byte, *endpoint) { } func (b *ToverSokBind) buildConnsSelectCaseMap() map[key.NodePublic]reflect.SelectCase { - var cases = make(map[key.NodePublic]reflect.SelectCase) + cases := make(map[key.NodePublic]reflect.SelectCase) b.connMu.RLock() defer b.connMu.RUnlock() @@ -220,7 +221,6 @@ func (b *ToverSokBind) Send(bufs [][]byte, ep conn.Endpoint) error { func (b *ToverSokBind) ParseEndpoint(s string) (conn.Endpoint, error) { np, err := key.UnmarshalPublic(s) - if err != nil { return nil, fmt.Errorf("failed to unmarshal nodepublic: %w", err) } diff --git a/usrwg/channel_conn.go b/usrwg/channel_conn.go index b149f26..ece9500 100644 --- a/usrwg/channel_conn.go +++ b/usrwg/channel_conn.go @@ -105,7 +105,6 @@ func (cc *ChannelConn) getOut(d time.Duration) (pkt []byte) { // nolint:unused // // Will return false on timeout. func (cc *ChannelConn) putIn(pkt []byte, d time.Duration) (ok bool) { - select { case cc.incoming <- pkt: ok = true diff --git a/usrwg/endpoint.go b/usrwg/endpoint.go index bb90fcf..dce21d2 100644 --- a/usrwg/endpoint.go +++ b/usrwg/endpoint.go @@ -1,9 +1,10 @@ package usrwg import ( - "github.com/edup2p/common/types/key" "net/netip" "slices" + + "github.com/edup2p/common/types/key" ) type endpoint struct { diff --git a/usrwg/router/router_bsd.go b/usrwg/router/router_bsd.go index a1d520f..2b47d50 100644 --- a/usrwg/router/router_bsd.go +++ b/usrwg/router/router_bsd.go @@ -4,16 +4,16 @@ package router import ( "fmt" - "go4.org/netipx" - "golang.zx2c4.com/wireguard/tun" "log/slog" "net/netip" "runtime" + + "go4.org/netipx" + "golang.zx2c4.com/wireguard/tun" ) func NewRouter(device tun.Device) (Router, error) { name, err := device.Name() - if err != nil { return nil, err } @@ -109,9 +109,11 @@ func (r *bsdRouter) addRoute(prefix netip.Prefix) error { nip := net.IP.Mask(net.Mask) nstr := fmt.Sprintf("%v/%d", nip, prefix.Bits()) - args := []string{"route", "-q", "-n", + args := []string{ + "route", "-q", "-n", "add", "-" + inet(prefix), nstr, - "-iface", r.tunName} + "-iface", r.tunName, + } if out, err := cmd(args...).CombinedOutput(); err != nil { return fmt.Errorf("route add failed: %v => %w\n%s", args, err, out) @@ -129,9 +131,11 @@ func (r *bsdRouter) removeRoute(prefix netip.Prefix) error { if runtime.GOOS == "darwin" { del = "delete" } - routedel := []string{"route", "-q", "-n", + routedel := []string{ + "route", "-q", "-n", del, "-" + inet(prefix), nstr, - "-iface", r.tunName} + "-iface", r.tunName, + } if out, err := cmd(routedel...).CombinedOutput(); err != nil { return fmt.Errorf("route del failed: %v: %w\n%s", routedel, err, out) diff --git a/usrwg/router/router_linux.go b/usrwg/router/router_linux.go index 263f8b8..71a2126 100644 --- a/usrwg/router/router_linux.go +++ b/usrwg/router/router_linux.go @@ -2,14 +2,14 @@ package router import ( "fmt" - "golang.zx2c4.com/wireguard/tun" "log/slog" "net/netip" + + "golang.zx2c4.com/wireguard/tun" ) func NewRouter(device tun.Device) (Router, error) { name, err := device.Name() - if err != nil { return nil, err } diff --git a/usrwg/router/router_windows.go b/usrwg/router/router_windows.go index 92ac234..aea0401 100644 --- a/usrwg/router/router_windows.go +++ b/usrwg/router/router_windows.go @@ -6,26 +6,26 @@ package router import ( "errors" "fmt" - "golang.org/x/sys/windows/svc" + "log" "log/slog" + "net/netip" "os" "os/exec" "path/filepath" + "slices" + "sort" "sync" "syscall" + "time" "github.com/dblohm7/wingoes/com" "github.com/edup2p/common/usrwg/router/winnet" "github.com/go-ole/go-ole" "go4.org/netipx" "golang.org/x/sys/windows" + "golang.org/x/sys/windows/svc" "golang.zx2c4.com/wireguard/tun" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" - "log" - "net/netip" - "slices" - "sort" - "time" ) func init() { @@ -55,7 +55,6 @@ func NewRouter(device tun.Device) (Router, error) { luid := winipcfg.LUID(nativeTun.LUID()) guid, err := luid.GUID() - if err != nil { return nil, fmt.Errorf("failed to get tun GUID: %w", err) } @@ -133,10 +132,10 @@ func (r *windowsRouter) Set(cfg *Config) (retErr error) { for i := 0; i < tries; i++ { found, err := setPrivateNetwork(r.luid) if err != nil { - //networkCategoryWarning.Set(fmt.Errorf("set-network-category: %w", err)) + // networkCategoryWarning.Set(fmt.Errorf("set-network-category: %w", err)) log.Printf("setPrivateNetwork(try=%d): %v", i, err) } else { - //networkCategoryWarning.Set(nil) + // networkCategoryWarning.Set(nil) if found { if i > 0 { log.Printf("setPrivateNetwork(try=%d): success", i) @@ -329,7 +328,7 @@ func (r *windowsRouter) Set(cfg *Config) (retErr error) { ipif6.UseAutomaticMetric = false ipif6.Metric = 0 } - //if mtu > 0 { + // if mtu > 0 { ipif6.NLMTU = uint32(r.mtu) //} ipif6.DadTransmits = 0 diff --git a/usrwg/router/winnet/winnet.go b/usrwg/router/winnet/winnet.go index 87ab967..fdfc2a2 100644 --- a/usrwg/router/winnet/winnet.go +++ b/usrwg/router/winnet/winnet.go @@ -16,8 +16,10 @@ import ( const CLSID_NetworkListManager = "{DCB00C01-570F-4A9B-8D69-199FDBA5723B}" -var IID_INetwork = ole.NewGUID("{8A40A45D-055C-4B62-ABD7-6D613E2CEAEC}") -var IID_INetworkConnection = ole.NewGUID("{DCB00005-570F-4A9B-8D69-199FDBA5723B}") +var ( + IID_INetwork = ole.NewGUID("{8A40A45D-055C-4B62-ABD7-6D613E2CEAEC}") + IID_INetworkConnection = ole.NewGUID("{DCB00005-570F-4A9B-8D69-199FDBA5723B}") +) type NetworkListManager struct { d *ole.Dispatch @@ -123,7 +125,6 @@ func (m *NetworkListManager) GetNetworkConnections() (ConnectionList, error) { cl = append(cl, nco) return nil }) - if err != nil { cl.Release() return nil, err diff --git a/usrwg/wgusp.go b/usrwg/wgusp.go index 9ba121e..23cb1c8 100644 --- a/usrwg/wgusp.go +++ b/usrwg/wgusp.go @@ -2,13 +2,14 @@ package usrwg import ( "fmt" + "log/slog" + "net/netip" + "github.com/edup2p/common/toversok" "github.com/edup2p/common/types" "github.com/edup2p/common/types/key" "github.com/edup2p/common/usrwg/router" "golang.zx2c4.com/wireguard/device" - "log/slog" - "net/netip" ) func init() { @@ -44,13 +45,11 @@ func (u *UserSpaceWireGuardHost) Controller(privateKey key.NodePrivate, addr4, a // TODO set this to 1392 per https://docs.eduvpn.org/server/v3/wireguard.html // and make adjustable by environment variable tunDev, err := createTUN(1280) - if err != nil { return nil, fmt.Errorf("failed to create TUN device: %w", err) } r, err := router.NewRouter(tunDev) - if err != nil { return nil, fmt.Errorf("failed to create router: %w", err) } @@ -126,7 +125,6 @@ func (u *UserSpaceWireGuardController) UpdatePeer(publicKey key.NodePublic, cfg publicKey.HexString(), cfg.VIPs.IPv4.String(), cfg.VIPs.IPv6.String(), publicKey.Marshal(), ), ) - if err != nil { err = fmt.Errorf("failed to do IPC set: %w", err) } From 550b5d569a95322f3088a16281d9400351e6f0d7 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 12:55:41 +0100 Subject: [PATCH 04/82] fix maps.Keys not being usable until go 1.23 --- toversok/actors/a_tman.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/toversok/actors/a_tman.go b/toversok/actors/a_tman.go index 78552a0..fa4bd7d 100644 --- a/toversok/actors/a_tman.go +++ b/toversok/actors/a_tman.go @@ -12,6 +12,7 @@ import ( "github.com/edup2p/common/types/msgactor" "github.com/edup2p/common/types/msgsess" "github.com/edup2p/common/types/stage" + xmaps "golang.org/x/exp/maps" ) type TrafficManager struct { @@ -152,7 +153,7 @@ func (tm *TrafficManager) Handle(m msgactor.ActorMessage) { func (tm *TrafficManager) DoStateTick() { // We explicitly range over a slice of the keys we already got, // since golang likes to complain when we mutate while we iterate. - for peer := range maps.Keys(tm.peerState) { + for _, peer := range xmaps.Keys(tm.peerState) { tm.forState(peer, func(s peerstate.PeerState) peerstate.PeerState { return s.OnTick() }) From 79b4451bc51012d9120de18dcc10cf832acd23fc Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 13:32:27 +0100 Subject: [PATCH 05/82] update some more dependencies --- go.mod | 9 ++++----- go.sum | 43 +++++++++++++++++++++++-------------------- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index f6eb0c5..92f24f9 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,12 @@ require ( github.com/abiosoft/ishell/v2 v2.0.2 github.com/dblohm7/wingoes v0.0.0-20240801171404-fc12d7c70140 github.com/go-ole/go-ole v1.3.0 - go.mongodb.org/mongo-driver v1.15.0 + github.com/stretchr/testify v1.9.0 go4.org/mem v0.0.0-20220726221520-4f986261bf13 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.17.0 + golang.org/x/crypto v0.33.0 golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f - golang.org/x/sys v0.15.0 + golang.org/x/sys v0.30.0 golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/windows v0.5.3 @@ -32,8 +32,7 @@ require ( github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/stretchr/testify v1.9.0 // indirect - golang.org/x/net v0.10.0 // indirect + golang.org/x/net v0.33.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 21910c9..be36655 100644 --- a/go.sum +++ b/go.sum @@ -6,7 +6,9 @@ github.com/abiosoft/ishell/v2 v2.0.2 h1:5qVfGiQISaYM8TkbBl7RFO6MddABoXpATrsFbVI+ github.com/abiosoft/ishell/v2 v2.0.2/go.mod h1:E4oTCXfo6QjoCart0QYa5m9w4S+deXs/P/9jA77A9Bs= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db h1:CjPUSXOiYptLbTdr1RceuZgSFDQ7U15ITERUGrUORx8= github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db/go.mod h1:rB3B4rKii8V21ydCbIzH5hZiCQE7f5E9SzUb/ZZx530= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -19,6 +21,8 @@ github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWT github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= @@ -33,43 +37,39 @@ github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/ github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= +github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/dblohm7/wingoes v0.0.0-20240801171404-fc12d7c70140/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU= -github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M= -github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= -github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= -github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.mongodb.org/mongo-driver v1.15.0 h1:rJCKC8eEliewXjZGf0ddURtl7tTVy1TK3bfl0gkUSLc= -go.mongodb.org/mongo-driver v1.15.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= +github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8= go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ= +golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo= @@ -78,7 +78,10 @@ golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 h1:CawjfCvY golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6/go.mod h1:3rxYc4HtVcSG9gVaTs2GEBdehh+sYPOwKtyUWEOTb80= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY= +gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0/go.mod h1:Dn5idtptoW1dIos9U6A2rpebLs/MtTwFacjKb8jLdQA= From 3fb6fd2d15b93c7a0538ab9c14e7125fcdbd04d0 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 13:32:49 +0100 Subject: [PATCH 06/82] make callbacks resilient to changes --- toversok/control_conn.go | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/toversok/control_conn.go b/toversok/control_conn.go index f21b5be..5a0c3de 100644 --- a/toversok/control_conn.go +++ b/toversok/control_conn.go @@ -67,7 +67,8 @@ type ResumableControlSession struct { // In to local msgInQueue []msgcontrol.ControlMessage - callbacks ifaces.ControlCallbacks + callbackLock sync.RWMutex + callbacks ifaces.ControlCallbacks } func CreateControlSession(ctx context.Context, opts dial.Opts, controlKey key.ControlPublic, getPriv func() *key.NodePrivate, getSess func() *key.SessionPrivate, logon types.LogonCallback) (*ResumableControlSession, error) { @@ -239,7 +240,7 @@ func (rcs *ResumableControlSession) Handle(msg msgcontrol.ControlMessage) error switch m := msg.(type) { case *msgcontrol.PeerAddition: rcs.knownPeers[m.PubKey] = true - return rcs.callbacks.AddPeer( + return rcs.ExpectCallbacks().AddPeer( m.PubKey, m.HomeRelay, m.Endpoints, @@ -254,7 +255,7 @@ func (rcs *ResumableControlSession) Handle(msg msgcontrol.ControlMessage) error endpoints = m.Endpoints } - return rcs.callbacks.UpdatePeer( + return rcs.ExpectCallbacks().UpdatePeer( m.PubKey, m.HomeRelay, endpoints, @@ -263,15 +264,18 @@ func (rcs *ResumableControlSession) Handle(msg msgcontrol.ControlMessage) error ) case *msgcontrol.PeerRemove: delete(rcs.knownPeers, m.PubKey) - return rcs.callbacks.RemovePeer(m.PubKey) + return rcs.ExpectCallbacks().RemovePeer(m.PubKey) case *msgcontrol.RelayUpdate: - return rcs.callbacks.UpdateRelays(m.Relays) + return rcs.ExpectCallbacks().UpdateRelays(m.Relays) default: return fmt.Errorf("got unexpected message from control: %v", msg) } } func (rcs *ResumableControlSession) CallbacksReady() bool { + rcs.callbackLock.RLock() + defer rcs.callbackLock.RUnlock() + return rcs.callbacks != nil } @@ -333,7 +337,21 @@ func (rcs *ResumableControlSession) IPv6() netip.Prefix { return rcs.ipv6 } +func (rcs *ResumableControlSession) ExpectCallbacks() ifaces.ControlCallbacks { + rcs.callbackLock.RLock() + defer rcs.callbackLock.RUnlock() + + if rcs.callbacks == nil { + panic("expected callbacks to be ready at this stage") + } + + return rcs.callbacks +} + func (rcs *ResumableControlSession) InstallCallbacks(callbacks ifaces.ControlCallbacks) { + rcs.callbackLock.Lock() + defer rcs.callbackLock.Unlock() + rcs.callbacks = callbacks } From 5ca74a9e8f98c8f6cc0c6ebcc9751b9df3287579 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 13:39:15 +0100 Subject: [PATCH 07/82] convert #32 into todo --- toversok/actors/a_direct.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/toversok/actors/a_direct.go b/toversok/actors/a_direct.go index e3bea43..e7eadc3 100644 --- a/toversok/actors/a_direct.go +++ b/toversok/actors/a_direct.go @@ -97,6 +97,8 @@ func (dm *DirectManager) WriteTo(pkt []byte, addr netip.AddrPort) { } } +// TODO: track when we last received a packet from AddrPair? + type DirectRouter struct { *ActorCommon From f0cec92d7e50e35b19f28c3cababe82656be304c Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 13:40:32 +0100 Subject: [PATCH 08/82] fix stray bad mass replace --- usrwg/router/router_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usrwg/router/router_windows.go b/usrwg/router/router_windows.go index aea0401..16e1c0a 100644 --- a/usrwg/router/router_windows.go +++ b/usrwg/router/router_windows.go @@ -869,7 +869,7 @@ func (ft *firewallTweaker) doAsyncSet() { needClear := !ft.known || len(ft.lastLocal) > 0 || len(val) == 0 ft.mu.Unlock() - err := ft.doSet(val, needclearMsg) + err := ft.doSet(val, needClear) if err != nil { slog.Warn("firewall: set failed: %v", err) } From a294f15d334366c27e0b1046847dcd8111e135ca Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 13:57:29 +0100 Subject: [PATCH 09/82] goland code inspection recommendations --- cmd/control_server/main.go | 1 + cmd/dev_client/main.go | 6 ++-- cmd/relay_server/main.go | 1 + extwg/wgctrl.go | 13 ++++++-- test_suite/README.md | 54 +++++++++++++++---------------- test_suite/control_server/main.go | 1 + test_suite/relay_server/main.go | 1 + toversok/actors/a_direct_test.go | 6 ++-- toversok/actors/a_relay_test.go | 6 ++-- toversok/actors/a_sockrecv.go | 7 ++-- toversok/actors/a_tman_test.go | 4 +-- types/control/client.go | 4 ++- types/control/server_session.go | 4 ++- types/dial/http.go | 15 ++++++--- types/dial/http_server.go | 16 ++++++--- types/dial/tcp.go | 11 ++++--- types/relay/client.go | 4 ++- types/stun/server.go | 16 +++++---- usrwg/bind.go | 15 +++++++-- 19 files changed, 118 insertions(+), 67 deletions(-) diff --git a/cmd/control_server/main.go b/cmd/control_server/main.go index 1290f2b..a75ee1c 100644 --- a/cmd/control_server/main.go +++ b/cmd/control_server/main.go @@ -489,6 +489,7 @@ func loadConfig() Config { return writeNewConfig() case err != nil: log.Fatal(err) + //goland:noinspection GoUnreachableCode panic("unreachable") default: var cfg Config diff --git a/cmd/dev_client/main.go b/cmd/dev_client/main.go index a35f7ab..0176404 100644 --- a/cmd/dev_client/main.go +++ b/cmd/dev_client/main.go @@ -406,7 +406,7 @@ func fcCmd() *ishell.Cmd { var ( err error peerKey *key.NodePublic - relay int64 + relayID int64 session key.SessionPublic ip4 netip.Addr ip6 netip.Addr @@ -418,7 +418,7 @@ func fcCmd() *ishell.Cmd { c.Err(err) return } - if relay, err = strconv.ParseInt(c.Args[1], 10, 64); err != nil { + if relayID, err = strconv.ParseInt(c.Args[1], 10, 64); err != nil { c.Err(err) return } @@ -458,7 +458,7 @@ func fcCmd() *ishell.Cmd { if err = fakeControl.addPeer(PeerDef{ Key: *peerKey, - HomeRelayID: relay, + HomeRelayID: relayID, SessionKey: session, Endpoints: endpoints, VIPs: toversok.VirtualIPs{ diff --git a/cmd/relay_server/main.go b/cmd/relay_server/main.go index 837040f..bc1668e 100644 --- a/cmd/relay_server/main.go +++ b/cmd/relay_server/main.go @@ -194,6 +194,7 @@ func loadConfig() Config { return writeNewConfig() case err != nil: log.Fatal(err) + //goland:noinspection GoUnreachableCode panic("unreachable") default: var cfg Config diff --git a/extwg/wgctrl.go b/extwg/wgctrl.go index ae3c997..c0e9706 100644 --- a/extwg/wgctrl.go +++ b/extwg/wgctrl.go @@ -1,6 +1,7 @@ package extwg import ( + "errors" "fmt" "log/slog" "net" @@ -54,8 +55,12 @@ func NewWGCtrl(client *wgctrl.Client, device string) *WGCtrl { } func (w *WGCtrl) Reset() error { + var errs []error + for _, m := range w.localMapping { - m.conn.Close() + if err := m.conn.Close(); err != nil { + errs = append(errs, err) + } } maps.Clear(w.localMapping) @@ -67,7 +72,11 @@ func (w *WGCtrl) Reset() error { ReplacePeers: true, Peers: []wgtypes.PeerConfig{}, }); err != nil { - return fmt.Errorf("error resetting wg device: %w", err) + errs = append(errs, fmt.Errorf("error resetting wg device: %w", err)) + } + + if len(errs) > 0 { + return fmt.Errorf("errors while wg device: %w", errors.Join(errs...)) } return nil diff --git a/test_suite/README.md b/test_suite/README.md index 30dcf1e..4b07ce3 100644 --- a/test_suite/README.md +++ b/test_suite/README.md @@ -551,8 +551,8 @@ establish a connection between peers are well-established This test suite uses the RFC 4787 [\[4\]](#ref-rfc4787) terminology, which does not categorize NAT into these four types. However, each of these four types of NAT described in RFC 3489 uses a different -combination of the NAT mapping and filtering behaviour described in RFC -4787. Below, the four types of NAT from RFC 3489 are listed, while +combination of the NAT mapping and filtering behaviour described in RFC 4787. +Below, the four types of NAT from RFC 3489 are listed, while noting the RFC 4787 mapping and filtering behaviour they are equivalent to: @@ -571,12 +571,12 @@ an ‘X’ if UDP hole punching is successful in the scenario where one peer is behind the NAT indicated by the cell’s row header, and the other peer is behind the NAT indicated by the cell’s column header. -| NAT Type | Full Cone | Restricted Cone | Port Restricted Cone | Symmetric | -|:---|:---|:---|:---|:---| -| **Full Cone** | X | X | X | X | -| **Restricted Cone** | X | X | X | X | -| **Port Restricted Cone** | X | X | X | | -| **Symmetric** | X | X | | | +| NAT Type | Full Cone | Restricted Cone | Port Restricted Cone | Symmetric | +|:-------------------------|:----------|:----------------|:---------------------|:----------| +| **Full Cone** | X | X | X | X | +| **Restricted Cone** | X | X | X | X | +| **Port Restricted Cone** | X | X | X | | +| **Symmetric** | X | X | | | As seen in the table, UDP hole punching succeeds unless one peer is behind a Port Restricted Cone NAT or Symmetric NAT, and the other peer @@ -685,12 +685,12 @@ of RFC 4787 NAT mapping and filtering behaviour. The results of repeating the UDP hole punching experiment with eduP2P are shown in the table below: -| NAT Type | Full Cone | Restricted Cone | Port Restricted Cone | Symmetric | -|:---|:---|:---|:---|:---| -| **Full Cone** | X | X | X | X | -| **Restricted Cone** | X | X | X | | -| **Port Restricted Cone** | X | X | X | | -| **Symmetric** | X | | | | +| NAT Type | Full Cone | Restricted Cone | Port Restricted Cone | Symmetric | +|:-------------------------|:----------|:----------------|:---------------------|:----------| +| **Full Cone** | X | X | X | X | +| **Restricted Cone** | X | X | X | | +| **Port Restricted Cone** | X | X | X | | +| **Symmetric** | X | | | | Comparing this table with the one in the previous section, we see that eduP2P is not able to establish a direct connection when one peer is @@ -983,17 +983,17 @@ The results of extending the UDP hole punching experiment to all combinations of RFC 4787 mapping (EIM, ADM, ADPM) and filtering (EIF, ADF, ADPF) behaviours are shown in the table below: -| NAT Type | EIM-EIF | EIM-ADF | EIM-ADPF | ADM-EIF | ADM-ADF | ADM-ADPF | ADPM-EIF | ADPM-ADF | ADPM-ADPF | -|:---|:---|:---|:---|:---|:---|:---|:---|:---|:---| -| **EIM-EIF** | X | X | X | X | X | X | X | X | X | -| **EIM-ADF** | X | X | X | X | X | X | X | X | X | -| **EIM-ADPF** | X | X | X | X | | | X | | | -| **ADM-EIF** | X | X | X | X | X | X | X | X | X | -| **ADM-ADF** | X | X | | X | | | X | | | -| **ADM-ADPF** | X | X | | X | | | X | | | -| **ADPM-EIF** | X | X | X | X | X | X | X | X | X | -| **ADPM-ADF** | X | X | | X | | | X | | | -| **ADPM-ADPF** | X | X | | X | | | X | | | +| NAT Type | EIM-EIF | EIM-ADF | EIM-ADPF | ADM-EIF | ADM-ADF | ADM-ADPF | ADPM-EIF | ADPM-ADF | ADPM-ADPF | +|:--------------|:--------|:--------|:---------|:--------|:--------|:---------|:---------|:---------|:----------| +| **EIM-EIF** | X | X | X | X | X | X | X | X | X | +| **EIM-ADF** | X | X | X | X | X | X | X | X | X | +| **EIM-ADPF** | X | X | X | X | | | X | | | +| **ADM-EIF** | X | X | X | X | X | X | X | X | X | +| **ADM-ADF** | X | X | | X | | | X | | | +| **ADM-ADPF** | X | X | | X | | | X | | | +| **ADPM-EIF** | X | X | X | X | X | X | X | X | X | +| **ADPM-ADF** | X | X | | X | | | X | | | +| **ADPM-ADPF** | X | X | | X | | | X | | | Based on these results, we can conclude that there are three (overlapping) types of NAT scenarios where the UDP hole punching process @@ -1319,8 +1319,8 @@ transfer a URL.” Available: J. Rosenberg, C. Huitema, R. Mahy, and J. Weinberger, “STUN - Simple Traversal of User Datagram Protocol (UDP) Through Network Address Translators -(NATs).” in Request for comments. RFC 3489; RFC Editor, Mar. -2003. doi: [10.17487/RFC3489](https://doi.org/10.17487/RFC3489). +(NATs).” in Request for comments. RFC 3489; RFC Editor, Mar. 2003. +doi: [10.17487/RFC3489](https://doi.org/10.17487/RFC3489). diff --git a/test_suite/control_server/main.go b/test_suite/control_server/main.go index e180a08..2fdd8d1 100644 --- a/test_suite/control_server/main.go +++ b/test_suite/control_server/main.go @@ -347,6 +347,7 @@ func loadConfig() Config { return writeNewConfig() case err != nil: log.Fatal(err) + //goland:noinspection GoUnreachableCode panic("unreachable") default: var cfg Config diff --git a/test_suite/relay_server/main.go b/test_suite/relay_server/main.go index a0ee18d..dbe376c 100644 --- a/test_suite/relay_server/main.go +++ b/test_suite/relay_server/main.go @@ -195,6 +195,7 @@ func loadConfig() Config { return writeNewConfig() case err != nil: log.Fatal(err) + //goland:noinspection GoUnreachableCode panic("unreachable") default: var cfg Config diff --git a/toversok/actors/a_direct_test.go b/toversok/actors/a_direct_test.go index a34897c..5fdf3b5 100644 --- a/toversok/actors/a_direct_test.go +++ b/toversok/actors/a_direct_test.go @@ -126,11 +126,11 @@ func TestDirectRouter(t *testing.T) { // For each peer: register peer in DirectRouter and Stage, and then send a message to their inConn for i, b := range []byte{1, 2} { ic := ics[i] - key := ic.peer + peer := ic.peer endpoint := peerEndpoints[i] - dr.setAKA(endpoint, key) - s.inConn[key] = ic + dr.setAKA(endpoint, peer) + s.inConn[peer] = ic frame := ifaces.DirectedPeerFrame{ SrcAddrPort: endpoint, diff --git a/toversok/actors/a_relay_test.go b/toversok/actors/a_relay_test.go index f231b44..3250907 100644 --- a/toversok/actors/a_relay_test.go +++ b/toversok/actors/a_relay_test.go @@ -106,13 +106,13 @@ func TestRelayRouter(t *testing.T) { // For each peer: register peer in RelayRouter and Stage, and then send a message to their inConn for i, b := range []byte{1, 2} { ic := ics[i] - key := ic.peer + peer := ic.peer - s.inConn[key] = ic + s.inConn[peer] = ic frame := ifaces.RelayedPeerFrame{ SrcRelay: 0, - SrcPeer: key, + SrcPeer: peer, Pkt: []byte{b}, } diff --git a/toversok/actors/a_sockrecv.go b/toversok/actors/a_sockrecv.go index 6f409a6..7e1cb79 100644 --- a/toversok/actors/a_sockrecv.go +++ b/toversok/actors/a_sockrecv.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "log/slog" "net" "net/netip" "slices" @@ -112,8 +113,10 @@ func (r *SockRecv) Run() { } func (r *SockRecv) Close() { - if context.Cause(r.ctx) == nil { - r.Conn.Close() + if r.ctx.Err() == nil { + if err := r.Conn.Close(); err != nil { + slog.Error("failed to close connection for sockrecv", "err", err) + } close(r.outCh) r.ctxCan() return diff --git a/toversok/actors/a_tman_test.go b/toversok/actors/a_tman_test.go index dcda7b8..f912019 100644 --- a/toversok/actors/a_tman_test.go +++ b/toversok/actors/a_tman_test.go @@ -19,9 +19,9 @@ func TestTrafficManager(t *testing.T) { wgConn := &MockUDPConn{} oc := MakeOutConn(wgConn, dummyKey, 0, s) - s.outConn = make(map[key.NodePublic]OutConnActor, 0) + s.outConn = make(map[key.NodePublic]OutConnActor) s.outConn[dummyKey] = oc - s.peerInfo = make(map[key.NodePublic]*stage.PeerInfo, 0) + s.peerInfo = make(map[key.NodePublic]*stage.PeerInfo) s.peerInfo[dummyKey] = &stage.PeerInfo{Session: testPub} // Run TrafficManager diff --git a/types/control/client.go b/types/control/client.go index 89ef562..86d00bb 100644 --- a/types/control/client.go +++ b/types/control/client.go @@ -221,5 +221,7 @@ func (c *Client) Recv(ttfbTimeout time.Duration) (msgcontrol.ControlMessage, err // } func (c *Client) Close() { - c.cc.mc.Close() + if err := c.cc.mc.Close(); err != nil { + slog.Error("error when closing control client", "err", err) + } } diff --git a/types/control/server_session.go b/types/control/server_session.go index e78c402..5892fab 100644 --- a/types/control/server_session.go +++ b/types/control/server_session.go @@ -340,7 +340,9 @@ func (s *ServerSession) Run() { s.server.RemoveSession(s) if s.conn != nil { - s.conn.mc.Close() + if err := s.conn.mc.Close(); err != nil { + slog.Error("failed to close metaconn", "err", err) + } } }() diff --git a/types/dial/http.go b/types/dial/http.go index 565b02e..b1193db 100644 --- a/types/dial/http.go +++ b/types/dial/http.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "io" + "log/slog" "net/http" "time" @@ -28,17 +29,23 @@ func HTTP[T any](ctx context.Context, opts Opts, url, protocol string, makeClien req.Header.Set("Upgrade", protocol) req.Header.Set("Connection", "Upgrade") + closeNetConn := func() { + if err := netConn.Close(); err != nil { + slog.Error("error when closing netconn", "err", err) + } + } + if err := req.Write(brw); err != nil { - netConn.Close() + closeNetConn() return nil, fmt.Errorf("could not write http request: %w", err) } if err := brw.Flush(); err != nil { - netConn.Close() + closeNetConn() return nil, fmt.Errorf("could not flush http request: %w", err) } if err := netConn.SetReadDeadline(time.Now().Add(time.Second * 5)); err != nil { - netConn.Close() + closeNetConn() return nil, fmt.Errorf("could not set read deadline: %w", err) } resp, err := http.ReadResponse(brw.Reader, req) @@ -57,7 +64,7 @@ func HTTP[T any](ctx context.Context, opts Opts, url, protocol string, makeClien c, err := makeClient(ctx, netConn, brw, opts) if err != nil { - netConn.Close() + closeNetConn() return nil, fmt.Errorf("failed to establish client: %w", err) } diff --git a/types/dial/http_server.go b/types/dial/http_server.go index d6314e7..0994044 100644 --- a/types/dial/http_server.go +++ b/types/dial/http_server.go @@ -42,18 +42,26 @@ func HTTPHandler(s ProtocolServer, proto string) http.Handler { return } - defer netConn.Close() + defer func() { + if err := netConn.Close(); err != nil { + slog.Error("error when closing netconn", "err", err) + } + }() // TODO re-add publickey frontloading? // pubKey := s.PublicKey() // "Relay-Public-Key: %s\r\n\r\n",pubKey.HexString() - fmt.Fprintf(brw, "HTTP/1.1 101 Switching Protocols\r\n"+ + if _, err := fmt.Fprintf(brw, "HTTP/1.1 101 Switching Protocols\r\n"+ "Upgrade: %s\r\n"+ "Connection: Upgrade\r\n\r\n", - up) + up); err != nil { + slog.Error("error when writing 101 response", "err", err) + } - brw.Flush() + if err := brw.Flush(); err != nil { + slog.Error("error when flushing 101 response", "err", err) + } remoteIPPort, _ := netip.ParseAddrPort(netConn.RemoteAddr().String()) diff --git a/types/dial/tcp.go b/types/dial/tcp.go index d8bc280..710c9f3 100644 --- a/types/dial/tcp.go +++ b/types/dial/tcp.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "errors" "fmt" + "log/slog" "net" "net/netip" "time" @@ -72,13 +73,15 @@ func TCP(ctx context.Context, opts Opts) (net.Conn, error) { for _, addr := range opts.Addrs { ap := netip.AddrPortFrom(addr, opts.Port) go func() { - c, e := dialOneTCP(dialCtx, ap) + conn, err := dialOneTCP(dialCtx, ap) select { - case results <- dialResult{c: c, e: e}: + case results <- dialResult{c: conn, e: err}: case <-returned: - if c != nil { - c.Close() + if conn != nil { + if err := conn.Close(); err != nil { + slog.Error("failed to close tcp connection while multi-dialing", "err", err) + } } } }() diff --git a/types/relay/client.go b/types/relay/client.go index 35ec18a..4e06295 100644 --- a/types/relay/client.go +++ b/types/relay/client.go @@ -271,7 +271,9 @@ func (c *Client) Close() { return } - c.mc.Close() + if err := c.mc.Close(); err != nil { + slog.Error("error when closing metaconn", "err", err) + } close(c.sendCh) close(c.recvCh) diff --git a/types/stun/server.go b/types/stun/server.go index 1b05f6f..769dcd2 100644 --- a/types/stun/server.go +++ b/types/stun/server.go @@ -12,8 +12,8 @@ import ( ) type Server struct { - ctx context.Context // ctx signals service shutdown - pc *net.UDPConn // pc is the UDP listener + ctx context.Context // ctx signals service shutdown + bind *net.UDPConn // bind is the UDP listener } func NewServer(ctx context.Context) *Server { @@ -24,7 +24,7 @@ func (s *Server) Listen(addrPort netip.AddrPort) error { ua := net.UDPAddrFromAddrPort(addrPort) var err error - s.pc, err = net.ListenUDP("udp", ua) + s.bind, err = net.ListenUDP("udp", ua) if err != nil { return err } @@ -32,14 +32,16 @@ func (s *Server) Listen(addrPort netip.AddrPort) error { // close the listener on shutdown in order to break out of the read loop go func() { <-s.ctx.Done() - s.pc.Close() + if err := s.bind.Close(); err != nil { + slog.Error("failed to close bind", "err", err) + } }() return nil } // LocalAddr returns the local address of the STUN server. It must not be called before ListenAndServe. func (s *Server) LocalAddr() net.Addr { - return s.pc.LocalAddr() + return s.bind.LocalAddr() } func (s *Server) Serve() error { @@ -50,7 +52,7 @@ func (s *Server) Serve() error { err error ) for { - n, ua, err = s.pc.ReadFromUDP(buf[:]) + n, ua, err = s.bind.ReadFromUDP(buf[:]) if err != nil { if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { return nil @@ -75,7 +77,7 @@ func (s *Server) Serve() error { addr, _ := netip.AddrFromSlice(ua.IP) res := Response(txid, netip.AddrPortFrom(addr, uint16(ua.Port))) - if _, err = s.pc.WriteTo(res, ua); err != nil { + if _, err = s.bind.WriteTo(res, ua); err != nil { slog.Info("writing back STUN response failed", "error", err) } } diff --git a/usrwg/bind.go b/usrwg/bind.go index 478c09a..e8c35ef 100644 --- a/usrwg/bind.go +++ b/usrwg/bind.go @@ -48,13 +48,21 @@ func (b *ToverSokBind) Close() error { maps.Clear(b.endpoints) + var errs []error + for _, cc := range b.conns { // TODO log error - cc.Close() + if err := cc.Close(); err != nil { + errs = append(errs, err) + } } maps.Clear(b.conns) + if len(errs) > 0 { + return fmt.Errorf("errors when closing connections: %w", errors.Join(errs...)) + } + return nil } @@ -272,8 +280,9 @@ func (b *ToverSokBind) CloseConn(peer key.NodePublic) { cc, ok := b.conns[peer] if ok { - // TODO log error - cc.Close() + if err := cc.Close(); err != nil { + slog.Error("failed to close channel", "peer", peer, "err", err) + } } delete(b.conns, peer) From 51327965d9c8651e49518730f50d3d3e40d9008a Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 14:28:21 +0100 Subject: [PATCH 10/82] handle, clear, or acknowledge a whole bunch of todos --- cmd/relay_server/main.go | 8 +-- test_suite/control_server/main.go | 8 --- toversok/actors/a_eman.go | 11 ---- toversok/actors/a_relay.go | 6 +- toversok/actors/a_tman.go | 15 ++++- toversok/actors/peerstate/common.go | 70 ++++++++++++++++++---- toversok/actors/peerstate/s_established.go | 2 +- toversok/actors/stage.go | 1 + toversok/control_conn.go | 2 - toversok/events.go | 60 ------------------- types/control/client.go | 10 ---- types/control/logic.go | 2 - types/key/iface.go | 3 - usrwg/bind.go | 1 - usrwg/router/router_bsd.go | 6 +- 15 files changed, 85 insertions(+), 120 deletions(-) delete mode 100644 toversok/events.go diff --git a/cmd/relay_server/main.go b/cmd/relay_server/main.go index bc1668e..1f030d4 100644 --- a/cmd/relay_server/main.go +++ b/cmd/relay_server/main.go @@ -79,10 +79,6 @@ func main() { } }() - // TODO add STUN here - - // TODO continue here - cfg := loadConfig() log.Printf("relay: using public key %s", cfg.PrivateKey.Public().Debug()) @@ -116,8 +112,6 @@ func main() { httpsrv := &http.Server{ Addr: *addr, Handler: mux, - // TODO - // ErrorLog: slog.NewLogLogger(), ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, @@ -130,7 +124,7 @@ func main() { } }() - // TODO setup TLS with autocert + // TODO setup TLS with autocert: https://github.com/eduP2P/relay-server/issues/2 slog.Info("relay: serving", "addr", *addr) err = httpsrv.ListenAndServe() diff --git a/test_suite/control_server/main.go b/test_suite/control_server/main.go index 2fdd8d1..7e24aad 100644 --- a/test_suite/control_server/main.go +++ b/test_suite/control_server/main.go @@ -51,8 +51,6 @@ func main() { mux.Handle("/control", controlhttp.ServerHandler(cserver.server)) - // TODO below is dup from relayserver main.go; dedup in a common library? - mux.Handle("/", handleStaticHTML(ToverSokControlDefaultHTML)) mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { @@ -66,8 +64,6 @@ func main() { httpsrv := &http.Server{ Addr: *addr, Handler: mux, - // TODO - // ErrorLog: slog.NewLogLogger(), ReadTimeout: 30 * time.Second, WriteTimeout: 30 * time.Second, @@ -80,8 +76,6 @@ func main() { } }() - // TODO setup TLS with autocert? - slog.Info("control: serving", "addr", *addr) err := httpsrv.ListenAndServe() @@ -232,7 +226,6 @@ func findNewIP(ipp netip.Prefix, used func(netip.Addr) bool) (netip.Prefix, neti // we exceeded the boundary, try a back-sweep backwards = true } else { - // TODO find better way to deal with this panic("address space exhausted") } } @@ -383,7 +376,6 @@ func newConfig() Config { return Config{ ControlKey: key.NewControlPrivate(), - //// TODO REPLACE WITH CONFIGURABLE VALUES IP4: netip.MustParsePrefix("10.42.0.0/16"), IP6: netip.MustParsePrefix("fd42:dead:beef::/64"), diff --git a/toversok/actors/a_eman.go b/toversok/actors/a_eman.go index d6b14f2..6d6c8e2 100644 --- a/toversok/actors/a_eman.go +++ b/toversok/actors/a_eman.go @@ -61,15 +61,6 @@ type stunResponse struct { latency time.Duration } -// TODO -// - receive relay info -// - do STUN requests to each and resolve remote endpoints -// - maybe determine when symmetric nat / "varies" is happening -// - do latency determination -// - inform relay manager that results are ready -// - relay manager switches home relay and informs stage of that decision -// - collect local endpoints - // TODO future: // - UPnP? Other stuff? @@ -280,8 +271,6 @@ func (em *EndpointManager) collectRelaySTUNEndpoints() map[netip.AddrPort]int64 } func (em *EndpointManager) getLocalEndpoints() { - // TODO disregard own address, obviously - localEndpoints := em.collectLocalEndpoints() L(em).Debug("local endpoints collected", "endpoints", localEndpoints) diff --git a/toversok/actors/a_relay.go b/toversok/actors/a_relay.go index b210b82..afc09e0 100644 --- a/toversok/actors/a_relay.go +++ b/toversok/actors/a_relay.go @@ -490,7 +490,11 @@ func (rr *RelayRouter) Run() { in := rr.s.InConnFor(frame.SrcPeer) if in == nil { - // todo log? metric? + L(rr).Debug( + "received incoming relay frame from peer that we don't know about (yet)", + "from-peer", frame.SrcPeer.Debug(), + "from-relay", frame.SrcRelay, + ) continue } diff --git a/toversok/actors/a_tman.go b/toversok/actors/a_tman.go index fa4bd7d..36447ab 100644 --- a/toversok/actors/a_tman.go +++ b/toversok/actors/a_tman.go @@ -241,9 +241,20 @@ func (tm *TrafficManager) Close() { tm.ticker.Stop() } +const PingReapTimeout = 10 * time.Minute + func (tm *TrafficManager) doPingManagement() { - // TODO - // - expire old pings + var oldPings []msgsess.TxID + + for txid, ping := range tm.pings { + if ping.At.Add(PingReapTimeout).Before(time.Now()) { + oldPings = append(oldPings, txid) + } + } + + for _, txid := range oldPings { + delete(tm.pings, txid) + } } type StateForState func(state peerstate.PeerState) peerstate.PeerState diff --git a/toversok/actors/peerstate/common.go b/toversok/actors/peerstate/common.go index fa49779..2a1cc53 100644 --- a/toversok/actors/peerstate/common.go +++ b/toversok/actors/peerstate/common.go @@ -54,22 +54,39 @@ func (sc *StateCommon) replyWithPongRelay(relay int64, node key.NodePublic, sess } // TODO add bool here and checks by callers -func (sc *StateCommon) ackPongDirect(_ netip.AddrPort, sess key.SessionPublic, pong *msgsess.Pong) { +func (sc *StateCommon) ackPongDirect(ap netip.AddrPort, sess key.SessionPublic, pong *msgsess.Pong) { sent, ok := sc.tm.Pings()[pong.TxID] if !ok { - // TODO log: Got pong for unknown ping + slog.Warn( + "got pong for unknown ping", + "from-ap", ap, + "txid", pong.TxID, + "sess", sess, + ) return } if sent.ToRelay { - // TODO log: got direct pong to relay ping + slog.Warn( + "got direct pong to relay ping", + "from-ap", ap, + "txid", pong.TxID, + "ping-to", sent.To.Debug(), + "to-relay", sent.RelayID, + "sess", sess, + ) return } if !sc.tm.ValidKeys(sc.peer, sess) { // ?? Somehow the pong is for a valid ping to a node that no longer has this session key? // Might happen between restarts, log and ignore. - // TODO log + slog.Warn( + "received valid pong for unexpected remote session", + "from-ap", ap, + "txid", pong.TxID, + "sess", sess, + ) return } @@ -79,32 +96,65 @@ func (sc *StateCommon) ackPongDirect(_ netip.AddrPort, sess key.SessionPublic, p } // TODO add bool here and checks by callers -func (sc *StateCommon) ackPongRelay(_ int64, node key.NodePublic, sess key.SessionPublic, pong *msgsess.Pong) { +func (sc *StateCommon) ackPongRelay(relayID int64, node key.NodePublic, sess key.SessionPublic, pong *msgsess.Pong) { // Relay pongs should come in response to relay pings, note if it is different. sent, ok := sc.tm.Pings()[pong.TxID] if !ok { - // TODO log: Got pong for unknown ping + slog.Warn( + "got pong for unknown ping", + "from-relay", relayID, + "txid", pong.TxID, + "sess", sess, + ) return } if !sent.ToRelay { - // TODO log: got relay reply to direct ping + slog.Warn( + "got relay pong to direct ping", + "from-relay", relayID, + "txid", pong.TxID, + "ping-to", sent.To.Debug(), + "to-relay", sent.RelayID, + "sess", sess, + ) return } - if !sc.tm.ValidKeys(node, sess) { - // TODO log + if node != sent.To { + slog.Warn( + "received pong to ping (with same TXID) from a different peer than we sent it to, possible collision", + "to-peer", sent.To.Debug(), + "from-peer", node.Debug(), + "from-relay", relayID, + "txid", pong.TxID, + "sess", sess, + ) return } if !sc.tm.ValidKeys(sent.To, sess) { // ?? Somehow the pong is for a valid ping to a node that no longer has this session key? // Might happen between restarts, log and ignore. - // TODO log + slog.Warn( + "received valid pong for unexpected remote session", + "from-relay", relayID, + "txid", pong.TxID, + "sess", sess, + ) return } + if sent.RelayID != relayID { + slog.Debug( + "received relay pong to relay ping from other relay, ignoring...", + "to-relay", sent.RelayID, + "from-relay", relayID, + "txid", pong.TxID, + ) + } + // TODO more checks? (permissive, but log) delete(sc.tm.Pings(), pong.TxID) diff --git a/toversok/actors/peerstate/s_established.go b/toversok/actors/peerstate/s_established.go index ef5441d..2e9010a 100644 --- a/toversok/actors/peerstate/s_established.go +++ b/toversok/actors/peerstate/s_established.go @@ -135,8 +135,8 @@ func (e *Established) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgses e.ackPongRelay(relay, peer, clearMsg.Session, m) return nil + // TODO maybe re-establishment logic? // case *msg.Rendezvous: - // TODO maybe re-establishment logic? default: L(e).Debug("ignoring relay session message", "relay", relay, diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index b6b7906..1798ab7 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -395,6 +395,7 @@ func (s *Stage) setLocalEndpoints(addrs []netip.Addr) { var endpoints []netip.AddrPort + // Filter own endpoint, and also append localport for _, addr := range addrs { if s.control.IPv4().Contains(addr) || s.control.IPv6().Contains(addr) { continue diff --git a/toversok/control_conn.go b/toversok/control_conn.go index 5a0c3de..4fa2a78 100644 --- a/toversok/control_conn.go +++ b/toversok/control_conn.go @@ -72,8 +72,6 @@ type ResumableControlSession struct { } func CreateControlSession(ctx context.Context, opts dial.Opts, controlKey key.ControlPublic, getPriv func() *key.NodePrivate, getSess func() *key.SessionPrivate, logon types.LogonCallback) (*ResumableControlSession, error) { - // TODO authCallback func(url string) - rcsCtx, rcsCcc := context.WithCancelCause(ctx) clientCtx := context.WithoutCancel(rcsCtx) diff --git a/toversok/events.go b/toversok/events.go deleted file mode 100644 index d5aa008..0000000 --- a/toversok/events.go +++ /dev/null @@ -1,60 +0,0 @@ -package toversok - -import ( - "net/netip" - - "github.com/LukaGiorgadze/gonull" - "github.com/edup2p/common/types/key" - "github.com/edup2p/common/types/relay" -) - -// TODO DEPRECATED, should be refactored into using fake control client and such - -type Event interface { - EventName() string -} - -type RelayUpdate struct { - // Updates relays referenced in this set. - // - // Note: Deliberately does not allow for unsetting relays. - Set []relay.Information -} - -func (r RelayUpdate) EventName() string { - return "RelayUpdate" -} - -type PeerAddition struct { - Key key.NodePublic - - HomeRelayID int64 - SessionKey key.SessionPublic - Endpoints []netip.AddrPort - - VIPs VirtualIPs -} - -func (p PeerAddition) EventName() string { - return "PeerAddition" -} - -type PeerUpdate struct { - Key key.NodePublic - - HomeRelayID gonull.Nullable[int64] - SessionKey gonull.Nullable[key.SessionPublic] - Endpoints gonull.Nullable[[]netip.AddrPort] -} - -func (p PeerUpdate) EventName() string { - return "PeerUpdate" -} - -type PeerRemoval struct { - Key key.NodePublic -} - -func (p PeerRemoval) EventName() string { - return "PeerRemoval" -} diff --git a/types/control/client.go b/types/control/client.go index 86d00bb..e20a09c 100644 --- a/types/control/client.go +++ b/types/control/client.go @@ -28,8 +28,6 @@ type Client struct { IPv4 netip.Prefix IPv6 netip.Prefix - - // TODO } func EstablishClient(parentCtx context.Context, mc types.MetaConn, brw *bufio.ReadWriter, timeout time.Duration, getPriv func() *key.NodePrivate, getSess func() *key.SessionPrivate, controlKey key.ControlPublic, session *string, logon types.LogonCallback) (*Client, error) { @@ -53,14 +51,6 @@ func EstablishClient(parentCtx context.Context, mc types.MetaConn, brw *bufio.Re } func (c *Client) Handshake(timeout time.Duration, logon types.LogonCallback) error { - // TODO - // 1. send ClientHello - // 2. expect ServerHello - // 3. send Logon - // 4. (optional) expect LogonAuthenticate - // - Allow sending LogonDeviceKey - // 4. expect LogonAccept|LogonReject - if timeout != 0 { if err := c.cc.mc.SetDeadline(time.Now().Add(timeout)); err != nil { return fmt.Errorf("can't set deadline: %w", err) diff --git a/types/control/logic.go b/types/control/logic.go index 0f90a1e..4468cc1 100644 --- a/types/control/logic.go +++ b/types/control/logic.go @@ -86,8 +86,6 @@ func (s *Server) GetConnectedClients() (map[SessID]ClientID, error) { } return retMap, nil - - // todo what do we use the error field for here? } func (s *Server) UpsertVisibilityPair(id ClientID, id2 ClientID, pair VisibilityPair) error { diff --git a/types/key/iface.go b/types/key/iface.go index 58c0fa6..ffff6fb 100644 --- a/types/key/iface.go +++ b/types/key/iface.go @@ -25,15 +25,12 @@ type publicKey interface { IsZero() bool Debug() string HexString() string - // TODO } type privateKey[Pub key] interface { key Public() Pub - - // TODO } type canSealTo[To publicKey] interface { diff --git a/usrwg/bind.go b/usrwg/bind.go index e8c35ef..0958076 100644 --- a/usrwg/bind.go +++ b/usrwg/bind.go @@ -51,7 +51,6 @@ func (b *ToverSokBind) Close() error { var errs []error for _, cc := range b.conns { - // TODO log error if err := cc.Close(); err != nil { errs = append(errs, err) } diff --git a/usrwg/router/router_bsd.go b/usrwg/router/router_bsd.go index 2b47d50..ba486b0 100644 --- a/usrwg/router/router_bsd.go +++ b/usrwg/router/router_bsd.go @@ -105,7 +105,8 @@ func (r *bsdRouter) removeAddr(prefix netip.Prefix) error { func (r *bsdRouter) addRoute(prefix netip.Prefix) error { net := netipx.PrefixIPNet(prefix) - // TODO replace with .Masked()? + // TODO replace with (Prefix).Masked()? + // need to figure out what the exact outputs are, and if .Masked does that nip := net.IP.Mask(net.Mask) nstr := fmt.Sprintf("%v/%d", nip, prefix.Bits()) @@ -124,7 +125,8 @@ func (r *bsdRouter) addRoute(prefix netip.Prefix) error { func (r *bsdRouter) removeRoute(prefix netip.Prefix) error { net := netipx.PrefixIPNet(prefix) - // TODO replace with .Masked()? + // TODO replace with (Prefix).Masked()? + // need to figure out what the exact outputs are, and if .Masked does that nip := net.IP.Mask(net.Mask) nstr := fmt.Sprintf("%v/%d", nip, prefix.Bits()) del := "del" From a88d4f0382cc3b83e1f3bcc69c6410d70afbc9fb Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 14:53:17 +0100 Subject: [PATCH 11/82] add relay dial function to Stage closes #70 --- toversok/actors/a_relay.go | 8 ++-- toversok/actors/stage.go | 11 +++++ toversok/session.go | 2 +- types/relay/client.go | 61 ++++++++++++++++++---------- types/relay/relayhttp/http_client.go | 6 ++- 5 files changed, 58 insertions(+), 30 deletions(-) diff --git a/toversok/actors/a_relay.go b/toversok/actors/a_relay.go index afc09e0..6dcb82b 100644 --- a/toversok/actors/a_relay.go +++ b/toversok/actors/a_relay.go @@ -14,7 +14,6 @@ import ( "github.com/edup2p/common/types/msgactor" "github.com/edup2p/common/types/msgsess" "github.com/edup2p/common/types/relay" - "github.com/edup2p/common/types/relay/relayhttp" ) // RestartableRelayConn is a Relay connection that will automatically reconnect, @@ -26,7 +25,7 @@ type RestartableRelayConn struct { config relay.Information - client *relay.Client + client relay.Client stay bool @@ -120,7 +119,7 @@ func (c *RestartableRelayConn) establish() (success bool) { } var err error - c.client, err = relayhttp.Dial(c.ctx, dial.Opts{ + c.client, err = c.man.s.dialRelayFunc(c.ctx, dial.Opts{ Domain: c.config.Domain, Addrs: types.SliceOrNil(c.config.IPs), Port: port, @@ -138,8 +137,7 @@ func (c *RestartableRelayConn) establish() (success bool) { return false } - go c.client.RunSend() - go c.client.RunReceive() + go c.client.Run() c.L().Debug("established") diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index 1798ab7..3769e75 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -3,6 +3,7 @@ package actors import ( "context" "errors" + "github.com/edup2p/common/types/relay/relayhttp" "log/slog" "net" "net/netip" @@ -44,9 +45,15 @@ func MakeStage( bindExt func() types.UDPConn, bindLocal func(peer key.NodePublic) types.UDPConn, controlSession ifaces.ControlInterface, + + dialRelayFunc relayhttp.RelayDialFunc, ) ifaces.Stage { ctx := context.WithoutCancel(pCtx) + if dialRelayFunc == nil { + dialRelayFunc = relayhttp.Dial + } + s := &Stage{ Ctx: ctx, @@ -65,6 +72,8 @@ func MakeStage( ext: bindExt(), bindLocal: bindLocal, control: controlSession, + + dialRelayFunc: dialRelayFunc, } s.DMan = s.makeDM(s.ext) @@ -128,6 +137,8 @@ type Stage struct { ext types.UDPConn bindLocal func(peer key.NodePublic) types.UDPConn + + dialRelayFunc relayhttp.RelayDialFunc } // Start kicks off goroutines for the stage and returns diff --git a/toversok/session.go b/toversok/session.go index 5c37f69..74c2117 100644 --- a/toversok/session.go +++ b/toversok/session.go @@ -74,7 +74,7 @@ func SetupSession( return nil, err } - sess.stage = actors.MakeStage(sess.ctx, getNodePriv, sess.getPriv, getExtSock, sess.wg.ConnFor, cc) + sess.stage = actors.MakeStage(sess.ctx, getNodePriv, sess.getPriv, getExtSock, sess.wg.ConnFor, cc, nil) cc.InstallCallbacks(sess) diff --git a/types/relay/client.go b/types/relay/client.go index 4e06295..96c1478 100644 --- a/types/relay/client.go +++ b/types/relay/client.go @@ -29,8 +29,20 @@ var ( errKeepAliveNonZeroLen = errors.New("keepalive frame has non-zero length") ) -// Client is a Relay client that lives as long as its conn does -type Client struct { +type Client interface { + Run() + RelayKey() key.NodePublic + + Send() chan<- SendPacket + Recv() <-chan RecvPacket + Done() <-chan struct{} + Err() error + + Close() +} + +// HTTPClient is a Relay client that lives as long as its conn does +type HTTPClient struct { ctx context.Context ccc context.CancelCauseFunc @@ -52,19 +64,19 @@ type Client struct { closed bool } -func (c *Client) Send() chan<- SendPacket { +func (c *HTTPClient) Send() chan<- SendPacket { return c.sendCh } -func (c *Client) Recv() <-chan RecvPacket { +func (c *HTTPClient) Recv() <-chan RecvPacket { return c.recvCh } -func (c *Client) Done() <-chan struct{} { +func (c *HTTPClient) Done() <-chan struct{} { return c.ctx.Done() } -func (c *Client) Err() error { +func (c *HTTPClient) Err() error { return c.ctx.Err() } @@ -82,14 +94,14 @@ type RecvPacket struct { Data []byte } -// EstablishClient creates a new relay.Client on a given MetaConn with associated bufio.ReadWriter. +// EstablishClient creates a new relay.HTTPClient on a given MetaConn with associated bufio.ReadWriter. // -// It logs in and authenticates the server before returning a Client object. +// It logs in and authenticates the server before returning a HTTPClient object. // If any error occurs, or no client can be established before timeout, it returns. -func EstablishClient(parentCtx context.Context, mc types.MetaConn, brw *bufio.ReadWriter, timeout time.Duration, getPriv func() *key.NodePrivate) (*Client, error) { +func EstablishClient(parentCtx context.Context, mc types.MetaConn, brw *bufio.ReadWriter, timeout time.Duration, getPriv func() *key.NodePrivate) (*HTTPClient, error) { ctx, ccc := context.WithCancelCause(parentCtx) - c := &Client{ + c := &HTTPClient{ ctx: ctx, ccc: ccc, @@ -148,28 +160,28 @@ func EstablishClient(parentCtx context.Context, mc types.MetaConn, brw *bufio.Re return c, nil } -func (c *Client) privateKey() *key.NodePrivate { +func (c *HTTPClient) privateKey() *key.NodePrivate { return c.getPriv() } -func (c *Client) publicKey() key.NodePublic { +func (c *HTTPClient) publicKey() key.NodePublic { return c.privateKey().Public() } // RelayKey returns the key of the relay we're connected to. -func (c *Client) RelayKey() key.NodePublic { +func (c *HTTPClient) RelayKey() key.NodePublic { return c.relayServerKey } // recvVersion assumes the caller has ownership, or lock -func (c *Client) recvVersion() (ProtocolVersion, error) { +func (c *HTTPClient) recvVersion() (ProtocolVersion, error) { b, err := c.reader.ReadByte() return ProtocolVersion(b), err } // recvServerKey assumes the caller has ownership, or lock -func (c *Client) recvServerKey() error { +func (c *HTTPClient) recvServerKey() error { frTyp, frLen, err := readFrameHeader(c.reader) if err != nil { return err @@ -198,7 +210,7 @@ func (c *Client) recvServerKey() error { } // sendClientInfo assumes the caller has ownership, or lock -func (c *Client) sendClientInfo() error { +func (c *HTTPClient) sendClientInfo() error { m, err := json.Marshal(ClientInfo{SendKeepalive: true}) if err != nil { return err @@ -222,7 +234,7 @@ func (c *Client) sendClientInfo() error { } // recvServerInfo assumes the caller has ownership, or lock -func (c *Client) recvServerInfo() (*ServerInfo, error) { +func (c *HTTPClient) recvServerInfo() (*ServerInfo, error) { frTyp, frLen, err := readFrameHeader(c.reader) if err != nil { return nil, err @@ -259,14 +271,14 @@ func (c *Client) recvServerInfo() (*ServerInfo, error) { return info, nil } -func (c *Client) Cancel(err error) { +func (c *HTTPClient) Cancel(err error) { c.ccc(err) if err := c.mc.SetDeadline(time.Now().Add(10 * time.Millisecond)); err != nil { slog.Error("could not set deadline in Cancel", "err", err) } } -func (c *Client) Close() { +func (c *HTTPClient) Close() { if c.closed || context.Cause(c.ctx) != nil { return } @@ -280,11 +292,16 @@ func (c *Client) Close() { c.closed = true } -func (c *Client) Closed() bool { +func (c *HTTPClient) Closed() bool { return c.closed } -func (c *Client) RunReceive() { +func (c *HTTPClient) Run() { + go c.RunReceive() + go c.RunSend() +} + +func (c *HTTPClient) RunReceive() { if !c.recvMutex.TryLock() { slog.Error("could not lock recvMutex, is RunReceive already running?") return @@ -359,7 +376,7 @@ func (c *Client) RunReceive() { } } -func (c *Client) RunSend() { +func (c *HTTPClient) RunSend() { if !c.sendMutex.TryLock() { slog.Error("could not lock sendMutex, is RunSend already running?") return diff --git a/types/relay/relayhttp/http_client.go b/types/relay/relayhttp/http_client.go index 5c28d34..1501eaa 100644 --- a/types/relay/relayhttp/http_client.go +++ b/types/relay/relayhttp/http_client.go @@ -23,10 +23,12 @@ func makeRelayURL(opts dial.Opts) string { return fmt.Sprintf("%s://%s/relay", proto, domain) } -func Dial(ctx context.Context, opts dial.Opts, getPriv func() *key.NodePrivate, expectKey key.NodePublic) (*relay.Client, error) { +type RelayDialFunc func(ctx context.Context, opts dial.Opts, getPriv func() *key.NodePrivate, expectKey key.NodePublic) (relay.Client, error) + +func Dial(ctx context.Context, opts dial.Opts, getPriv func() *key.NodePrivate, expectKey key.NodePublic) (relay.Client, error) { opts.SetDefaults() - c, err := dial.HTTP(ctx, opts, makeRelayURL(opts), relay.UpgradeProtocol, func(parentCtx context.Context, mc types.MetaConn, brw *bufio.ReadWriter, opts dial.Opts) (*relay.Client, error) { + c, err := dial.HTTP(ctx, opts, makeRelayURL(opts), relay.UpgradeProtocol, func(parentCtx context.Context, mc types.MetaConn, brw *bufio.ReadWriter, opts dial.Opts) (*relay.HTTPClient, error) { return relay.EstablishClient(parentCtx, mc, brw, opts.EstablishTimeout, getPriv) }) if err != nil { From fa107e18748fccb5edf89d85a1b75f7b87ca5b5c Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 14:55:39 +0100 Subject: [PATCH 12/82] gofumpt --- toversok/actors/stage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index 3769e75..06a30f9 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -3,7 +3,6 @@ package actors import ( "context" "errors" - "github.com/edup2p/common/types/relay/relayhttp" "log/slog" "net" "net/netip" @@ -18,6 +17,7 @@ import ( "github.com/edup2p/common/types/msgactor" "github.com/edup2p/common/types/msgcontrol" "github.com/edup2p/common/types/relay" + "github.com/edup2p/common/types/relay/relayhttp" "github.com/edup2p/common/types/stage" "golang.org/x/exp/maps" ) From d3caff4e0b8173fb393677ec3c553759d7f6b826 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 15:08:16 +0100 Subject: [PATCH 13/82] additional gofumpt checks --- toversok/actors/peerstate/util.go | 2 +- toversok/actors/stage.go | 4 ++-- toversok/session.go | 2 +- types/control/logic.go | 4 ++-- types/ifaces/control.go | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/toversok/actors/peerstate/util.go b/toversok/actors/peerstate/util.go index c2a58e0..f479f36 100644 --- a/toversok/actors/peerstate/util.go +++ b/toversok/actors/peerstate/util.go @@ -45,7 +45,7 @@ func L(s PeerState) *slog.Logger { return slog.With("peer", s.Peer().Debug(), "state", s.Name()) } -func LogTransition(from PeerState, to PeerState) PeerState { +func LogTransition(from, to PeerState) PeerState { L(from).Log(context.Background(), types.LevelTrace, "transitioning state", "to-state", to.Name()) return to diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index 06a30f9..9c889ec 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -264,7 +264,7 @@ func (s *Stage) reapableConnsLocked() []key.NodePublic { return peers } -func (s *Stage) syncableConnsLocked() (added []key.NodePublic, deleted []key.NodePublic) { +func (s *Stage) syncableConnsLocked() (added, deleted []key.NodePublic) { piPeers := maps.Keys(s.peerInfo) connPeers := types.SetUnion(maps.Keys(s.inConn), maps.Keys(s.outConn)) @@ -462,7 +462,7 @@ func (s *Stage) notifyEndpointChanged() { } } -func (s *Stage) AddPeer(peer key.NodePublic, homeRelay int64, endpoints []netip.AddrPort, session key.SessionPublic, _ netip.Addr, _ netip.Addr, _ msgcontrol.Properties) error { +func (s *Stage) AddPeer(peer key.NodePublic, homeRelay int64, endpoints []netip.AddrPort, session key.SessionPublic, _, _ netip.Addr, _ msgcontrol.Properties) error { s.peerInfoMutex.Lock() defer func() { diff --git a/toversok/session.go b/toversok/session.go index 74c2117..c6a9fe7 100644 --- a/toversok/session.go +++ b/toversok/session.go @@ -133,7 +133,7 @@ func (s *Session) triggerQuarantineUpdate() { // CONTROL CALLBACKS -func (s *Session) AddPeer(peer key.NodePublic, homeRelay int64, endpoints []netip.AddrPort, session key.SessionPublic, ip4 netip.Addr, ip6 netip.Addr, prop msgcontrol.Properties) error { +func (s *Session) AddPeer(peer key.NodePublic, homeRelay int64, endpoints []netip.AddrPort, session key.SessionPublic, ip4, ip6 netip.Addr, prop msgcontrol.Properties) error { s.registerPeerAddrs(peer, ip4, ip6) if prop.Quarantine { diff --git a/types/control/logic.go b/types/control/logic.go index 4468cc1..dc9b81b 100644 --- a/types/control/logic.go +++ b/types/control/logic.go @@ -88,7 +88,7 @@ func (s *Server) GetConnectedClients() (map[SessID]ClientID, error) { return retMap, nil } -func (s *Server) UpsertVisibilityPair(id ClientID, id2 ClientID, pair VisibilityPair) error { +func (s *Server) UpsertVisibilityPair(id, id2 ClientID, pair VisibilityPair) error { s.sessLock.RLock() defer s.sessLock.RUnlock() @@ -169,7 +169,7 @@ func (s *Server) UpsertMultiVisibilityPair(id ClientID, m map[ClientID]Visibilit return nil } -func (s *Server) RemoveVisibilityPair(from ClientID, to ClientID) error { +func (s *Server) RemoveVisibilityPair(from, to ClientID) error { s.sessLock.RLock() defer s.sessLock.RUnlock() diff --git a/types/ifaces/control.go b/types/ifaces/control.go index 3c43dff..1c205fb 100644 --- a/types/ifaces/control.go +++ b/types/ifaces/control.go @@ -14,7 +14,7 @@ type ControlCallbacks interface { AddPeer( peer key.NodePublic, homeRelay int64, endpoints []netip.AddrPort, session key.SessionPublic, - ip4 netip.Addr, ip6 netip.Addr, + ip4, ip6 netip.Addr, prop msgcontrol.Properties, ) error From 7668064ff29990b2319d2e6ec881c527a848ddf6 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 15:07:01 +0100 Subject: [PATCH 14/82] add golangci-lint CI checks --- .github/workflows/golangci-lint-main.yml | 23 +++++++++++++++++++++++ .github/workflows/golangci-lint-pr.yml | 23 +++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 .github/workflows/golangci-lint-main.yml create mode 100644 .github/workflows/golangci-lint-pr.yml diff --git a/.github/workflows/golangci-lint-main.yml b/.github/workflows/golangci-lint-main.yml new file mode 100644 index 0000000..609faaf --- /dev/null +++ b/.github/workflows/golangci-lint-main.yml @@ -0,0 +1,23 @@ +name: golangci-lint +on: + push: + branches: + - main + +permissions: + contents: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.60 + args: -E stylecheck,revive,gocritic,gofumpt diff --git a/.github/workflows/golangci-lint-pr.yml b/.github/workflows/golangci-lint-pr.yml new file mode 100644 index 0000000..28f0167 --- /dev/null +++ b/.github/workflows/golangci-lint-pr.yml @@ -0,0 +1,23 @@ +name: golangci-lint +on: + pull_request: + +permissions: + contents: read + pull-requests: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: stable + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.60 + args: -E stylecheck,revive,gocritic,gofumpt + only-new-issues: true From cc0d12a6c364af6efa34109191b51e563f2d2c9c Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 15:11:54 +0100 Subject: [PATCH 15/82] specify go.mod as golang version source --- .github/workflows/golangci-lint-main.yml | 2 +- .github/workflows/golangci-lint-pr.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/golangci-lint-main.yml b/.github/workflows/golangci-lint-main.yml index 609faaf..64e8b55 100644 --- a/.github/workflows/golangci-lint-main.yml +++ b/.github/workflows/golangci-lint-main.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: stable + go-version-file: go.mod - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: diff --git a/.github/workflows/golangci-lint-pr.yml b/.github/workflows/golangci-lint-pr.yml index 28f0167..fac0d8b 100644 --- a/.github/workflows/golangci-lint-pr.yml +++ b/.github/workflows/golangci-lint-pr.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: stable + go-version-file: go.mod - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: From 5d05c3eb80e574243255d6cd018dee26939f5599 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Feb 2025 15:14:15 +0100 Subject: [PATCH 16/82] additional: specify go.mod as golang version source --- .github/workflows/CI_test_suite.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI_test_suite.yml b/.github/workflows/CI_test_suite.yml index d5e18af..12e008f 100644 --- a/.github/workflows/CI_test_suite.yml +++ b/.github/workflows/CI_test_suite.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version-file: go.mod - name: Set up Python uses: actions/setup-python@v5 @@ -55,7 +55,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: '1.22' + go-version-file: go.mod - name: Run integration tests id: integration-test From 04dc1b42032b67d3eb086918171e86275acc1d0d Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Tue, 18 Feb 2025 13:21:33 +0100 Subject: [PATCH 17/82] Fix linting errors --- usrwg/router/util.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/usrwg/router/util.go b/usrwg/router/util.go index eae3d63..9822638 100644 --- a/usrwg/router/util.go +++ b/usrwg/router/util.go @@ -38,6 +38,8 @@ func prefixesToRemove(newP, currP []netip.Prefix) (remove []netip.Prefix) { return } +// nolint:unused +// used in router_bsd, golangci-lint on linux trips over it func inet(p netip.Prefix) string { if p.Addr().Is6() { return "inet6" @@ -52,6 +54,8 @@ func cmd(args ...string) *exec.Cmd { return exec.Command(args[0], args[1:]...) } +// nolint:unused +// used in router_bsd, golangci-lint on linux trips over it func prefixToSingle(prefix netip.Prefix) netip.Prefix { return netip.PrefixFrom(prefix.Addr(), prefix.Addr().BitLen()) } From d2390133b75668a8bcbdf4884c522234a9361761 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Tue, 18 Feb 2025 13:10:39 +0100 Subject: [PATCH 18/82] Add pingtracker --- toversok/actors/peerstate/common.go | 28 +++-- toversok/actors/peerstate/e_half.go | 5 + toversok/actors/peerstate/e_rendez.go | 5 + toversok/actors/peerstate/e_t_finalising.go | 8 +- toversok/actors/peerstate/e_transmitting.go | 5 + toversok/actors/peerstate/pingtracker.go | 102 ++++++++++++++++++ toversok/actors/peerstate/pingtracker_test.go | 76 +++++++++++++ toversok/actors/peerstate/s_established.go | 48 +++++++-- toversok/actors/peerstate/s_inactive.go | 7 +- toversok/actors/peerstate/s_trying.go | 7 +- 10 files changed, 275 insertions(+), 16 deletions(-) create mode 100644 toversok/actors/peerstate/pingtracker.go create mode 100644 toversok/actors/peerstate/pingtracker_test.go diff --git a/toversok/actors/peerstate/common.go b/toversok/actors/peerstate/common.go index 2a1cc53..e814c22 100644 --- a/toversok/actors/peerstate/common.go +++ b/toversok/actors/peerstate/common.go @@ -2,6 +2,7 @@ package peerstate import ( "context" + "errors" "log/slog" "net/netip" "time" @@ -53,8 +54,7 @@ func (sc *StateCommon) replyWithPongRelay(relay int64, node key.NodePublic, sess }) } -// TODO add bool here and checks by callers -func (sc *StateCommon) ackPongDirect(ap netip.AddrPort, sess key.SessionPublic, pong *msgsess.Pong) { +func (sc *StateCommon) pongDirectValid(ap netip.AddrPort, sess key.SessionPublic, pong *msgsess.Pong) error { sent, ok := sc.tm.Pings()[pong.TxID] if !ok { slog.Warn( @@ -63,7 +63,7 @@ func (sc *StateCommon) ackPongDirect(ap netip.AddrPort, sess key.SessionPublic, "txid", pong.TxID, "sess", sess, ) - return + return errors.New("pong txid does not correspond to any sent ping") } if sent.ToRelay { @@ -75,7 +75,7 @@ func (sc *StateCommon) ackPongDirect(ap netip.AddrPort, sess key.SessionPublic, "to-relay", sent.RelayID, "sess", sess, ) - return + return errors.New("direct pong is reply to relay ping") } if !sc.tm.ValidKeys(sc.peer, sess) { @@ -87,11 +87,15 @@ func (sc *StateCommon) ackPongDirect(ap netip.AddrPort, sess key.SessionPublic, "txid", pong.TxID, "sess", sess, ) - return + return errors.New("got pong from invalid session") } // TODO more checks? (permissive, but log) + return nil +} + +func (sc *StateCommon) clearPongDirect(_ netip.AddrPort, _ key.SessionPublic, pong *msgsess.Pong) { delete(sc.tm.Pings(), pong.TxID) } @@ -172,14 +176,26 @@ type EstablishingCommon struct { lastPing time.Time pingCount uint + + tracker *PingTracker } func mkEstComm(sc *StateCommon, attempts int) *EstablishingCommon { - ec := &EstablishingCommon{StateCommon: sc, attempt: attempts + 1} + ec := &EstablishingCommon{ + StateCommon: sc, + attempt: attempts + 1, + tracker: NewPingTracker(), + } ec.resetDeadline() return ec } +func (ec *EstablishingCommon) ackPongDirect(ap netip.AddrPort, sess key.SessionPublic, pong *msgsess.Pong) { + ec.tracker.GotPong(ap) + + ec.clearPongDirect(ap, sess, pong) +} + func (ec *EstablishingCommon) resetDeadline() { ec.deadline = time.Now().Add(EstablishmentTimeout) } diff --git a/toversok/actors/peerstate/e_half.go b/toversok/actors/peerstate/e_half.go index 05675bf..c117c51 100644 --- a/toversok/actors/peerstate/e_half.go +++ b/toversok/actors/peerstate/e_half.go @@ -50,6 +50,11 @@ func (e *EstHalf) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) Pe e.lastPing = time.Now() return nil case *msgsess.Pong: + if err := e.pongDirectValid(ap, clearMsg.Session, m); err != nil { + L(e).Warn("dropping invalid pong", "ap", ap.String(), "err", err) + return nil + } + e.tm.Poke() return LogTransition(e, &Finalizing{ EstablishingCommon: e.EstablishingCommon, diff --git a/toversok/actors/peerstate/e_rendez.go b/toversok/actors/peerstate/e_rendez.go index 2ebf737..8fbd8ac 100644 --- a/toversok/actors/peerstate/e_rendez.go +++ b/toversok/actors/peerstate/e_rendez.go @@ -50,6 +50,11 @@ func (e *EstRendezAck) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessag ping: m, }) case *msgsess.Pong: + if err := e.pongDirectValid(ap, clearMsg.Session, m); err != nil { + L(e).Warn("dropping invalid pong", "ap", ap.String(), "err", err) + return nil + } + e.tm.Poke() return LogTransition(e, &Finalizing{ EstablishingCommon: e.EstablishingCommon, diff --git a/toversok/actors/peerstate/e_t_finalising.go b/toversok/actors/peerstate/e_t_finalising.go index 1df4f01..2d7e0f7 100644 --- a/toversok/actors/peerstate/e_t_finalising.go +++ b/toversok/actors/peerstate/e_t_finalising.go @@ -22,9 +22,15 @@ func (f *Finalizing) Name() string { func (f *Finalizing) OnTick() PeerState { f.ackPongDirect(f.ap, f.sess, f.pong) + bap, err := f.tracker.BestAddrPort() + if err != nil { + // We just acked a pong, so there should at least be 1 pair in there, so panic + panic(err) + } + return LogTransition(f, &Booting{ StateCommon: f.StateCommon, - ap: f.ap, + ap: bap, }) } diff --git a/toversok/actors/peerstate/e_transmitting.go b/toversok/actors/peerstate/e_transmitting.go index 6337a76..a35b999 100644 --- a/toversok/actors/peerstate/e_transmitting.go +++ b/toversok/actors/peerstate/e_transmitting.go @@ -50,6 +50,11 @@ func (e *EstTransmitting) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMes ping: m, }) case *msgsess.Pong: + if err := e.pongDirectValid(ap, clearMsg.Session, m); err != nil { + L(e).Warn("dropping invalid pong", "ap", ap.String(), "err", err) + return nil + } + e.tm.Poke() return LogTransition(e, &Finalizing{ EstablishingCommon: e.EstablishingCommon, diff --git a/toversok/actors/peerstate/pingtracker.go b/toversok/actors/peerstate/pingtracker.go new file mode 100644 index 0000000..95a8d67 --- /dev/null +++ b/toversok/actors/peerstate/pingtracker.go @@ -0,0 +1,102 @@ +package peerstate + +import ( + "errors" + "net/netip" + "slices" + "sync" + + "github.com/edup2p/common/types" +) + +type PingTracker struct { + rw sync.RWMutex + gotPong map[netip.AddrPort]bool +} + +func NewPingTracker() *PingTracker { + return &PingTracker{ + gotPong: make(map[netip.AddrPort]bool), + } +} + +func (pt *PingTracker) validAPs() []netip.AddrPort { + var aps []netip.AddrPort + + for ap, gotPong := range pt.gotPong { + if gotPong { + aps = append(aps, ap) + } + } + + return aps +} + +func (pt *PingTracker) GotPong(ap netip.AddrPort) { + pt.rw.Lock() + defer pt.rw.Unlock() + + nap := types.NormaliseAddrPort(ap) + pt.gotPong[nap] = true +} + +func (pt *PingTracker) BestAddrPort() (netip.AddrPort, error) { + pt.rw.RLock() + defer pt.rw.RUnlock() + + aps := pt.validAPs() + if len(aps) == 0 { + return netip.AddrPort{}, errors.New("no valid pings") + } + + slices.SortFunc(aps, gradeAPs) + slices.Reverse(aps) + + return aps[0], nil +} + +const ( + aBetter = 1 + bBetter = -1 + neither = 0 +) + +func gradeAPs(a, b netip.AddrPort) int { + if verCmp := gradeVer(a, b); verCmp != neither { + return verCmp + } + + if privCmp := gradePriv(a, b); privCmp != neither { + return privCmp + } + + return a.Compare(b) +} + +// IPv6 > IPv4 +func gradeVer(ap, bp netip.AddrPort) int { + a := ap.Addr() + b := bp.Addr() + + if a.Is4() && b.Is6() { + return bBetter + } else if a.Is6() && b.Is4() { + return aBetter + } + + return neither +} + +// Private/Unique Local > Non-Private/Unique Global +func gradePriv(ap, bp netip.AddrPort) int { + a := ap.Addr() + b := bp.Addr() + + if a.IsPrivate() && !b.IsPrivate() { + return aBetter + } else if !a.IsPrivate() && b.IsPrivate() { + return bBetter + } + + return neither +} diff --git a/toversok/actors/peerstate/pingtracker_test.go b/toversok/actors/peerstate/pingtracker_test.go new file mode 100644 index 0000000..79118fb --- /dev/null +++ b/toversok/actors/peerstate/pingtracker_test.go @@ -0,0 +1,76 @@ +package peerstate + +import ( + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + pub4Addr = netip.MustParseAddrPort("8.0.0.1:1337") + pub6Addr = netip.MustParseAddrPort("[2000::1]:1337") + + priv4Addr = netip.MustParseAddrPort("10.0.0.1:1337") + priv6Addr = netip.MustParseAddrPort("[fd00::1]:1337") +) + +func TestPingTracker_FullSelection(t *testing.T) { + pt := NewPingTracker() + + for _, ip := range []netip.AddrPort{ + pub4Addr, + pub6Addr, + + priv4Addr, + priv6Addr, + } { + pt.GotPong(ip) + } + + bap, err := pt.BestAddrPort() + + assert.NoError(t, err) + assert.Equal(t, bap, priv6Addr) +} + +func TestPingTracker_NoPings(t *testing.T) { + pt := NewPingTracker() + + _, err := pt.BestAddrPort() + + assert.Error(t, err) +} + +func TestPingTracker_BestAddrPort(t *testing.T) { + pt := NewPingTracker() + + var bap netip.AddrPort + var err error + + // First add private ip4 + pt.GotPong(priv4Addr) + bap, err = pt.BestAddrPort() + assert.NoError(t, err) + assert.Equal(t, bap, priv4Addr) + + // Then add public ip4, + // this changes nothing + pt.GotPong(pub4Addr) + bap, err = pt.BestAddrPort() + assert.NoError(t, err) + assert.Equal(t, bap, priv4Addr) + + // Then add private ip6 + pt.GotPong(priv6Addr) + bap, err = pt.BestAddrPort() + assert.NoError(t, err) + assert.Equal(t, bap, priv6Addr) + + // Then add public ip6, + // this changes nothing + pt.GotPong(pub6Addr) + bap, err = pt.BestAddrPort() + assert.NoError(t, err) + assert.Equal(t, bap, priv6Addr) +} diff --git a/toversok/actors/peerstate/s_established.go b/toversok/actors/peerstate/s_established.go index 2e9010a..9d70ba1 100644 --- a/toversok/actors/peerstate/s_established.go +++ b/toversok/actors/peerstate/s_established.go @@ -16,6 +16,8 @@ const EstablishedPingInterval = time.Second * 2 type Established struct { *StateCommon + tracker *PingTracker + lastPingRecv time.Time lastPongRecv time.Time @@ -25,11 +27,6 @@ type Established struct { inactive bool inactiveSince time.Time - // TODO: this can flap, - // and basically picks the first best endpoint that the other client responds with, - // which may be non-ideal. - // Tailscale has logic to pick and switch between different endpoints, and sort them. - // We could possibly build this into the state logic. currentOutEndpoint netip.AddrPort knownInEndpoints map[netip.AddrPort]bool @@ -106,8 +103,16 @@ func (e *Established) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage return nil case *msgsess.Pong: - e.lastPongRecv = time.Now() - e.ackPongDirect(ap, clearMsg.Session, m) + if err := e.pongDirectValid(ap, clearMsg.Session, m); err != nil { + L(e).Warn("dropping invalid pong", "ap", ap.String(), "err", err) + } else { + e.lastPongRecv = time.Now() + e.tracker.GotPong(ap) + e.clearPongDirect(ap, clearMsg.Session, m) + + e.checkChangedPreferredEndpoint() + } + return nil default: @@ -181,3 +186,32 @@ func (e *Established) canTrustEndpoint(ap netip.AddrPort) bool { return false } + +func (e *Established) checkChangedPreferredEndpoint() { + bap, err := e.tracker.BestAddrPort() + if err != nil { + // this should not happen, at this point we have at least one happy pair + panic(err) + } + + if bap != e.currentOutEndpoint { + // not the best one, switch + e.switchToEndpoint(bap) + } +} + +func (e *Established) switchToEndpoint(ep netip.AddrPort) { + previous := e.currentOutEndpoint + + e.currentOutEndpoint = ep + + e.tm.OutConnUseAddrPort(e.peer, ep) + e.tm.DManSetAKA(e.peer, ep) + + L(e).Info( + "SWITCHED direct peer connection to better endpoint", + "peer", e.peer.Debug(), + "from", previous.String(), + "to", ep.String(), + ) +} diff --git a/toversok/actors/peerstate/s_inactive.go b/toversok/actors/peerstate/s_inactive.go index 0f7c2f1..21d5198 100644 --- a/toversok/actors/peerstate/s_inactive.go +++ b/toversok/actors/peerstate/s_inactive.go @@ -43,7 +43,12 @@ func (i *Inactive) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) P i.replyWithPongDirect(ap, clearMsg.Session, m) return nil case *msgsess.Pong: - i.ackPongDirect(ap, clearMsg.Session, m) + if err := i.pongDirectValid(ap, clearMsg.Session, m); err != nil { + L(i).Warn("dropping invalid pong", "ap", ap.String(), "err", err) + } else { + i.clearPongDirect(ap, clearMsg.Session, m) + } + return nil default: L(i).Warn("ignoring direct session message", diff --git a/toversok/actors/peerstate/s_trying.go b/toversok/actors/peerstate/s_trying.go index e2beea5..4cb65e6 100644 --- a/toversok/actors/peerstate/s_trying.go +++ b/toversok/actors/peerstate/s_trying.go @@ -47,7 +47,12 @@ func (t *Trying) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage) Pee t.replyWithPongDirect(ap, clearMsg.Session, m) return nil case *msgsess.Pong: - t.ackPongDirect(ap, clearMsg.Session, m) + if err := t.pongDirectValid(ap, clearMsg.Session, m); err != nil { + L(t).Warn("dropping invalid pong", "ap", ap.String(), "err", err) + } else { + t.clearPongDirect(ap, clearMsg.Session, m) + } + return nil default: L(t).Warn("ignoring direct session message", From 6975c136b0217f9a4d62e740a3a5593cf2cd7db2 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Tue, 18 Feb 2025 13:15:54 +0100 Subject: [PATCH 19/82] Prevent asymmetric status quo send ping to have pong be added to tracker, and then have it update outpath --- toversok/actors/peerstate/s_established.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/toversok/actors/peerstate/s_established.go b/toversok/actors/peerstate/s_established.go index 9d70ba1..4f01897 100644 --- a/toversok/actors/peerstate/s_established.go +++ b/toversok/actors/peerstate/s_established.go @@ -100,6 +100,18 @@ func (e *Established) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage e.lastPingRecv = time.Now() e.replyWithPongDirect(ap, clearMsg.Session, m) + + if ap != e.currentOutEndpoint { + // We're not sending pings to this, yet we may want to, to prevent asymmetric glare + pi := e.getPeerInfo() + if pi == nil { + // Peer info unavailable + return nil + } + + e.tm.SendPingDirect(ap, e.peer, pi.Session) + } + return nil case *msgsess.Pong: From 22903651f26d5804e7e3c796926dc8c50eab3d01 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Tue, 18 Feb 2025 14:18:44 +0100 Subject: [PATCH 20/82] Add stack print entries for recovers --- toversok/actors/a_conn.go | 5 +++-- toversok/actors/a_direct.go | 6 ++++-- toversok/actors/a_eman.go | 3 ++- toversok/actors/a_relay.go | 5 +++-- toversok/actors/a_sman.go | 3 ++- toversok/actors/a_sockrecv.go | 3 ++- toversok/actors/a_tman.go | 3 ++- 7 files changed, 18 insertions(+), 10 deletions(-) diff --git a/toversok/actors/a_conn.go b/toversok/actors/a_conn.go index 245d0fb..9f04744 100644 --- a/toversok/actors/a_conn.go +++ b/toversok/actors/a_conn.go @@ -5,6 +5,7 @@ import ( "errors" "net" "net/netip" + "runtime/debug" "time" "github.com/edup2p/common/types" @@ -56,7 +57,7 @@ func MakeOutConn(udp types.UDPConn, peer key.NodePublic, homeRelay int64, s *Sta func (oc *OutConn) Run() { defer func() { if v := recover(); v != nil { - L(oc).Error("panicked", "panic", v) + L(oc).Error("panicked", "panic", v, "stack", string(debug.Stack())) oc.Cancel() bail(oc.ctx, v) } @@ -218,7 +219,7 @@ func MakeInConn(udp types.UDPConn, peer key.NodePublic, s *Stage) *InConn { func (ic *InConn) Run() { defer func() { if v := recover(); v != nil { - L(ic).Error("panicked", "panic", v) + L(ic).Error("panicked", "panic", v, "stack", string(debug.Stack())) ic.Cancel() ic.Close() bail(ic.ctx, v) diff --git a/toversok/actors/a_direct.go b/toversok/actors/a_direct.go index e7eadc3..f6057ae 100644 --- a/toversok/actors/a_direct.go +++ b/toversok/actors/a_direct.go @@ -5,6 +5,7 @@ import ( "log/slog" "net/netip" "runtime" + "runtime/debug" "github.com/edup2p/common/types" "github.com/edup2p/common/types/ifaces" @@ -41,7 +42,7 @@ func (s *Stage) makeDM(udpSocket types.UDPConn) *DirectManager { func (dm *DirectManager) Run() { defer func() { if v := recover(); v != nil { - L(dm).Error("panicked", "panic", v) + L(dm).Error("panicked", "panic", v, "stack", string(debug.Stack())) dm.Cancel() bail(dm.ctx, v) } @@ -132,8 +133,9 @@ func (dr *DirectRouter) Push(frame ifaces.DirectedPeerFrame) { func (dr *DirectRouter) Run() { defer func() { if v := recover(); v != nil { - // TODO logging + L(dr).Error("panicked", "panic", v, "stack", string(debug.Stack())) dr.Cancel() + bail(dr.ctx, v) } }() diff --git a/toversok/actors/a_eman.go b/toversok/actors/a_eman.go index 6d6c8e2..0aae096 100644 --- a/toversok/actors/a_eman.go +++ b/toversok/actors/a_eman.go @@ -5,6 +5,7 @@ import ( "fmt" "net" "net/netip" + "runtime/debug" "slices" "time" @@ -67,7 +68,7 @@ type stunResponse struct { func (em *EndpointManager) Run() { defer func() { if v := recover(); v != nil { - L(em).Error("panicked", "panic", v) + L(em).Error("panicked", "panic", v, "stack", string(debug.Stack())) em.Cancel() bail(em.ctx, v) } diff --git a/toversok/actors/a_relay.go b/toversok/actors/a_relay.go index 6dcb82b..6ee7370 100644 --- a/toversok/actors/a_relay.go +++ b/toversok/actors/a_relay.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "runtime" + "runtime/debug" "time" "github.com/edup2p/common/types" @@ -281,7 +282,7 @@ func (s *Stage) makeRM() *RelayManager { func (rm *RelayManager) Run() { defer func() { if v := recover(); v != nil { - L(rm).Error("panicked", "panic", v) + L(rm).Error("panicked", "panic", v, "stack", string(debug.Stack())) rm.Cancel() bail(rm.ctx, v) } @@ -456,7 +457,7 @@ func (rr *RelayRouter) Push(frame ifaces.RelayedPeerFrame) { func (rr *RelayRouter) Run() { defer func() { if v := recover(); v != nil { - L(rr).Warn("panicked", "error", v) + L(rr).Warn("panicked", "error", v, "stack", string(debug.Stack())) rr.Cancel() bail(rr.ctx, v) } diff --git a/toversok/actors/a_sman.go b/toversok/actors/a_sman.go index 7249e73..3dd8f60 100644 --- a/toversok/actors/a_sman.go +++ b/toversok/actors/a_sman.go @@ -2,6 +2,7 @@ package actors import ( "fmt" + "runtime/debug" "slices" "github.com/edup2p/common/types/key" @@ -38,7 +39,7 @@ func (s *Stage) makeSM(priv func() *key.SessionPrivate) *SessionManager { func (sm *SessionManager) Run() { defer func() { if v := recover(); v != nil { - L(sm).Error("panicked", "panic", v) + L(sm).Error("panicked", "panic", v, "stack", string(debug.Stack())) sm.Cancel() bail(sm.ctx, v) } diff --git a/toversok/actors/a_sockrecv.go b/toversok/actors/a_sockrecv.go index 7e1cb79..4cb9a50 100644 --- a/toversok/actors/a_sockrecv.go +++ b/toversok/actors/a_sockrecv.go @@ -7,6 +7,7 @@ import ( "log/slog" "net" "net/netip" + "runtime/debug" "slices" "time" @@ -41,7 +42,7 @@ func MakeSockRecv(ctx context.Context, udp types.UDPConn) *SockRecv { func (r *SockRecv) Run() { defer func() { if v := recover(); v != nil { - L(r).Error("panicked", "err", v) + L(r).Error("panicked", "err", v, "stack", string(debug.Stack())) r.Cancel() bail(r.ctx, v) } diff --git a/toversok/actors/a_tman.go b/toversok/actors/a_tman.go index 36447ab..40a0258 100644 --- a/toversok/actors/a_tman.go +++ b/toversok/actors/a_tman.go @@ -3,6 +3,7 @@ package actors import ( "maps" "net/netip" + "runtime/debug" "time" "github.com/edup2p/common/toversok/actors/peerstate" @@ -51,7 +52,7 @@ func (s *Stage) makeTM() *TrafficManager { func (tm *TrafficManager) Run() { defer func() { if v := recover(); v != nil { - L(tm).Error("panicked", "error", v) + L(tm).Error("panicked", "error", v, "stack", string(debug.Stack())) tm.Cancel() tm.Close() bail(tm.ctx, v) From 8ed5b3b70ba3f9e0bda6ec5838e78c3d917ed186 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Tue, 18 Feb 2025 14:22:37 +0100 Subject: [PATCH 21/82] Add tracker carry-forward to Booting and Established --- toversok/actors/peerstate/e_t_finalising.go | 1 + toversok/actors/peerstate/s_t_booting.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/toversok/actors/peerstate/e_t_finalising.go b/toversok/actors/peerstate/e_t_finalising.go index 2d7e0f7..f5a1f92 100644 --- a/toversok/actors/peerstate/e_t_finalising.go +++ b/toversok/actors/peerstate/e_t_finalising.go @@ -30,6 +30,7 @@ func (f *Finalizing) OnTick() PeerState { return LogTransition(f, &Booting{ StateCommon: f.StateCommon, + tracker: f.tracker, ap: bap, }) } diff --git a/toversok/actors/peerstate/s_t_booting.go b/toversok/actors/peerstate/s_t_booting.go index 68f6519..e4e2134 100644 --- a/toversok/actors/peerstate/s_t_booting.go +++ b/toversok/actors/peerstate/s_t_booting.go @@ -12,6 +12,8 @@ import ( type Booting struct { *StateCommon + tracker *PingTracker + ap netip.AddrPort } @@ -27,6 +29,7 @@ func (b *Booting) OnTick() PeerState { return LogTransition(b, &Established{ StateCommon: b.StateCommon, + tracker: b.tracker, lastPingRecv: time.Now(), lastPongRecv: time.Now(), nextPingDeadline: time.Now(), From 251ab061e0d025ba3addbaa44588abbde2cacfbc Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 20 Feb 2025 11:28:11 +0100 Subject: [PATCH 22/82] change task label to task type --- .github/ISSUE_TEMPLATE/task.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/task.md b/.github/ISSUE_TEMPLATE/task.md index 7ea27b9..622779f 100644 --- a/.github/ISSUE_TEMPLATE/task.md +++ b/.github/ISSUE_TEMPLATE/task.md @@ -2,7 +2,7 @@ name: Task about: Template for the smallest actionable chunk title: '' -labels: Task +type: Task assignees: '' --- From e65a61f720ebeb2ae553eed9aaefd69d24af6e49 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 20 Feb 2025 10:10:17 +0100 Subject: [PATCH 23/82] add disconnect type and client handling --- cmd/dev_client/main.go | 4 ++++ toversok/control_conn.go | 9 +++++++++ toversok/engine.go | 4 ++++ toversok/session.go | 4 ++++ types/control/client.go | 13 +++++++++++-- types/control/conn.go | 9 ++++++--- types/ifaces/control.go | 3 +++ types/msgcontrol/msg.go | 17 ++++++++++++++--- types/msgcontrol/msg_iface.go | 12 ++++++++---- 9 files changed, 63 insertions(+), 12 deletions(-) diff --git a/cmd/dev_client/main.go b/cmd/dev_client/main.go index 0176404..97abfcd 100644 --- a/cmd/dev_client/main.go +++ b/cmd/dev_client/main.go @@ -896,6 +896,10 @@ func (s *StokControl) UpdateHomeRelay(i int64) error { return nil } +func (s *StokControl) Context() context.Context { + return context.Background() +} + func (s *StokControl) InstallCallbacks(callbacks ifaces.ControlCallbacks) { s.callback = callbacks diff --git a/toversok/control_conn.go b/toversok/control_conn.go index 4fa2a78..0059c51 100644 --- a/toversok/control_conn.go +++ b/toversok/control_conn.go @@ -232,6 +232,8 @@ func (rcs *ResumableControlSession) Run() { } } +var ErrDisconnected = errors.New("control requested disconnect") + func (rcs *ResumableControlSession) Handle(msg msgcontrol.ControlMessage) error { slog.Debug("Handle", "msg", msg) @@ -265,6 +267,9 @@ func (rcs *ResumableControlSession) Handle(msg msgcontrol.ControlMessage) error return rcs.ExpectCallbacks().RemovePeer(m.PubKey) case *msgcontrol.RelayUpdate: return rcs.ExpectCallbacks().UpdateRelays(m.Relays) + case *msgcontrol.Disconnect: + rcs.client.Cancel(fmt.Errorf("received disconnect: %w, %w", ErrDisconnected, m.RetryStrategy)) + return nil default: return fmt.Errorf("got unexpected message from control: %v", msg) } @@ -346,6 +351,10 @@ func (rcs *ResumableControlSession) ExpectCallbacks() ifaces.ControlCallbacks { return rcs.callbacks } +func (rcs *ResumableControlSession) Context() context.Context { + return rcs.ctx +} + func (rcs *ResumableControlSession) InstallCallbacks(callbacks ifaces.ControlCallbacks) { rcs.callbackLock.Lock() defer rcs.callbackLock.Unlock() diff --git a/toversok/engine.go b/toversok/engine.go index 606af7b..d4b9a31 100644 --- a/toversok/engine.go +++ b/toversok/engine.go @@ -374,6 +374,10 @@ func (f *FakeControl) IPv6() netip.Prefix { return f.ipv6 } +func (f *FakeControl) Context() context.Context { + return context.Background() +} + func (f *FakeControl) InstallCallbacks(ifaces.ControlCallbacks) { // NOP } diff --git a/toversok/session.go b/toversok/session.go index c6a9fe7..4b447a0 100644 --- a/toversok/session.go +++ b/toversok/session.go @@ -2,6 +2,7 @@ package toversok import ( "context" + "errors" "fmt" "log/slog" "net/netip" @@ -77,6 +78,9 @@ func SetupSession( sess.stage = actors.MakeStage(sess.ctx, getNodePriv, sess.getPriv, getExtSock, sess.wg.ConnFor, cc, nil) cc.InstallCallbacks(sess) + context.AfterFunc(cc.Context(), func() { + sess.ccc(errors.New("resumable control session exited")) + }) return sess, nil } diff --git a/types/control/client.go b/types/control/client.go index e20a09c..1d807b3 100644 --- a/types/control/client.go +++ b/types/control/client.go @@ -16,6 +16,7 @@ import ( type Client struct { ctx context.Context + ccc context.CancelCauseFunc cc *Conn @@ -31,10 +32,13 @@ type Client struct { } func EstablishClient(parentCtx context.Context, mc types.MetaConn, brw *bufio.ReadWriter, timeout time.Duration, getPriv func() *key.NodePrivate, getSess func() *key.SessionPrivate, controlKey key.ControlPublic, session *string, logon types.LogonCallback) (*Client, error) { + ctx, ccc := context.WithCancelCause(parentCtx) + c := &Client{ - ctx: parentCtx, + ctx: ctx, + ccc: ccc, - cc: NewConn(parentCtx, mc, brw), + cc: NewConn(ctx, mc, brw), getPriv: getPriv, getSess: getSess, @@ -215,3 +219,8 @@ func (c *Client) Close() { slog.Error("error when closing control client", "err", err) } } + +func (c *Client) Cancel(err error) { + c.ccc(err) + c.Close() +} diff --git a/types/control/conn.go b/types/control/conn.go index b339461..b77b7e6 100644 --- a/types/control/conn.go +++ b/types/control/conn.go @@ -94,8 +94,6 @@ func (c *Conn) Read(ttfbTimeout time.Duration) (msgcontrol.ControlMessage, error to = new(msgcontrol.LogonAccept) case msgcontrol.LogonRejectType: to = new(msgcontrol.LogonReject) - case msgcontrol.LogoutType: - to = new(msgcontrol.Logout) case msgcontrol.PingType: to = new(msgcontrol.Ping) case msgcontrol.PongType: @@ -113,8 +111,13 @@ func (c *Conn) Read(ttfbTimeout time.Duration) (msgcontrol.ControlMessage, error to = new(msgcontrol.PeerRemove) case msgcontrol.RelayUpdateType: to = new(msgcontrol.RelayUpdate) + case msgcontrol.LogoutType: + to = new(msgcontrol.Logout) + case msgcontrol.DisconnectType: + to = new(msgcontrol.Disconnect) + default: - panic(fmt.Sprintf("Unknown type %v", typ)) + return nil, fmt.Errorf("unknown type %v", typ) } if err = c.UnmarshalInto(data, to); err != nil { diff --git a/types/ifaces/control.go b/types/ifaces/control.go index 1c205fb..9852a78 100644 --- a/types/ifaces/control.go +++ b/types/ifaces/control.go @@ -1,6 +1,7 @@ package ifaces import ( + "context" "net/netip" "github.com/edup2p/common/types/key" @@ -55,6 +56,8 @@ type ControlInterface interface { type ControlSession interface { ControlInterface + Context() context.Context + // InstallCallbacks installs the current session's callbacks to another interface. // // This interface will be informed of updates from the control server. diff --git a/types/msgcontrol/msg.go b/types/msgcontrol/msg.go index 8b13a81..3ca3c9a 100644 --- a/types/msgcontrol/msg.go +++ b/types/msgcontrol/msg.go @@ -18,7 +18,6 @@ const ( LogonDeviceKeyType LogonAcceptType LogonRejectType - LogoutType PingType PongType ) @@ -30,6 +29,8 @@ const ( PeerUpdateType PeerRemoveType RelayUpdateType + LogoutType + DisconnectType ) // === handshake phase @@ -109,8 +110,6 @@ type LogonReject struct { RetryAfter time.Duration `json:",omitempty"` } -type Logout struct{} - type Ping struct { // random data encrypted with shared key (control priv x client pub) // to be signed with shared key of nodekey and sesskey @@ -126,6 +125,18 @@ type Pong struct { // === during session +// -> control +type Logout struct{} + +// -> client +type Disconnect struct { + Reason string + + RetryStrategy RetryStrategyType `json:",omitempty"` + + RetryAfter time.Duration `json:",omitempty"` +} + // -> control type EndpointUpdate struct { Endpoints []netip.AddrPort diff --git a/types/msgcontrol/msg_iface.go b/types/msgcontrol/msg_iface.go index b246f00..7fc4841 100644 --- a/types/msgcontrol/msg_iface.go +++ b/types/msgcontrol/msg_iface.go @@ -32,10 +32,6 @@ func (c *LogonReject) CMsgType() ControlMessageType { return LogonRejectType } -func (c *Logout) CMsgType() ControlMessageType { - return LogoutType -} - func (c *Ping) CMsgType() ControlMessageType { return PingType } @@ -67,3 +63,11 @@ func (c *PeerRemove) CMsgType() ControlMessageType { func (c *RelayUpdate) CMsgType() ControlMessageType { return RelayUpdateType } + +func (c *Logout) CMsgType() ControlMessageType { + return LogoutType +} + +func (c *Disconnect) CMsgType() ControlMessageType { + return DisconnectType +} From e32f559f3e68d56f4bc81b9729d3083085633c02 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 20 Feb 2025 11:20:08 +0100 Subject: [PATCH 24/82] add more expiry in places, and also to observer --- cmd/control_server/main.go | 6 +++-- cmd/dev_client/main.go | 5 ++++ test_suite/control_server/main.go | 6 +++-- toversok/actors/a_eman_test.go | 9 +++++-- toversok/control_conn.go | 26 ++++++++++++++++++++ toversok/engine.go | 40 +++++++++++++++++++++++++------ toversok/observer.go | 4 +++- toversok/session.go | 12 ++++++---- types/control/client.go | 4 ++++ types/control/iface.go | 23 ++++++++++++++---- types/control/logic.go | 30 +++++++++++++++++++++++ types/control/server_session.go | 24 ++++++++++++++++++- types/ifaces/control.go | 4 ++++ types/msgcontrol/msg.go | 2 ++ 14 files changed, 170 insertions(+), 25 deletions(-) diff --git a/cmd/control_server/main.go b/cmd/control_server/main.go index a75ee1c..91cbfde 100644 --- a/cmd/control_server/main.go +++ b/cmd/control_server/main.go @@ -202,10 +202,12 @@ func (cs *ControlServer) OnDeviceKey(sess control.SessID, deviceKey string) { slog.Info("OnDeviceKey", "sess", sess, "deviceKey", deviceKey) } -func (cs *ControlServer) OnSessionFinalize(sess control.SessID, cid control.ClientID) (netip.Prefix, netip.Prefix) { +func (cs *ControlServer) OnSessionFinalize(sess control.SessID, cid control.ClientID) (netip.Prefix, netip.Prefix, time.Time) { slog.Info("OnSessionFinalize", "sess", sess, "cid", cid) - return cs.getIPs(key.NodePublic(cid)) + ip4, ip6 := cs.getIPs(key.NodePublic(cid)) + + return ip4, ip6, time.Now().Add(time.Hour * 24 * 7) } func (cs *ControlServer) OnSessionDestroy(sess control.SessID, cid control.ClientID) { diff --git a/cmd/dev_client/main.go b/cmd/dev_client/main.go index 97abfcd..70b08fc 100644 --- a/cmd/dev_client/main.go +++ b/cmd/dev_client/main.go @@ -17,6 +17,7 @@ import ( "strconv" "strings" "sync" + "time" "github.com/abiosoft/ishell/v2" "github.com/edup2p/common/extwg" @@ -884,6 +885,10 @@ func (s *StokControl) IPv6() netip.Prefix { return *s.ip6 } +func (s *StokControl) Expiry() time.Time { + return time.Time{} +} + func (s *StokControl) UpdateEndpoints(endpoints []netip.AddrPort) error { slog.Info("called UpdateEndpoints", "endpoints", endpoints) diff --git a/test_suite/control_server/main.go b/test_suite/control_server/main.go index 7e24aad..64eced6 100644 --- a/test_suite/control_server/main.go +++ b/test_suite/control_server/main.go @@ -111,10 +111,12 @@ func (cs *ControlServer) OnDeviceKey(sess control.SessID, deviceKey string) { slog.Info("OnDeviceKey", "sess", sess, "deviceKey", deviceKey) } -func (cs *ControlServer) OnSessionFinalize(sess control.SessID, cid control.ClientID) (netip.Prefix, netip.Prefix) { +func (cs *ControlServer) OnSessionFinalize(sess control.SessID, cid control.ClientID) (netip.Prefix, netip.Prefix, time.Time) { slog.Info("OnSessionFinalize", "sess", sess, "cid", cid) - return cs.getIPs(key.NodePublic(cid)) + ip4, ip6 := cs.getIPs(key.NodePublic(cid)) + + return ip4, ip6, time.Time{} } func (cs *ControlServer) OnSessionDestroy(sess control.SessID, cid control.ClientID) { diff --git a/toversok/actors/a_eman_test.go b/toversok/actors/a_eman_test.go index bfd667c..f511f9f 100644 --- a/toversok/actors/a_eman_test.go +++ b/toversok/actors/a_eman_test.go @@ -20,8 +20,9 @@ type MockControl struct { controlKey func() key.ControlPublic - ipv4 func() netip.Prefix - ipv6 func() netip.Prefix + ipv4 func() netip.Prefix + ipv6 func() netip.Prefix + expiry func() time.Time updateEndpoints func([]netip.AddrPort) error updateHomeRelay func(int64) error @@ -39,6 +40,10 @@ func (m *MockControl) IPv6() netip.Prefix { return m.ipv6() } +func (m *MockControl) Expiry() time.Time { + return m.expiry() +} + func (m *MockControl) UpdateEndpoints(endpoints []netip.AddrPort) error { m.endpoints = endpoints return m.updateEndpoints(endpoints) diff --git a/toversok/control_conn.go b/toversok/control_conn.go index 0059c51..a3fbe64 100644 --- a/toversok/control_conn.go +++ b/toversok/control_conn.go @@ -50,6 +50,7 @@ type ResumableControlSession struct { // Airlifted out of Client, expected to stay the same as long as the session does ipv4 netip.Prefix ipv6 netip.Prefix + expiry time.Time controlKey key.ControlPublic session string @@ -215,6 +216,27 @@ func (rcs *ResumableControlSession) Run() { panic("not implemented") } + if rcs.ipv4 != client.IPv4 { + slog.Error("control-given IPv4 prefix is different than cached IPv4, bailing...", "cached", rcs.ipv4, "given", client.IPv4) + rcs.ccc(fmt.Errorf("IPv4 changed from %s to %s", rcs.ipv4, client.IPv4)) + + return + } + + if rcs.ipv6 != client.IPv6 { + slog.Error("control-given IPv6 prefix is different than cached IPv6, bailing...", "cached", rcs.ipv6, "given", client.IPv6) + rcs.ccc(fmt.Errorf("IPv6 changed from %s to %s", rcs.ipv6, client.IPv6)) + + return + } + + if rcs.expiry != client.Expiry { + slog.Error("control-given expiry is different than cached expiry, bailing...", "cached", rcs.expiry, "given", client.Expiry) + rcs.ccc(fmt.Errorf("expiry changed from %s to %s", rcs.expiry, client.Expiry)) + + return + } + // retry/resume continue } @@ -340,6 +362,10 @@ func (rcs *ResumableControlSession) IPv6() netip.Prefix { return rcs.ipv6 } +func (rcs *ResumableControlSession) Expiry() time.Time { + return rcs.expiry +} + func (rcs *ResumableControlSession) ExpectCallbacks() ifaces.ControlCallbacks { rcs.callbackLock.RLock() defer rcs.callbackLock.RUnlock() diff --git a/toversok/engine.go b/toversok/engine.go index d4b9a31..6d005f3 100644 --- a/toversok/engine.go +++ b/toversok/engine.go @@ -147,7 +147,10 @@ func (e *Engine) installSession(allowLogon bool) error { logon = func(url string, _ chan<- string) error { // TODO register/use device key channel - e.state.currentLoginURL = url + e.state.alter(func(o *stateObserver) { + o.currentLoginURL = url + }) + e.state.change(CreatingSession, NeedsLogin) return nil } @@ -159,6 +162,10 @@ func (e *Engine) installSession(allowLogon bool) error { return fmt.Errorf("failed to setup session: %w", err) } + e.state.alter(func(o *stateObserver) { + o.expiry = e.sess.cs.Expiry() + }) + if !(e.state.change(CreatingSession, Established) || e.state.change(NeedsLogin, Established)) { e.ccc(errors.New("incorrect state transition")) panic("incorrect state transition to established") @@ -188,10 +195,12 @@ func newStateObserver() stateObserver { } type stateObserver struct { - mu sync.Mutex - state EngineState + mu sync.Mutex + state EngineState + callbacks []func(state EngineState) + currentLoginURL string - callbacks []func(state EngineState) + expiry time.Time } func (s *stateObserver) CurrentState() EngineState { @@ -218,9 +227,15 @@ func (s *stateObserver) GetNeedsLoginState() (url string, err error) { return s.currentLoginURL, nil } -func (s *stateObserver) GetEstablishedState() { - // TODO implement me - panic("implement me") +func (s *stateObserver) GetEstablishedState() (time.Time, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.state != Established { + return time.Time{}, ErrWrongState + } + + return s.expiry, nil } func (s *stateObserver) change(oldState, newState EngineState) bool { @@ -251,6 +266,13 @@ func (s *stateObserver) set(newState EngineState) { s.asyncFireCallbacks(newState) } +func (s *stateObserver) alter(f func(observer *stateObserver)) { + s.mu.Lock() + defer s.mu.Unlock() + + f(s) +} + func (s *stateObserver) asyncFireCallbacks(state EngineState) { for _, cb := range s.callbacks { go cb(state) @@ -374,6 +396,10 @@ func (f *FakeControl) IPv6() netip.Prefix { return f.ipv6 } +func (f *FakeControl) Expiry() time.Time { + return time.Time{} +} + func (f *FakeControl) Context() context.Context { return context.Background() } diff --git a/toversok/observer.go b/toversok/observer.go index 8a4c828..2dc66f2 100644 --- a/toversok/observer.go +++ b/toversok/observer.go @@ -1,5 +1,7 @@ package toversok +import "time" + // Observer functions as a state observer for the Engine, effectively allowing calling clients to peek into the engine state in an abstracted way. type Observer interface { RegisterStateChangeListener(func(state EngineState)) @@ -7,7 +9,7 @@ type Observer interface { CurrentState() EngineState GetNeedsLoginState() (url string, err error) - GetEstablishedState() // TODO + GetEstablishedState() (expiry time.Time, err error) } type EngineState byte diff --git a/toversok/session.go b/toversok/session.go index 4b447a0..c99d438 100644 --- a/toversok/session.go +++ b/toversok/session.go @@ -23,6 +23,7 @@ type Session struct { wg WireGuardController fw FirewallController + cs ifaces.ControlSession quarantineMu sync.Mutex quarantinedPeers map[key.NodePublic]bool @@ -57,13 +58,14 @@ func SetupSession( stage: nil, } - cc, err := co.CreateClient(sess.ctx, getNodePriv, sess.getPriv, logon) + var err error + sess.cs, err = co.CreateClient(sess.ctx, getNodePriv, sess.getPriv, logon) if err != nil { sess.ccc(err) return nil, fmt.Errorf("could not create control client: %w", err) } - if sess.wg, err = wg.Controller(*getNodePriv(), cc.IPv4(), cc.IPv6()); err != nil { + if sess.wg, err = wg.Controller(*getNodePriv(), sess.cs.IPv4(), sess.cs.IPv6()); err != nil { err = fmt.Errorf("could not init wireguard: %w", err) sess.ccc(err) return nil, err @@ -75,10 +77,10 @@ func SetupSession( return nil, err } - sess.stage = actors.MakeStage(sess.ctx, getNodePriv, sess.getPriv, getExtSock, sess.wg.ConnFor, cc, nil) + sess.stage = actors.MakeStage(sess.ctx, getNodePriv, sess.getPriv, getExtSock, sess.wg.ConnFor, sess.cs, nil) - cc.InstallCallbacks(sess) - context.AfterFunc(cc.Context(), func() { + sess.cs.InstallCallbacks(sess) + context.AfterFunc(sess.cs.Context(), func() { sess.ccc(errors.New("resumable control session exited")) }) diff --git a/types/control/client.go b/types/control/client.go index 1d807b3..32f0831 100644 --- a/types/control/client.go +++ b/types/control/client.go @@ -29,6 +29,8 @@ type Client struct { IPv4 netip.Prefix IPv6 netip.Prefix + + Expiry time.Time } func EstablishClient(parentCtx context.Context, mc types.MetaConn, brw *bufio.ReadWriter, timeout time.Duration, getPriv func() *key.NodePrivate, getSess func() *key.SessionPrivate, controlKey key.ControlPublic, session *string, logon types.LogonCallback) (*Client, error) { @@ -128,6 +130,8 @@ func (c *Client) Handshake(timeout time.Duration, logon types.LogonCallback) err c.IPv4 = m.IP4 c.IPv6 = m.IP6 + c.Expiry = m.AuthExpiry + slog.Debug("logon accepted", "as-peer", nodePubKey.Debug(), "as-sess", sessPubKey.Debug(), "with-sess-id", types.PtrOr(c.SessionID, ""), "with-ipv4", c.IPv4.String(), "with-ipv6", c.IPv6.String()) return nil diff --git a/types/control/iface.go b/types/control/iface.go index 5fc6b52..85c52b4 100644 --- a/types/control/iface.go +++ b/types/control/iface.go @@ -3,6 +3,7 @@ package control import ( "errors" "net/netip" + "time" "github.com/edup2p/common/types/key" ) @@ -12,9 +13,12 @@ type ( SessID string ) -var ErrSessionDoesNotExist = errors.New("session does not exist") - -var ErrSessionIsNotAuthenticating = errors.New("session is not authenticating") +var ( + ErrSessionDoesNotExist = errors.New("session does not exist") + ErrSessionIsNotAuthenticating = errors.New("session is not authenticating") + ErrNeedsDisconnect = errors.New("session needs disconnect") + ErrClientNotConnected = errors.New("client is not connected") +) // ServerLogic denotes exposed functions that a control server must provide for any business logic to interface with it. type ServerLogic interface { @@ -36,6 +40,7 @@ type ServerLogic interface { SendAuthURL(id SessID, url string) error // AcceptAuthentication will accept the pending authentication of the indicated session ID. // Must be called, or RejectAuthentication must be called. + // Second time argument dictates for how long the // Will error if the session is not pending authentication. AcceptAuthentication(SessID) error // RejectAuthentication will reject the pending authentication of the indicated session ID. @@ -43,6 +48,13 @@ type ServerLogic interface { // Will error if the session is not pending authentication. RejectAuthentication(id SessID, reason string) error + // DisconnectSession will disconnect a running client session (and invalidate its ID), if it exists. + // Will error if session does not exist. + DisconnectSession(id SessID) error + // DisconnectClient will disconnect a running session per client (and invalidate its ID), if its connected. + // Will error if client is not connected. + DisconnectClient(id ClientID) error + /// The following functions pertain to client-client networking visibility. // GetVisibilityPairs gets all pairs of a particular ClientID. @@ -75,8 +87,9 @@ type ServerCallbacks interface { OnDeviceKey(id SessID, key string) // OnSessionFinalize is called right after ServerLogic.AcceptAuthentication, but before that message is sent to the client. - // The client needs to known which virtual IPs it can use, and this function will provide it to the control server. - OnSessionFinalize(SessID, ClientID) (netip.Prefix, netip.Prefix) + // The client needs to known which virtual IPs it can use, and the expiry time of the authentication, + // and this function will provide it to the control server. + OnSessionFinalize(SessID, ClientID) (netip.Prefix, netip.Prefix, time.Time) // OnSessionDestroy is called after the client has been disconnected. OnSessionDestroy(SessID, ClientID) diff --git a/types/control/logic.go b/types/control/logic.go index dc9b81b..5de4219 100644 --- a/types/control/logic.go +++ b/types/control/logic.go @@ -217,6 +217,36 @@ func (s *Server) GetVisibilityPairs(id ClientID) (map[ClientID]VisibilityPair, e return pairs, nil } +func (s *Server) DisconnectSession(id SessID) error { + s.sessLock.RLock() + defer s.sessLock.RUnlock() + + sess, ok := s.sessByID[string(id)] + + if !ok { + return ErrSessionDoesNotExist + } + + sess.Ccc(ErrNeedsDisconnect) + + return nil +} + +func (s *Server) DisconnectClient(id ClientID) error { + s.sessLock.RLock() + defer s.sessLock.RUnlock() + + sess, ok := s.sessByNode[key.NodePublic(id)] + + if !ok { + return ErrClientNotConnected + } + + sess.Ccc(ErrNeedsDisconnect) + + return nil +} + //nolint:unused func (s *Server) atomicDoVisibilityPairs(id key.NodePublic, f func(map[ClientID]VisibilityPair) error) error { s.sessLock.RLock() diff --git a/types/control/server_session.go b/types/control/server_session.go index 5892fab..9ca359a 100644 --- a/types/control/server_session.go +++ b/types/control/server_session.go @@ -45,6 +45,8 @@ type ServerSession struct { server *Server + expiry time.Time + // TODO // all synced state, known changes, queued changes, etc. } @@ -315,7 +317,7 @@ func (s *ServerSession) AuthenticateAccept() (err error) { } func (s *ServerSession) AuthAndStart() error { - s.IPv4, s.IPv6 = s.server.callbacks.OnSessionFinalize(SessID(s.ID), ClientID(s.Peer)) + s.IPv4, s.IPv6, s.expiry = s.server.callbacks.OnSessionFinalize(SessID(s.ID), ClientID(s.Peer)) err := s.AuthenticateAccept() if err != nil { @@ -335,6 +337,14 @@ func (s *ServerSession) Run() { go func() { <-s.Ctx.Done() + if errors.Is(s.Ctx.Err(), ErrNeedsDisconnect) { + if err := s.conn.Write(&msgcontrol.Disconnect{ + Reason: "control requested disconnect", + }); err != nil { + slog.Error("error writing disconnect message", "err", err) + } + } + s.Slog().Info("session exiting", "err", context.Cause(s.Ctx), "peer", s.Peer.Debug()) s.server.RemoveSession(s) @@ -389,6 +399,18 @@ func (s *ServerSession) Run() { return } + if s.expiry != (time.Time{}) { + go func() { + select { + case <-s.Ctx.Done(): + // FIXME on suspend/delay/wallclock change, this won't work properly, + // find a time-until api that deals with wall-clock differences + case <-time.After(time.Until(s.expiry)): + s.Ccc(ErrNeedsDisconnect) + } + }() + } + s.Slog().Info("established session") for { diff --git a/types/ifaces/control.go b/types/ifaces/control.go index 9852a78..0aa5093 100644 --- a/types/ifaces/control.go +++ b/types/ifaces/control.go @@ -3,6 +3,7 @@ package ifaces import ( "context" "net/netip" + "time" "github.com/edup2p/common/types/key" "github.com/edup2p/common/types/msgcontrol" @@ -45,6 +46,9 @@ type ControlInterface interface { // // As it is a netip.Prefix, it also includes the expected ipv6 range that all peers will be on. IPv6() netip.Prefix + // Expiry of the current control session, defaults to zero-value if there is no expiry, + // or session is not connected. + Expiry() time.Time // UpdateEndpoints informs the server of any changes in STUN-resolved endpoints. This is a set-replace operation. UpdateEndpoints([]netip.AddrPort) error diff --git a/types/msgcontrol/msg.go b/types/msgcontrol/msg.go index 3ca3c9a..85ce1be 100644 --- a/types/msgcontrol/msg.go +++ b/types/msgcontrol/msg.go @@ -74,6 +74,8 @@ type LogonAccept struct { IP4 netip.Prefix IP6 netip.Prefix + AuthExpiry time.Time + SessionID string } From c80bd369d468af29483c59466c3ef2d6e2342de3 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 20 Feb 2025 11:49:42 +0100 Subject: [PATCH 25/82] properly fix expiry handling and passdown --- toversok/control_conn.go | 40 ++++++++++++++++++--------------- toversok/engine.go | 8 +++++++ toversok/observer.go | 2 +- types/control/server_session.go | 15 +++++++------ 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/toversok/control_conn.go b/toversok/control_conn.go index a3fbe64..1516cdb 100644 --- a/toversok/control_conn.go +++ b/toversok/control_conn.go @@ -82,7 +82,10 @@ func CreateControlSession(ctx context.Context, opts dial.Opts, controlKey key.Co return nil, fmt.Errorf("could not create control session: %w", err) } - slog.Debug("created initial control connection") + slog.Debug( + "created initial control connection", + "ipv4", c.IPv4.String(), "ipv6", c.IPv6.String(), "expiry", c.Expiry, + ) rcs := &ResumableControlSession{ ctx: rcsCtx, @@ -90,6 +93,7 @@ func CreateControlSession(ctx context.Context, opts dial.Opts, controlKey key.Co ipv4: c.IPv4, ipv6: c.IPv6, + expiry: c.Expiry, controlKey: c.ControlKey, session: *c.SessionID, @@ -216,29 +220,29 @@ func (rcs *ResumableControlSession) Run() { panic("not implemented") } - if rcs.ipv4 != client.IPv4 { - slog.Error("control-given IPv4 prefix is different than cached IPv4, bailing...", "cached", rcs.ipv4, "given", client.IPv4) - rcs.ccc(fmt.Errorf("IPv4 changed from %s to %s", rcs.ipv4, client.IPv4)) + // retry/resume + continue + } - return - } + if rcs.ipv4 != client.IPv4 { + slog.Error("control-given IPv4 prefix is different than cached IPv4, bailing...", "cached", rcs.ipv4, "given", client.IPv4) + rcs.ccc(fmt.Errorf("IPv4 changed from %s to %s", rcs.ipv4, client.IPv4)) - if rcs.ipv6 != client.IPv6 { - slog.Error("control-given IPv6 prefix is different than cached IPv6, bailing...", "cached", rcs.ipv6, "given", client.IPv6) - rcs.ccc(fmt.Errorf("IPv6 changed from %s to %s", rcs.ipv6, client.IPv6)) + return + } - return - } + if rcs.ipv6 != client.IPv6 { + slog.Error("control-given IPv6 prefix is different than cached IPv6, bailing...", "cached", rcs.ipv6, "given", client.IPv6) + rcs.ccc(fmt.Errorf("IPv6 changed from %s to %s", rcs.ipv6, client.IPv6)) - if rcs.expiry != client.Expiry { - slog.Error("control-given expiry is different than cached expiry, bailing...", "cached", rcs.expiry, "given", client.Expiry) - rcs.ccc(fmt.Errorf("expiry changed from %s to %s", rcs.expiry, client.Expiry)) + return + } - return - } + if rcs.expiry != client.Expiry { + slog.Error("control-given expiry is different than cached expiry, bailing...", "cached", rcs.expiry, "given", client.Expiry) + rcs.ccc(fmt.Errorf("expiry changed from %s to %s", rcs.expiry, client.Expiry)) - // retry/resume - continue + return } slog.Debug("resumed control connection") diff --git a/toversok/engine.go b/toversok/engine.go index 6d005f3..22d9d99 100644 --- a/toversok/engine.go +++ b/toversok/engine.go @@ -335,6 +335,14 @@ func NewEngine( } else { e.slog().Error("could not get login state when prompted for it", "err", err) } + } else if state == Established { + expiry, err := e.Observer().GetEstablishedState() + if err != nil { + panic("should never happen") + } + //if expiry != (time.Time{}) { + slog.Info("established session with expiry", "expiry", expiry, "in", time.Until(expiry)) + //} } }) diff --git a/toversok/observer.go b/toversok/observer.go index 2dc66f2..4b4233b 100644 --- a/toversok/observer.go +++ b/toversok/observer.go @@ -9,7 +9,7 @@ type Observer interface { CurrentState() EngineState GetNeedsLoginState() (url string, err error) - GetEstablishedState() (expiry time.Time, err error) + GetEstablishedState() (expiry time.Time, err error) // TODO add ipv4,ipv6? } type EngineState byte diff --git a/types/control/server_session.go b/types/control/server_session.go index 9ca359a..66b686c 100644 --- a/types/control/server_session.go +++ b/types/control/server_session.go @@ -45,7 +45,7 @@ type ServerSession struct { server *Server - expiry time.Time + Expiry time.Time // TODO // all synced state, known changes, queued changes, etc. @@ -305,9 +305,10 @@ func (s *ServerSession) AuthenticateAccept() (err error) { s.Slog().Debug("AuthenticateAccept") if err = s.conn.Write(&msgcontrol.LogonAccept{ - IP4: s.IPv4, - IP6: s.IPv6, - SessionID: s.ID, + IP4: s.IPv4, + IP6: s.IPv6, + AuthExpiry: s.Expiry, + SessionID: s.ID, }); err != nil { err = fmt.Errorf("error when sending accept: %w", err) return @@ -317,7 +318,7 @@ func (s *ServerSession) AuthenticateAccept() (err error) { } func (s *ServerSession) AuthAndStart() error { - s.IPv4, s.IPv6, s.expiry = s.server.callbacks.OnSessionFinalize(SessID(s.ID), ClientID(s.Peer)) + s.IPv4, s.IPv6, s.Expiry = s.server.callbacks.OnSessionFinalize(SessID(s.ID), ClientID(s.Peer)) err := s.AuthenticateAccept() if err != nil { @@ -399,13 +400,13 @@ func (s *ServerSession) Run() { return } - if s.expiry != (time.Time{}) { + if s.Expiry != (time.Time{}) { go func() { select { case <-s.Ctx.Done(): // FIXME on suspend/delay/wallclock change, this won't work properly, // find a time-until api that deals with wall-clock differences - case <-time.After(time.Until(s.expiry)): + case <-time.After(time.Until(s.Expiry)): s.Ccc(ErrNeedsDisconnect) } }() From 0e3acdf14983b9652ad31614e1f5b1e6cb0eeac6 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 20 Feb 2025 11:51:03 +0100 Subject: [PATCH 26/82] handle zero time.Time{} objects --- toversok/engine.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/toversok/engine.go b/toversok/engine.go index 22d9d99..4e06ae4 100644 --- a/toversok/engine.go +++ b/toversok/engine.go @@ -340,9 +340,9 @@ func NewEngine( if err != nil { panic("should never happen") } - //if expiry != (time.Time{}) { - slog.Info("established session with expiry", "expiry", expiry, "in", time.Until(expiry)) - //} + if expiry != (time.Time{}) { + slog.Info("established session with expiry", "expiry", expiry, "in", time.Until(expiry)) + } } }) From 446a28fb8f8cc949e0c52d98055cf8a0109c79f1 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 21 Feb 2025 10:26:21 +0100 Subject: [PATCH 27/82] Properly clean up goroutines and contexts closes #112 --- toversok/actors/a_eman.go | 4 +- toversok/actors/stage.go | 5 ++- toversok/engine.go | 87 +++++++++++++++------------------------ toversok/fakecontrol.go | 50 ++++++++++++++++++++++ toversok/session.go | 1 - types/relay/client.go | 12 ++---- usrwg/bind.go | 21 ++++++++++ usrwg/wgusp.go | 4 +- 8 files changed, 115 insertions(+), 69 deletions(-) create mode 100644 toversok/fakecontrol.go diff --git a/toversok/actors/a_eman.go b/toversok/actors/a_eman.go index 0aae096..3435bac 100644 --- a/toversok/actors/a_eman.go +++ b/toversok/actors/a_eman.go @@ -324,6 +324,6 @@ func (em *EndpointManager) collectLocalEndpoints() []netip.Addr { } func (em *EndpointManager) Close() { - // TODO implement me - panic("implement me") + em.ticker.Stop() + em.stunTimeout.Stop() } diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index 9c889ec..9083e1f 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -48,14 +48,15 @@ func MakeStage( dialRelayFunc relayhttp.RelayDialFunc, ) ifaces.Stage { - ctx := context.WithoutCancel(pCtx) + // FIXME ??? why the fuck did we ever decide on this + // ctx := context.WithoutCancel(pCtx) if dialRelayFunc == nil { dialRelayFunc = relayhttp.Dial } s := &Stage{ - Ctx: ctx, + Ctx: pCtx, connMutex: sync.RWMutex{}, inConn: make(map[key.NodePublic]InConnActor), diff --git a/toversok/engine.go b/toversok/engine.go index 4e06ae4..766137b 100644 --- a/toversok/engine.go +++ b/toversok/engine.go @@ -11,7 +11,6 @@ import ( "time" "github.com/edup2p/common/types" - "github.com/edup2p/common/types/ifaces" "github.com/edup2p/common/types/key" ) @@ -66,18 +65,8 @@ func (e *Engine) start(allowLogon bool) error { e.sess.ccc(errors.New("engine state desynced, shutting down")) } - if e.dirty { - if err := e.wg.Reset(); err != nil { - e.slog().Error("dirty start: could not reset wireguard", "err", err) - e.state.set(NoSession) - return err - } - - if err := e.fw.Reset(); err != nil { - e.slog().Error("dirty start: could not reset firewall", "err", err) - e.state.set(NoSession) - return err - } + if err := e.maybeClean(); err != nil { + return fmt.Errorf("engine state cleaning failed: %w", err) } e.dirty = true @@ -93,6 +82,26 @@ func (e *Engine) Context() context.Context { return e.ctx } +func (e *Engine) maybeClean() error { + slog.Debug("maybeClean called", "dirty", e.dirty) + + if e.dirty { + if err := e.wg.Reset(); err != nil { + e.slog().Error("clean: could not reset wireguard", "err", err) + e.state.set(NoSession) + return err + } + + if err := e.fw.Reset(); err != nil { + e.slog().Error("clean: could not reset firewall", "err", err) + e.state.set(NoSession) + return err + } + } + + return nil +} + // StalledEngineRestartInterval represents how many seconds to wait before restarting an engine, // after it has stalled/failed. const StalledEngineRestartInterval = time.Second * 2 @@ -103,6 +112,12 @@ func (e *Engine) autoRestart() { slog.Info("autoRestart: will retry in 10 seconds") time.AfterFunc(StalledEngineRestartInterval, e.autoRestart) } + } else { + slog.Debug("will not auto-restart") + + if err := e.maybeClean(); err != nil { + slog.Error("engine state cleaning failed", "err", err) + } } } @@ -346,6 +361,12 @@ func NewEngine( } }) + context.AfterFunc(e.ctx, func() { + if err := e.maybeClean(); err != nil { + slog.Error("after-ctx: engine state cleaning failed", "err", err) + } + }) + return e, nil } @@ -385,43 +406,3 @@ func (e *Engine) SupplyDeviceKey(string) error { // TODO panic("not implemented") } - -type FakeControl struct { - controlKey key.ControlPublic - ipv4 netip.Prefix - ipv6 netip.Prefix -} - -func (f *FakeControl) ControlKey() key.ControlPublic { - return f.controlKey -} - -func (f *FakeControl) IPv4() netip.Prefix { - return f.ipv4 -} - -func (f *FakeControl) IPv6() netip.Prefix { - return f.ipv6 -} - -func (f *FakeControl) Expiry() time.Time { - return time.Time{} -} - -func (f *FakeControl) Context() context.Context { - return context.Background() -} - -func (f *FakeControl) InstallCallbacks(ifaces.ControlCallbacks) { - // NOP -} - -func (f *FakeControl) UpdateEndpoints([]netip.AddrPort) error { - // NOP - return nil -} - -func (f *FakeControl) UpdateHomeRelay(int64) error { - // NOP - return nil -} diff --git a/toversok/fakecontrol.go b/toversok/fakecontrol.go new file mode 100644 index 0000000..501bc23 --- /dev/null +++ b/toversok/fakecontrol.go @@ -0,0 +1,50 @@ +package toversok + +import ( + "context" + "net/netip" + "time" + + "github.com/edup2p/common/types/ifaces" + "github.com/edup2p/common/types/key" +) + +type FakeControl struct { + controlKey key.ControlPublic + ipv4 netip.Prefix + ipv6 netip.Prefix +} + +func (f *FakeControl) ControlKey() key.ControlPublic { + return f.controlKey +} + +func (f *FakeControl) IPv4() netip.Prefix { + return f.ipv4 +} + +func (f *FakeControl) IPv6() netip.Prefix { + return f.ipv6 +} + +func (f *FakeControl) Expiry() time.Time { + return time.Time{} +} + +func (f *FakeControl) Context() context.Context { + return context.Background() +} + +func (f *FakeControl) InstallCallbacks(ifaces.ControlCallbacks) { + // NOP +} + +func (f *FakeControl) UpdateEndpoints([]netip.AddrPort) error { + // NOP + return nil +} + +func (f *FakeControl) UpdateHomeRelay(int64) error { + // NOP + return nil +} diff --git a/toversok/session.go b/toversok/session.go index c99d438..cb18e61 100644 --- a/toversok/session.go +++ b/toversok/session.go @@ -44,7 +44,6 @@ func SetupSession( logon types.LogonCallback, ) (*Session, error) { ctx, ccc := context.WithCancelCause(engineCtx) - sCtx := context.WithValue(ctx, types.CCC, ccc) sess := &Session{ diff --git a/types/relay/client.go b/types/relay/client.go index 96c1478..3098739 100644 --- a/types/relay/client.go +++ b/types/relay/client.go @@ -152,10 +152,7 @@ func EstablishClient(parentCtx context.Context, mc types.MetaConn, brw *bufio.Re return nil, fmt.Errorf("could not reset deadline: %w", err) } - go func() { - <-c.ctx.Done() - c.Close() - }() + context.AfterFunc(c.ctx, c.Close) return c, nil } @@ -279,7 +276,7 @@ func (c *HTTPClient) Cancel(err error) { } func (c *HTTPClient) Close() { - if c.closed || context.Cause(c.ctx) != nil { + if c.closed { return } @@ -323,11 +320,8 @@ func (c *HTTPClient) RunReceive() { for { frTyp, frLen, err = readFrameHeader(c.reader) - select { - case <-c.ctx.Done(): + if c.ctx.Err() != nil { return - default: - // fallthrough } if err != nil { diff --git a/usrwg/bind.go b/usrwg/bind.go index 0958076..6ed67f2 100644 --- a/usrwg/bind.go +++ b/usrwg/bind.go @@ -5,8 +5,10 @@ import ( "errors" "fmt" "log/slog" + "net" "reflect" "runtime" + "runtime/debug" "sync" "time" @@ -21,6 +23,8 @@ type ToverSokBind struct { conns map[key.NodePublic]*ChannelConn connChange chan bool + permClosed bool + endpointMu sync.RWMutex endpoints map[key.NodePublic]*endpoint } @@ -46,6 +50,8 @@ func (b *ToverSokBind) Close() error { defer b.connMu.Unlock() defer b.endpointMu.Unlock() + slog.Debug("bind close called", "stack", string(debug.Stack())) + maps.Clear(b.endpoints) var errs []error @@ -62,11 +68,22 @@ func (b *ToverSokBind) Close() error { return fmt.Errorf("errors when closing connections: %w", errors.Join(errs...)) } + b.notifyConnChange() + return nil } +func (b *ToverSokBind) Cancel() error { + b.permClosed = true + return b.Close() +} + // ReadFromConns implements conn.ReceiveFunc func (b *ToverSokBind) ReadFromConns(packets [][]byte, sizes []int, eps []conn.Endpoint) (n int, err error) { + if b.isPermanentlyClosed() { + return 0, net.ErrClosed + } + // We get a keys slice that could potentially get immediately outdated, // but we use it to fill buffers from existing conns first. b.connMu.RLock() @@ -136,6 +153,10 @@ fill: return } +func (b *ToverSokBind) isPermanentlyClosed() bool { + return b.permClosed +} + func (b *ToverSokBind) waitForValueFromConns() ([]byte, *endpoint) { caseMap := b.buildConnsSelectCaseMap() connChangeCase := b.createConnChangeSelectCase() diff --git a/usrwg/wgusp.go b/usrwg/wgusp.go index 23cb1c8..d10156f 100644 --- a/usrwg/wgusp.go +++ b/usrwg/wgusp.go @@ -151,11 +151,11 @@ func (u *UserSpaceWireGuardController) ConnFor(node key.NodePublic) types.UDPCon } func (u *UserSpaceWireGuardController) Close() { - u.wgDev.Close() - if err := u.bind.Close(); err != nil { + if err := u.bind.Cancel(); err != nil { slog.Error("Failed to close wireguard bind", "err", err) } if err := u.router.Close(); err != nil { slog.Error("Failed to close router", "err", err) } + u.wgDev.Close() } From fa17a3a7dde4e2d860bf15e6514918a62779793b Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 21 Feb 2025 10:27:28 +0100 Subject: [PATCH 28/82] doAutoRestart = true Closes #113 --- toversok/engine.go | 1 + 1 file changed, 1 insertion(+) diff --git a/toversok/engine.go b/toversok/engine.go index 766137b..1f68bb4 100644 --- a/toversok/engine.go +++ b/toversok/engine.go @@ -47,6 +47,7 @@ type Engine struct { // // After the engine has successfully started once, it will automatically restart on any failure. func (e *Engine) Start() error { + e.doAutoRestart = true return e.start(true) } From f69c9267e4708b0c085ec2e0bf0013342478f9fc Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 21 Feb 2025 10:29:44 +0100 Subject: [PATCH 29/82] remove bind close debug line --- usrwg/bind.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/usrwg/bind.go b/usrwg/bind.go index 6ed67f2..654929b 100644 --- a/usrwg/bind.go +++ b/usrwg/bind.go @@ -8,7 +8,6 @@ import ( "net" "reflect" "runtime" - "runtime/debug" "sync" "time" @@ -50,8 +49,6 @@ func (b *ToverSokBind) Close() error { defer b.connMu.Unlock() defer b.endpointMu.Unlock() - slog.Debug("bind close called", "stack", string(debug.Stack())) - maps.Clear(b.endpoints) var errs []error From 73b309eb189488a2550883bee571b978e0a9f3aa Mon Sep 17 00:00:00 2001 From: Henk Date: Fri, 21 Feb 2025 16:04:16 +0100 Subject: [PATCH 30/82] Update python script to aggregate the data from multiple repetitions before saving and plotting the aggregated data --- test_suite/performance_test.sh | 15 ++--- test_suite/visualize_performance_tests.py | 75 ++++++++++++++++++----- 2 files changed, 63 insertions(+), 27 deletions(-) diff --git a/test_suite/performance_test.sh b/test_suite/performance_test.sh index a5e0de1..2ef309d 100755 --- a/test_suite/performance_test.sh +++ b/test_suite/performance_test.sh @@ -264,19 +264,12 @@ fi performance_test_value_array=$(echo $performance_test_values | tr ',' ' ') performance_test_value_array=($performance_test_value_array) -if [[ $performance_test_reps -gt 1 ]]; then - for ((i=1;i<=$performance_test_reps;i++)); do - # Directory to store performance test results for this repetition - performance_test_dir=$log_dir/performance_tests_$performance_test_var/repetition$i - - performance_tests $performance_test_value_array $performance_test_dir $i - done -else - # No unnecessary subdirectories if test is run only once - performance_test_dir=$log_dir/performance_tests_$performance_test_var +for ((i=1;i<=$performance_test_reps;i++)); do + # Directory to store performance test results for this repetition + performance_test_dir=$log_dir/performance_tests_$performance_test_var/repetition$i performance_tests $performance_test_value_array $performance_test_dir $i -fi +done clean_exit 0 diff --git a/test_suite/visualize_performance_tests.py b/test_suite/visualize_performance_tests.py index fe35a3a..95c6392 100644 --- a/test_suite/visualize_performance_tests.py +++ b/test_suite/visualize_performance_tests.py @@ -4,6 +4,7 @@ import re import sys import matplotlib.pyplot as plt +from itertools import groupby if len(sys.argv) != 2: print(f""" @@ -47,7 +48,14 @@ def test_iteration(): m = p.match(test_dir) test_var = m.group(1) - test_var_values, extracted_data = connection_iteration(test_path, test_var) + test_var_values, extracted_data = repetition_iteration(test_path, test_var) + extracted_data = aggregate_repetitions(extracted_data) + + with open(f"{parent_path}/performance_test_data.json", 'w') as file: + # Delete transform key from bitrate metric, since it is not JSON serializable + del extracted_data["bitrate"]["transform"] + + json.dump(extracted_data, file) for metric in extracted_data.keys(): create_graph(test_var, test_var_values, metric, extracted_data, parent_path) @@ -56,8 +64,8 @@ def test_iteration(): plural = "s" if n_tests > 1 else "" print(f"Generated graphs to visualize {n_tests} performance test{plural}") -# Recursively iterate over all connection subdirectories (eduP2P/WireGuard/Direct) -def connection_iteration(test_path : str, test_var : str) -> dict: +# Recursively iterate over all repetition subdirectories +def repetition_iteration(test_path: str, test_var: str) -> tuple[list[float], dict]: extracted_data = { "bitrate" : { "label" : "Measured bitrate", @@ -86,7 +94,24 @@ def connection_iteration(test_path : str, test_var : str) -> dict: }, } - paths = Path(test_path).glob("*") + paths = Path(test_path).rglob("repetition*") + + # Iterate over repetitions sorted from lowest to highest number (default sorting order is inconsistent) + for path in sorted(paths, key=lambda p: str(p)): + repetition_path = str(path) + repetition_id = repetition_path.split('/')[-1] + + # Initialize the dictionary of measurements for this repetition + for metric in extracted_data.keys(): + extracted_data[metric]["values"][repetition_id] = {} + + test_var_values, extracted_data = connection_iteration(repetition_path, repetition_id, test_var, extracted_data) + + return test_var_values, extracted_data + +# Recursively iterate over all connection subdirectories (eduP2P/WireGuard/Direct) +def connection_iteration(repetition_path: str, repetition_id: str, test_var: str, extracted_data: dict) -> tuple[list[float], dict]: + paths = Path(repetition_path).glob("*") for path in paths: connection_path = str(path) @@ -94,14 +119,14 @@ def connection_iteration(test_path : str, test_var : str) -> dict: # Initialize the lists of measurements for this connection type for metric in extracted_data.keys(): - extracted_data[metric]["values"][connection_type] = [] + extracted_data[metric]["values"][repetition_id][connection_type] = [] - test_var_values, extracted_data = file_iteration(connection_type, connection_path, test_var, extracted_data) + test_var_values, extracted_data = file_iteration(connection_type, connection_path, repetition_id, test_var, extracted_data) return test_var_values, extracted_data # Recursively iterate over all json files in the connection subdirectories (each file corresponds to one test variable value) -def file_iteration(connection_type : str, connection_path : str, test_var : str, extracted_data : dict) -> tuple[list[float], dict]: +def file_iteration(connection_type: str, connection_path: str, repetition_id: str, test_var: str, extracted_data: dict) -> tuple[list[float], dict]: test_var_values = [] paths = Path(connection_path).glob(f"{test_var}=*") @@ -118,18 +143,19 @@ def file_iteration(connection_type : str, connection_path : str, test_var : str, with open(path_str, 'r') as file: data = json.load(file) - extracted_data = extract_data(connection_type, data, extracted_data) + extracted_data = extract_data(connection_type, repetition_id, data, extracted_data) # Sort data sorted_indices=np.argsort(test_var_values) test_var_values = np.array(test_var_values)[sorted_indices] for metric in extracted_data.keys(): - extracted_data[metric]["values"][connection_type] = np.array(extracted_data[metric]["values"][connection_type])[sorted_indices] + sorted_measurements = np.array(extracted_data[metric]["values"][repetition_id][connection_type])[sorted_indices] + extracted_data[metric]["values"][repetition_id][connection_type] = list(sorted_measurements) return test_var_values, extracted_data -def extract_data(connection_type : str, data : dict, extracted_data : dict) -> dict: +def extract_data(connection_type: str, repetition_id: str, data: dict, extracted_data: dict) -> dict: data = data["end"]["sum"] for metric in extracted_data.keys(): @@ -143,13 +169,28 @@ def extract_data(connection_type : str, data : dict, extracted_data : dict) -> d except KeyError: pass - extracted_data[metric]["values"][connection_type].append(measurement) + extracted_data[metric]["values"][repetition_id][connection_type].append(measurement) + + return extracted_data + +def aggregate_repetitions(extracted_data: dict) -> dict: + for metric in extracted_data.keys(): + measurements = extracted_data[metric]["values"] + connection_dicts = [v for _, v in measurements.items()] + connection_measurement_pairs = [(k, v) for c in connection_dicts for k, v in list(c.items())] + aggregated_measurements = {} + + # Group the measurements by connection type to compute the average + for connection_type, groups in groupby(sorted(connection_measurement_pairs, reverse=True), key=lambda t: t[0]): + aggregated_measurements[connection_type] = list(np.array([group[1] for group in groups]).mean(axis=0)) + + measurements["average"] = aggregated_measurements return extracted_data -def create_graph(test_var : str, test_var_values : list[float], metric : str, extracted_data : dict, save_path : str): +def create_graph(test_var: str, test_var_values: list[float], metric: str, extracted_data: dict, save_path: str): metric_data = extracted_data[metric] - connection_measurements = metric_data["values"] + connection_measurements = metric_data["values"]["average"] test_var_label = TEST_VARS[test_var]["label"] test_var_unit = TEST_VARS[test_var]["unit"] @@ -165,12 +206,14 @@ def create_graph(test_var : str, test_var_values : list[float], metric : str, ex ls=line_styles[i] lw=line_widths[i] - # Plot the measured independent variable values on the X axis instead of the target values, unless the measured values or the delay are plotted on the Y axis (delay is not affected by the iperf3 measured values) - if metric == test_var or metric == "delay": + # Plot the measured independent variable values on the X axis instead of the target values unless: + # - The measured values are plotted on the Y axis + # - One-way delay/HTTP latency are plotted respectively on the X/Y axis, since the HTTP latency and iperf3 measured values are independendent + if metric == test_var or metric == "delay" or test_var == "delay": plt.plot(test_var_values, y, linestyle=ls, linewidth=lw, label=connection) x_label = test_var_label else: - measured_test_var_values = sorted(extracted_data[test_var]["values"][connection]) + measured_test_var_values = sorted(extracted_data[test_var]["values"]["average"][connection]) plt.plot(measured_test_var_values, y, linestyle=ls, linewidth=lw, label=connection) x_label = extracted_data[test_var]["label"] From e4f0349399c1d2a85e056ab0c21883e3a7c2f309 Mon Sep 17 00:00:00 2001 From: Henk Date: Fri, 21 Feb 2025 16:17:30 +0100 Subject: [PATCH 31/82] Run CI test suite on all henk/ branches (as well as main --- .github/workflows/CI_test_suite.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI_test_suite.yml b/.github/workflows/CI_test_suite.yml index d5e18af..f4014db 100644 --- a/.github/workflows/CI_test_suite.yml +++ b/.github/workflows/CI_test_suite.yml @@ -2,7 +2,9 @@ name: CI Test Suite on: push: - branches: [ "main" ] + branches: + - "main" + - "henk/*" pull_request: branches: [ "main" ] From e528e6ef786257a6f19b0d474646943683b88130 Mon Sep 17 00:00:00 2001 From: Henk Date: Fri, 21 Feb 2025 16:31:35 +0100 Subject: [PATCH 32/82] Add bitrate performance test in CI --- .github/workflows/CI_test_suite.yml | 41 ++++++++++++++++++++++++++++- test_suite/system_tests.sh | 3 +-- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI_test_suite.yml b/.github/workflows/CI_test_suite.yml index f4014db..d45d5ec 100644 --- a/.github/workflows/CI_test_suite.yml +++ b/.github/workflows/CI_test_suite.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.12' - name: System tests dependencies run: xargs -a system_test_requirements.txt sudo apt-get install @@ -49,6 +49,45 @@ jobs: if: ${{ steps.system-test.outcome == 'failure' }} run: exit 1 + PerformanceTests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: System tests dependencies + run: xargs -a system_test_requirements.txt sudo apt-get install + working-directory: test_suite + + - name: Performance tests dependencies + run: pip install -r python_requirements.txt + working-directory: test_suite + + - name: Run performance test with varying bitrate + id: system-test + run: ./system_tests.sh -p + working-directory: test_suite + continue-on-error: true + + - name: Upload performance test logs + uses: actions/upload-artifact@v4 + with: + name: performance-test-logs + path: test_suite/system_test_logs/ + + - name: Fail job if performance test failed (for clarity in GitHub UI) + if: ${{ steps.system-test.outcome == 'failure' }} + run: exit 1 + IntegrationTests: runs-on: ubuntu-latest steps: diff --git a/test_suite/system_tests.sh b/test_suite/system_tests.sh index 3900f84..2bf7632 100755 --- a/test_suite/system_tests.sh +++ b/test_suite/system_tests.sh @@ -262,8 +262,7 @@ fi if [[ $performance == true ]]; then echo -e "\nPerformance tests (without NAT)" - run_system_test -k bitrate -v 25,50,75,100 -d 3 -b TS_PASS_DIRECT router1-router2 : : - run_system_test -k packet_loss -v 0,1.5,3,4.5 -d 3 -b TS_PASS_DIRECT router1-router2 : : + run_system_test -k bitrate -v 50,100,150,200 -d 3 -r 3 -b TS_PASS_DIRECT router1-router2 : wg0:wg0 elif [[ -n $file ]]; then echo -e "\nTests from file: $file" From 9336d9f674f260143ff787bfb67b915f5bd630b0 Mon Sep 17 00:00:00 2001 From: Henk Date: Fri, 21 Feb 2025 16:56:18 +0100 Subject: [PATCH 33/82] Higher bitrate values in CI performance test --- test_suite/system_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_suite/system_tests.sh b/test_suite/system_tests.sh index 2bf7632..b902a59 100755 --- a/test_suite/system_tests.sh +++ b/test_suite/system_tests.sh @@ -262,7 +262,7 @@ fi if [[ $performance == true ]]; then echo -e "\nPerformance tests (without NAT)" - run_system_test -k bitrate -v 50,100,150,200 -d 3 -r 3 -b TS_PASS_DIRECT router1-router2 : wg0:wg0 + run_system_test -k bitrate -v 250,500,750,1000 -d 3 -r 3 -b TS_PASS_DIRECT router1-router2 : wg0:wg0 elif [[ -n $file ]]; then echo -e "\nTests from file: $file" From dcd6d1286a0a151cc2ba89058ee5f55e1a306444 Mon Sep 17 00:00:00 2001 From: Henk Date: Sun, 23 Feb 2025 11:44:21 +0100 Subject: [PATCH 34/82] Add options to performance test -b flag to choose whether to compare against direct and/or WireGuard connection, instead of always comparing against both --- test_suite/performance_test.sh | 31 ++++++++++++++++++++++++------- test_suite/system_test.sh | 13 ++++++++----- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/test_suite/performance_test.sh b/test_suite/performance_test.sh index 2ef309d..c9d145e 100755 --- a/test_suite/performance_test.sh +++ b/test_suite/performance_test.sh @@ -4,8 +4,8 @@ usage_str=""" Usage: ${0} [OPTIONAL ARGUMENTS] [OPTIONAL ARGUMENTS]: - -b - With this flag, eduP2P's performance is compared to the performance of a direct connection, and a connection using only WireGuard + -b + With this flag, eduP2P's performance is compared to the performance of a direct connection and/or a connection using only WireGuard This flag should only be used when both peers reside in the 'public' network Executes performance tests between the peers using iperf3, where peer 1 acts as the server and peer 2 as the client @@ -24,10 +24,24 @@ This script must be executed with root permissions . ./util.sh # Validate optional arguments -while getopts ":bh" opt; do +while getopts ":b:h" opt; do case $opt in b) - baseline=true + baseline=$OPTARG + validate_str $baseline "^direct|wireguard|both$" + + case $baseline in + "direct") + baseline_direct=true + ;; + "wireguard") + baseline_wireguard=true + ;; + "both") + baseline_direct=true + baseline_wireguard=true + ;; + esac ;; h) echo "$usage_str" @@ -60,7 +74,7 @@ function clean_exit() { exit_code=$1 # Delete baseline WireGuard interfaces and private keys, and kill keep-alive process - if [[ $baseline == true ]]; then + if [[ $baseline_wireguard == true ]]; then for ns in $peer1 $peer2; do sudo ip netns exec $ns ip link del wg_$ns rm private_$ns @@ -200,8 +214,11 @@ function performance_tests() { performance_test $performance_test_val $performance_test_dir "eduP2P" $peer1_ip # If -b is set, the performance test is repeated over a direct/WireGuard connection instead of over the eduP2P connection - if [[ $baseline == true ]]; then + if [[ $baseline_direct == true ]]; then performance_test $performance_test_val $performance_test_dir "Direct" $peer1_pub_ip + fi + + if [[ $baseline_wireguard == true ]]; then performance_test $performance_test_val $performance_test_dir "WireGuard" 10.0.0.1 fi @@ -245,7 +262,7 @@ function wg_setup() { } # For the baseline comparison, we need the peers' public IPs, which are also needed to setup a WireGuard connection between them -if [[ $baseline == true ]]; then +if [[ $baseline_direct == true || $baseline_wireguard == true ]]; then peer1_pub_ip=$(ip netns exec $peer1 ip address | grep -Eo "inet 192.168.[0-9.]+" | cut -d ' ' -f2) peer2_pub_ip=$(ip netns exec $peer2 ip address | grep -Eo "inet 192.168.[0-9.]+" | cut -d ' ' -f2) diff --git a/test_suite/system_test.sh b/test_suite/system_test.sh index c4d8137..da2636d 100755 --- a/test_suite/system_test.sh +++ b/test_suite/system_test.sh @@ -13,8 +13,8 @@ Usage: ${0} [OPTIONAL ARGUMENTS] [NAT CO -v -d -r - -b - With this flag, eduP2P's performance is compared to the performance of a direct connection, and a connection using only WireGuard + -b + With this flag, eduP2P's performance is compared to the performance of a direct connection and/or a connection using only WireGuard This flag should only be used when both peers reside in the 'public' network specifies the peer and router namespaces to be used in this system test. It should be a string with one of the following formats: @@ -43,7 +43,7 @@ performance_test_duration=5 # Default value in case -d is not used performance_test_reps=1 # Default value in case -r is not used # Validate optional arguments -while getopts ":k:v:d:r:bh" opt; do +while getopts ":k:v:d:r:b:h" opt; do case $opt in k) performance_test_var=$OPTARG @@ -73,7 +73,10 @@ while getopts ":k:v:d:r:bh" opt; do ;; b) - baseline="-b" + performance_test_baseline=$OPTARG + validate_str $performance_test_baseline "^direct|wireguard|both$" + + baseline="-b $performance_test_baseline" ;; h) echo "$usage_str" @@ -311,7 +314,7 @@ if [[ -n $performance_test_var ]]; then peer1_ns=${peer_ns_list[0]} peer1_ip=$(extract_ipv4 $peer1_ns $peer1_interface) - sudo ./performance_test.sh $baseline $peer1_ns ${peer_ns_list[1]} $peer1_ip $performance_test_var $performance_test_values $performance_test_duration $performance_test_reps $log_dir + sudo ./performance_test.sh ${baseline} $peer1_ns ${peer_ns_list[1]} $peer1_ip $performance_test_var $performance_test_values $performance_test_duration $performance_test_reps $log_dir if [[ $? -ne 0 ]]; then clean_exit 1 From 925ab6a3b58cdba2decda8c0492e027f8a6eab67 Mon Sep 17 00:00:00 2001 From: Henk Date: Fri, 28 Feb 2025 09:40:29 +0100 Subject: [PATCH 35/82] Simplify X-axis of performance test graphs: always plot independent test variables --- test_suite/visualize_performance_tests.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/test_suite/visualize_performance_tests.py b/test_suite/visualize_performance_tests.py index 95c6392..aacd428 100644 --- a/test_suite/visualize_performance_tests.py +++ b/test_suite/visualize_performance_tests.py @@ -202,24 +202,15 @@ def create_graph(test_var: str, test_var_values: list[float], metric: str, extra line_widths=[4,3,2] for i, connection in enumerate(connection_measurements.keys()): + x = test_var_values y = connection_measurements[connection] ls=line_styles[i] lw=line_widths[i] + plt.plot(x, y, linestyle=ls, linewidth=lw, label=connection) - # Plot the measured independent variable values on the X axis instead of the target values unless: - # - The measured values are plotted on the Y axis - # - One-way delay/HTTP latency are plotted respectively on the X/Y axis, since the HTTP latency and iperf3 measured values are independendent - if metric == test_var or metric == "delay" or test_var == "delay": - plt.plot(test_var_values, y, linestyle=ls, linewidth=lw, label=connection) - x_label = test_var_label - else: - measured_test_var_values = sorted(extracted_data[test_var]["values"]["average"][connection]) - plt.plot(measured_test_var_values, y, linestyle=ls, linewidth=lw, label=connection) - x_label = extracted_data[test_var]["label"] - - plt.xlabel(f"{x_label} ({test_var_unit})") + plt.xlabel(f"{test_var_label} ({test_var_unit})") plt.ylabel(f"{metric_label} ({metric_unit})") - plt.title(f"{metric_label} for varying {x_label}") + plt.title(f"{metric_label} for varying {test_var_label}") plt.ticklabel_format(useOffset=False) plt.legend() From 8cb029a87db8c9ce97e351648d45139702bb5041 Mon Sep 17 00:00:00 2001 From: Henk Date: Fri, 28 Feb 2025 17:33:05 +0100 Subject: [PATCH 36/82] Update -b flag in performance test command, and allow files passed to -f flag without empty line at end of file --- test_suite/system_tests.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test_suite/system_tests.sh b/test_suite/system_tests.sh index b902a59..9467d09 100755 --- a/test_suite/system_tests.sh +++ b/test_suite/system_tests.sh @@ -88,6 +88,8 @@ while getopts ":c:d:ef:l:ph" opt; do esac done +echo FILE: $file + # Shift positional parameters indexing by accounting for the optional arguments shift $((OPTIND-1)) @@ -262,11 +264,12 @@ fi if [[ $performance == true ]]; then echo -e "\nPerformance tests (without NAT)" - run_system_test -k bitrate -v 250,500,750,1000 -d 3 -r 3 -b TS_PASS_DIRECT router1-router2 : wg0:wg0 + run_system_test -k bitrate -v 250,500,750,1000 -d 3 -r 3 -b both TS_PASS_DIRECT router1-router2 : wg0:wg0 elif [[ -n $file ]]; then echo -e "\nTests from file: $file" - while read test_cmd; do + # Read line by line from $file (also last line which may not end with a newline, but still contain a command) + while IFS= read -r test_cmd || [ -n "$test_cmd" ]; do eval $test_cmd done < $file else From 8187106b63f1e98f4b9e9183d368b605aeb278f2 Mon Sep 17 00:00:00 2001 From: Henk Date: Fri, 28 Feb 2025 17:33:42 +0100 Subject: [PATCH 37/82] Skip delay measurement for independent variable bitrate (no correlation) --- test_suite/performance_test.sh | 6 ++++-- test_suite/visualize_performance_tests.py | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/test_suite/performance_test.sh b/test_suite/performance_test.sh index c9d145e..12096f2 100755 --- a/test_suite/performance_test.sh +++ b/test_suite/performance_test.sh @@ -186,8 +186,10 @@ function performance_test() { wait $server_pid # Measure delay and store it in the iperf3 log file - delay=$(measure_delay $server_ip) - store_delay $delay $log_path + if [[ $performance_test_var != "bitrate" ]]; then + delay=$(measure_delay $server_ip) + store_delay $delay $log_path + fi } # Function to do performance tests for all performance test values diff --git a/test_suite/visualize_performance_tests.py b/test_suite/visualize_performance_tests.py index aacd428..a50cf44 100644 --- a/test_suite/visualize_performance_tests.py +++ b/test_suite/visualize_performance_tests.py @@ -94,6 +94,10 @@ def repetition_iteration(test_path: str, test_var: str) -> tuple[list[float], di }, } + # Delay is not affected by the iperf3 target bitrate, so this data has not been measured + if(test_var == "bitrate"): + del extracted_data["delay"] + paths = Path(test_path).rglob("repetition*") # Iterate over repetitions sorted from lowest to highest number (default sorting order is inconsistent) From fb01c207fc16e1e1cb969848c53da35de76e98d4 Mon Sep 17 00:00:00 2001 From: Henk Date: Sat, 1 Mar 2025 10:15:43 +0100 Subject: [PATCH 38/82] Add -L flag to specify the name of the test log directory --- test_suite/system_tests.sh | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/test_suite/system_tests.sh b/test_suite/system_tests.sh index 9467d09..0f5ace1 100755 --- a/test_suite/system_tests.sh +++ b/test_suite/system_tests.sh @@ -24,8 +24,10 @@ The following options can be used to configure additional parameters during the The delay should be provided as an integer that represents the one-way delay in milliseconds -l Specifies the log level used in the eduP2P client of the two peers - The log level 'info' should not be used if a system test is run where one of the peers uses userspace WireGuard (the other peer's IP address is not logged in this case)""" - + The log level 'info' should not be used if a system test is run where one of the peers uses userspace WireGuard (the other peer's IP address is not logged in this case) + -L + Specifies the alphanumeric name of the directory inside system_test_logs/ where the test logs will be stored + If this argument is not provided, the directory name is the current timestamp""" # Use functions and constants from util.sh . ./util.sh @@ -33,7 +35,7 @@ The following options can be used to configure additional parameters during the log_lvl="debug" # Validate optional arguments -while getopts ":c:d:ef:l:ph" opt; do +while getopts ":c:d:ef:l:L:ph" opt; do case $opt in c) connectivity=true @@ -75,6 +77,18 @@ while getopts ":c:d:ef:l:ph" opt; do log_lvl_regex="^info|debug|trace?$" validate_str $log_lvl $log_lvl_regex ;; + L) + alphanum_regex="^[a-zA-Z0-9]+$" + validate_str $OPTARG $alphanum_regex + log_dir_rel=system_test_logs/$OPTARG + + # Ensure log dir does not exist yet + ls $log_dir_rel &> /dev/null + + if [[ $? -eq 0 ]]; then + exit_with_error "$log_dir_rel already exists" + fi + ;; p) performance=true ;; @@ -88,8 +102,6 @@ while getopts ":c:d:ef:l:ph" opt; do esac done -echo FILE: $file - # Shift positional parameters indexing by accounting for the optional arguments shift $((OPTIND-1)) @@ -118,8 +130,11 @@ function build_go() { build_go function create_log_dir() { - timestamp=$(date +"%Y-%m-%dT%H_%M_%S") - log_dir_rel=system_test_logs/${timestamp} # Relative path for pretty printing + if [[ -z $log_dir_rel ]]; then + timestamp=$(date +"%Y-%m-%dT%H_%M_%S") + log_dir_rel=system_test_logs/${timestamp} # Relative path for pretty printing + fi + log_dir=${repo_dir}/test_suite/${log_dir_rel} # Absolute path for use in scripts running from different directories mkdir -p ${log_dir} echo "Logging to ${log_dir_rel}" From 5ea23a17d086b070aea20c2e099ba4a4c2072372 Mon Sep 17 00:00:00 2001 From: Henk Date: Sat, 1 Mar 2025 10:30:23 +0100 Subject: [PATCH 39/82] Use new -L flag to upload individual performance test graphs as artifacts in GitHub workflow --- .github/workflows/CI_test_suite.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI_test_suite.yml b/.github/workflows/CI_test_suite.yml index d45d5ec..3a1865f 100644 --- a/.github/workflows/CI_test_suite.yml +++ b/.github/workflows/CI_test_suite.yml @@ -74,11 +74,17 @@ jobs: - name: Run performance test with varying bitrate id: system-test - run: ./system_tests.sh -p + run: ./system_tests.sh -p -L performance working-directory: test_suite continue-on-error: true - - name: Upload performance test logs + - name: Upload performance test packet loss graph + uses: actions/upload-artifact@v4 + with: + name: graph_X_bitrate_Y_packet_loss + path: test_suite/system_test_logs/performance/1_No-NAT_No-NAT/performance_test_packet_loss.png + + - name: Upload full performance test logs uses: actions/upload-artifact@v4 with: name: performance-test-logs From 9e31b0fbf6b5d7db49b40b6b6bc8e6c4928b51b3 Mon Sep 17 00:00:00 2001 From: Henk Date: Sat, 1 Mar 2025 10:37:01 +0100 Subject: [PATCH 40/82] Adjust workflow to upload all performance test graphs with a wildcard pattern --- .github/workflows/CI_test_suite.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI_test_suite.yml b/.github/workflows/CI_test_suite.yml index 3a1865f..4106d62 100644 --- a/.github/workflows/CI_test_suite.yml +++ b/.github/workflows/CI_test_suite.yml @@ -78,11 +78,11 @@ jobs: working-directory: test_suite continue-on-error: true - - name: Upload performance test packet loss graph + - name: Upload performance test graphs uses: actions/upload-artifact@v4 with: - name: graph_X_bitrate_Y_packet_loss - path: test_suite/system_test_logs/performance/1_No-NAT_No-NAT/performance_test_packet_loss.png + name: performance-test-graphs + path: test_suite/system_test_logs/performance/1_No-NAT_No-NAT/*.png - name: Upload full performance test logs uses: actions/upload-artifact@v4 From 40880c43cf9898e1f59bbb19652ba41ee80e909b Mon Sep 17 00:00:00 2001 From: Henk Date: Sat, 1 Mar 2025 10:50:00 +0100 Subject: [PATCH 41/82] Increase system test timeout duration to prevent failures in GitHub runners --- test_suite/system_test.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test_suite/system_test.sh b/test_suite/system_test.sh index da2636d..d84eead 100755 --- a/test_suite/system_test.sh +++ b/test_suite/system_test.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +# Amount of seconds to wait for one system test to finish +SYSTEM_TEST_TIMEOUT=60 + usage_str=""" Usage: ${0} [OPTIONAL ARGUMENTS] [NAT CONFIGURATION 1]:[NAT CONFIGURATION 2] [WIREGUARD INTERFACE 1]:[WIREGUARD INTERFACE 2] @@ -271,7 +274,7 @@ done for i in {0..1}; do peer_id="peer$((i+1))" export LOG_FILE=${log_dir}/$peer_id.txt # Export to use in bash -c - timeout 30s bash -c 'tail -n +1 -f $LOG_FILE | sed -n "/TS_PASS/q2; /TS_FAIL/q3"' # bash -c is necessary to use timeout with | and still get the right exit codes + timeout ${SYSTEM_TEST_TIMEOUT}s bash -c 'tail -n +1 -f $LOG_FILE | sed -n "/TS_PASS/q2; /TS_FAIL/q3"' # bash -c is necessary to use timeout with | and still get the right exit codes # Branch on exit code of previous command case $? in From d538fdaa626fcd9102cb8b601d93269fe4323f5c Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 3 Mar 2025 13:28:18 +0100 Subject: [PATCH 42/82] Implement client-side device key Closes #117 --- toversok/engine.go | 35 ++++++++++++++++++++++------------- toversok/observer.go | 2 +- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/toversok/engine.go b/toversok/engine.go index 1f68bb4..33ff634 100644 --- a/toversok/engine.go +++ b/toversok/engine.go @@ -36,6 +36,8 @@ type Engine struct { state stateObserver doAutoRestart bool dirty bool + + deviceKey *string } // Start will fire up the Engine. @@ -160,11 +162,10 @@ func (e *Engine) installSession(allowLogon bool) error { var logon types.LogonCallback if allowLogon { - logon = func(url string, _ chan<- string) error { - // TODO register/use device key channel - + logon = func(url string, devKeyCh chan<- string) error { e.state.alter(func(o *stateObserver) { - o.currentLoginURL = url + o.loginURL = url + o.loginDeviceKeyCh = devKeyCh }) e.state.change(CreatingSession, NeedsLogin) @@ -215,8 +216,9 @@ type stateObserver struct { state EngineState callbacks []func(state EngineState) - currentLoginURL string - expiry time.Time + loginURL string + loginDeviceKeyCh chan<- string + expiry time.Time } func (s *stateObserver) CurrentState() EngineState { @@ -232,15 +234,15 @@ func (s *stateObserver) RegisterStateChangeListener(f func(state EngineState)) { var ErrWrongState = errors.New("wrong state") -func (s *stateObserver) GetNeedsLoginState() (url string, err error) { +func (s *stateObserver) GetNeedsLoginState() (url string, devKeyCh chan<- string, err error) { s.mu.Lock() defer s.mu.Unlock() if s.state != NeedsLogin { - return "", ErrWrongState + return "", nil, ErrWrongState } - return s.currentLoginURL, nil + return s.loginURL, s.loginDeviceKeyCh, nil } func (s *stateObserver) GetEstablishedState() (time.Time, error) { @@ -345,12 +347,16 @@ func NewEngine( e.Observer().RegisterStateChangeListener(func(state EngineState) { if state == NeedsLogin { - url, err := e.Observer().GetNeedsLoginState() + url, devKeyCh, err := e.Observer().GetNeedsLoginState() if err == nil { e.slog().Info("control wants logon", "url", url) } else { e.slog().Error("could not get login state when prompted for it", "err", err) } + + if e.deviceKey != nil { + devKeyCh <- *e.deviceKey + } } else if state == Established { expiry, err := e.Observer().GetEstablishedState() if err != nil { @@ -403,7 +409,10 @@ func (e *Engine) Observer() Observer { return &e.state } -func (e *Engine) SupplyDeviceKey(string) error { - // TODO - panic("not implemented") +// SupplyDeviceKey gives the device key that'll be used when logging on. +// This must be called BEFORE Start. +func (e *Engine) SupplyDeviceKey(key string) error { + e.deviceKey = &key + + return nil } diff --git a/toversok/observer.go b/toversok/observer.go index 4b4233b..51529b1 100644 --- a/toversok/observer.go +++ b/toversok/observer.go @@ -8,7 +8,7 @@ type Observer interface { CurrentState() EngineState - GetNeedsLoginState() (url string, err error) + GetNeedsLoginState() (url string, deviceKeyCh chan<- string, err error) GetEstablishedState() (expiry time.Time, err error) // TODO add ipv4,ipv6? } From 24e7372491d5635b898b6a726d9d7174897bacdd Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 3 Mar 2025 13:28:52 +0100 Subject: [PATCH 43/82] control: Misc changes, remove TODO, add LogonDeviceKey anti-glare --- types/control/client.go | 1 - types/control/server_session.go | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/types/control/client.go b/types/control/client.go index 32f0831..a174c06 100644 --- a/types/control/client.go +++ b/types/control/client.go @@ -168,7 +168,6 @@ func (c *Client) handleLogon(url string, logon types.LogonCallback) (msgcontrol. } }() - // TODO also add context error / close here select { case deviceKey := <-deviceKeyChan: if err := c.cc.Write(&msgcontrol.LogonDeviceKey{ diff --git a/types/control/server_session.go b/types/control/server_session.go index 66b686c..9d51f66 100644 --- a/types/control/server_session.go +++ b/types/control/server_session.go @@ -448,7 +448,10 @@ func (s *ServerSession) Run() { session.UpdateHomeRelay(s.Peer, msg.HomeRelay) }) case *msgcontrol.Pong: + s.Slog().Debug("received pong") // TODO + case *msgcontrol.LogonDeviceKey: + s.Slog().Debug("received after-logon logon device key, ignoring...") default: err = fmt.Errorf("received unknown type of message: %#v", msg) return From b9cb25fb991eb947b811b9c40d581adb8c20dcb1 Mon Sep 17 00:00:00 2001 From: Henk Date: Mon, 3 Mar 2025 14:50:22 +0100 Subject: [PATCH 44/82] Add more low-range test variable values to bitrate performance test --- test_suite/system_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_suite/system_tests.sh b/test_suite/system_tests.sh index 0f5ace1..6ea35c5 100755 --- a/test_suite/system_tests.sh +++ b/test_suite/system_tests.sh @@ -279,7 +279,7 @@ fi if [[ $performance == true ]]; then echo -e "\nPerformance tests (without NAT)" - run_system_test -k bitrate -v 250,500,750,1000 -d 3 -r 3 -b both TS_PASS_DIRECT router1-router2 : wg0:wg0 + run_system_test -k bitrate -v 50,100,150,250,500,750,1000 -d 3 -b both TS_PASS_DIRECT router1-router2 : wg0:wg0 elif [[ -n $file ]]; then echo -e "\nTests from file: $file" From ce435835a5ac0e98467a0feedf34fd23a41e6db6 Mon Sep 17 00:00:00 2001 From: Henk Date: Mon, 3 Mar 2025 14:58:04 +0100 Subject: [PATCH 45/82] Refine bitrate performance test variable values to improve graph clarity --- test_suite/system_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_suite/system_tests.sh b/test_suite/system_tests.sh index 6ea35c5..8088da4 100755 --- a/test_suite/system_tests.sh +++ b/test_suite/system_tests.sh @@ -279,7 +279,7 @@ fi if [[ $performance == true ]]; then echo -e "\nPerformance tests (without NAT)" - run_system_test -k bitrate -v 50,100,150,250,500,750,1000 -d 3 -b both TS_PASS_DIRECT router1-router2 : wg0:wg0 + run_system_test -k bitrate -v 100,200,300,400,500 -d 3 -b both TS_PASS_DIRECT router1-router2 : wg0:wg0 elif [[ -n $file ]]; then echo -e "\nTests from file: $file" From 34108d5a6633e0da2716d1d0abcb9d9a40459105 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Tue, 4 Mar 2025 09:36:14 +0100 Subject: [PATCH 46/82] Add `go test` to CI Closes #106 --- .github/workflows/go-test.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/go-test.yml diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml new file mode 100644 index 0000000..24bf857 --- /dev/null +++ b/.github/workflows/go-test.yml @@ -0,0 +1,19 @@ +name: go test +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Install dependencies + run: go get . + - name: Build + run: go build -v ./... + - name: Test with the Go CLI + run: go test \ No newline at end of file From 44bff508150ddbbc39de8a2a65159961b321f2d9 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Tue, 4 Mar 2025 09:38:05 +0100 Subject: [PATCH 47/82] fix 'go get' for go test cli --- .github/workflows/go-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index 24bf857..4e47e8a 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -12,7 +12,7 @@ jobs: with: go-version-file: go.mod - name: Install dependencies - run: go get . + run: go get ./... - name: Build run: go build -v ./... - name: Test with the Go CLI From 35294a8e931ee27e1f62bb5eaf55d1ba59096efc Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Tue, 4 Mar 2025 09:40:18 +0100 Subject: [PATCH 48/82] fix 'go test' for go test ci --- .github/workflows/go-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml index 4e47e8a..cd3f070 100644 --- a/.github/workflows/go-test.yml +++ b/.github/workflows/go-test.yml @@ -16,4 +16,4 @@ jobs: - name: Build run: go build -v ./... - name: Test with the Go CLI - run: go test \ No newline at end of file + run: go test -v ./... \ No newline at end of file From e403e46fd9a78f0e8dd655ff20bb17a06e3a5edb Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 6 Mar 2025 11:01:11 +0100 Subject: [PATCH 49/82] add sidebanddata.go, clean up msgsess --- toversok/actors/a_sman.go | 2 +- types/msgsess/msgsess.go | 4 ++- types/msgsess/parsing.go | 63 ++++++----------------------------- types/msgsess/ping.go | 17 +++++++++- types/msgsess/pong.go | 15 ++++++++- types/msgsess/rendezvous.go | 25 +++++++++++++- types/msgsess/sidebanddata.go | 41 +++++++++++++++++++++++ 7 files changed, 110 insertions(+), 57 deletions(-) create mode 100644 types/msgsess/sidebanddata.go diff --git a/toversok/actors/a_sman.go b/toversok/actors/a_sman.go index 3dd8f60..927b357 100644 --- a/toversok/actors/a_sman.go +++ b/toversok/actors/a_sman.go @@ -133,7 +133,7 @@ func (sm *SessionManager) Unpack(frameWithMagic []byte) (*msgsess.ClearMessage, } func (sm *SessionManager) Pack(sMsg msgsess.SessionMessage, toSession key.SessionPublic) []byte { - clearBytes := sMsg.MarshalSessionMessage() + clearBytes := sMsg.Marshal() cipherBytes := sm.session().Shared(toSession).Seal(clearBytes) diff --git a/types/msgsess/msgsess.go b/types/msgsess/msgsess.go index 60c07c4..61067f0 100644 --- a/types/msgsess/msgsess.go +++ b/types/msgsess/msgsess.go @@ -6,10 +6,12 @@ package msgsess import "github.com/edup2p/common/types/key" type SessionMessage interface { - MarshalSessionMessage() []byte + Marshal() []byte // todo maybe convert to slog.Group? Debug() string + + Parse([]byte) error } // ClearMessage represents a full session wire message in decrypted view diff --git a/types/msgsess/parsing.go b/types/msgsess/parsing.go index 66bf518..dc283dd 100644 --- a/types/msgsess/parsing.go +++ b/types/msgsess/parsing.go @@ -3,9 +3,6 @@ package msgsess import ( "errors" "fmt" - "net/netip" - - "github.com/edup2p/common/types" "github.com/edup2p/common/types/key" ) @@ -36,64 +33,26 @@ func ParseSessionMessage(usrMsg []byte) (SessionMessage, error) { return nil, fmt.Errorf("invalid version: %x", version) } + var msg SessionMessage + switch MessageType(msgType) { case PingMessage: - return parsePing(specificMsg) + msg = new(Ping) case PongMessage: - return parsePong(specificMsg) + msg = new(Pong) case RendezvousMessage: - return parseRendezvous(specificMsg) + msg = new(Rendezvous) + case SideBandDataMessage: + msg = new(SideBandData) default: return nil, fmt.Errorf("invalid message type: %x", msgType) } -} - -var errTooSmall = errors.New("session message too small") - -func parsePing(b []byte) (*Ping, error) { - if len(b) < key.Len+12 { - return nil, errTooSmall - } - - txid := [12]byte(b[:12]) - b = b[12:] - nKey := key.NodePublic(b[:key.Len]) - - return &Ping{ - TxID: txid, - NodeKey: nKey, - }, nil -} -func parsePong(b []byte) (*Pong, error) { - if len(b) < 12+16+2 { - return nil, errTooSmall + if err := msg.Parse(specificMsg); err != nil { + return nil, fmt.Errorf("failed to parse message of type %d: %w", msgType, err) } - txid := [12]byte(b[:12]) - b = b[12:] - - ap := types.ParseAddrPort([18]byte(b[:18])) - - return &Pong{TxID: txid, Src: ap}, nil + return msg, nil } -func parseRendezvous(b []byte) (*Rendezvous, error) { - if len(b)%18 != 0 { - return nil, errors.New("malformed rendezvous addresses") - } - - aps := make([]netip.AddrPort, 0) - - for { - ap := types.ParseAddrPort([18]byte(b[:18])) - aps = append(aps, ap) - b = b[18:] - - if len(b) == 0 { - break - } - } - - return &Rendezvous{MyAddresses: aps}, nil -} +var errTooSmall = errors.New("session message too small") diff --git a/types/msgsess/ping.go b/types/msgsess/ping.go index 33e9b8b..6ecda0a 100644 --- a/types/msgsess/ping.go +++ b/types/msgsess/ping.go @@ -28,10 +28,25 @@ type Ping struct { Padding int } -func (p *Ping) MarshalSessionMessage() []byte { +func (p *Ping) Marshal() []byte { + // TODO add padding return slices.Concat([]byte{byte(v1), byte(PingMessage)}, p.TxID[:], p.NodeKey[:]) } +func (p *Ping) Parse(b []byte) error { + if len(b) < key.Len+12 { + return errTooSmall + } + + p.TxID = [12]byte(b[:12]) + b = b[12:] + p.NodeKey = key.NodePublic(b[:key.Len]) + + // TODO count remaining bytes as padding + + return nil +} + func (p *Ping) Debug() string { return fmt.Sprintf("ping tx=%x nodekey=%s padding=%v", p.TxID, p.NodeKey.Debug(), p.Padding) } diff --git a/types/msgsess/pong.go b/types/msgsess/pong.go index fa027f4..210d90b 100644 --- a/types/msgsess/pong.go +++ b/types/msgsess/pong.go @@ -14,10 +14,23 @@ type Pong struct { Src netip.AddrPort // 18 bytes (16+2) on the wire; v4-mapped ipv6 for IPv4 } -func (p *Pong) MarshalSessionMessage() []byte { +func (p *Pong) Marshal() []byte { return slices.Concat([]byte{byte(v1), byte(PongMessage)}, p.TxID[:], types.PutAddrPort(p.Src)) } +func (p *Pong) Parse(b []byte) error { + if len(b) < 12+16+2 { + return errTooSmall + } + + p.TxID = [12]byte(b[:12]) + b = b[12:] + + p.Src = types.ParseAddrPort([18]byte(b[:18])) + + return nil +} + func (p *Pong) Debug() string { return fmt.Sprintf("pong tx=%x src=%s", p.TxID, p.Src.String()) } diff --git a/types/msgsess/rendezvous.go b/types/msgsess/rendezvous.go index e5640f7..b7f63cc 100644 --- a/types/msgsess/rendezvous.go +++ b/types/msgsess/rendezvous.go @@ -1,6 +1,7 @@ package msgsess import ( + "errors" "fmt" "net/netip" "slices" @@ -12,7 +13,7 @@ type Rendezvous struct { MyAddresses []netip.AddrPort } -func (r *Rendezvous) MarshalSessionMessage() []byte { +func (r *Rendezvous) Marshal() []byte { b := make([]byte, 0) for _, ap := range r.MyAddresses { @@ -22,6 +23,28 @@ func (r *Rendezvous) MarshalSessionMessage() []byte { return slices.Concat([]byte{byte(v1), byte(RendezvousMessage)}, b) } +func (r *Rendezvous) Parse(b []byte) error { + if len(b)%18 != 0 { + return errors.New("malformed rendezvous addresses") + } + + aps := make([]netip.AddrPort, 0) + + for { + ap := types.ParseAddrPort([18]byte(b[:18])) + aps = append(aps, ap) + b = b[18:] + + if len(b) == 0 { + break + } + } + + r.MyAddresses = aps + + return nil +} + func (r *Rendezvous) Debug() string { return fmt.Sprintf("rendezvous addresses=%s", types.PrettyAddrPortSlice(r.MyAddresses)) } diff --git a/types/msgsess/sidebanddata.go b/types/msgsess/sidebanddata.go new file mode 100644 index 0000000..e3666da --- /dev/null +++ b/types/msgsess/sidebanddata.go @@ -0,0 +1,41 @@ +package msgsess + +import ( + "fmt" + "slices" +) + +type SideBandDataType byte + +const ( + MDNSType SideBandDataType = iota +) + +type SideBandData struct { + Type SideBandDataType + Data []byte +} + +func (s *SideBandData) Marshal() []byte { + b := make([]byte, 0) + + b = append(b, byte(s.Type)) + b = append(b, s.Data...) + + return slices.Concat([]byte{byte(v1), byte(SideBandDataMessage)}, b) +} + +func (s *SideBandData) Parse(b []byte) error { + if len(b) < 1 { + return errTooSmall + } + + s.Type = SideBandDataType(b[0]) + s.Data = b[1:] + + return nil +} + +func (s *SideBandData) Debug() string { + return fmt.Sprintf("sidebanddata type=%d data=%x", s.Type, s.Data) +} From 7e53f69e6fc6d72ce98f572d89fd56600d91b170 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 6 Mar 2025 11:02:27 +0100 Subject: [PATCH 50/82] add first-iteration mDNS manager, does not work (see #124) closes #118 closes #119 closes #120 --- cmd/control_server/main.go | 8 +- cmd/mdns_monitor/main.go | 150 +++++++++++++++++ go.mod | 7 +- go.sum | 22 ++- toversok/actors/a_mman.go | 183 +++++++++++++++++++++ toversok/actors/a_sman_test.go | 14 +- toversok/actors/a_sockrecv.go | 2 +- toversok/actors/a_tman.go | 94 ++++++++++- toversok/actors/consts.go | 1 + toversok/actors/peerstate/pingtracker.go | 8 + toversok/actors/peerstate/s_established.go | 14 +- toversok/actors/stage.go | 29 +++- toversok/engine.go | 2 +- toversok/session.go | 17 +- types/ifaces/actor.go | 7 + types/ifaces/injectable.go | 9 + types/msgactor/msg.go | 13 ++ types/msgactor/msg_iface.go | 3 + types/msgsess/consts.go | 5 +- types/stage/stage.go | 2 + usrwg/wgusp.go | 52 +++++- 21 files changed, 611 insertions(+), 31 deletions(-) create mode 100644 cmd/mdns_monitor/main.go create mode 100644 toversok/actors/a_mman.go create mode 100644 types/ifaces/injectable.go diff --git a/cmd/control_server/main.go b/cmd/control_server/main.go index 91cbfde..8bbeab3 100644 --- a/cmd/control_server/main.go +++ b/cmd/control_server/main.go @@ -249,7 +249,9 @@ func (cs *ControlServer) loadExistingNodes() { continue } - if err := cs.server.UpsertVisibilityPair(client, client2, control.VisibilityPair{}); err != nil { + if err := cs.server.UpsertVisibilityPair(client, client2, control.VisibilityPair{ + MDNS: true, + }); err != nil { panic(err) } } @@ -262,7 +264,9 @@ func (cs *ControlServer) addNewNode(node key.NodePublic) { continue } - if err := cs.server.UpsertVisibilityPair(control.ClientID(node), control.ClientID(node2), control.VisibilityPair{}); err != nil { + if err := cs.server.UpsertVisibilityPair(control.ClientID(node), control.ClientID(node2), control.VisibilityPair{ + MDNS: true, + }); err != nil { panic(err) } } diff --git a/cmd/mdns_monitor/main.go b/cmd/mdns_monitor/main.go new file mode 100644 index 0000000..f45aa65 --- /dev/null +++ b/cmd/mdns_monitor/main.go @@ -0,0 +1,150 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "fmt" + "log" + "net" + "net/netip" + "time" + + "github.com/sethvargo/go-limiter/memorystore" + "golang.org/x/net/dns/dnsmessage" +) + +//func Control(network, address string, c syscall.RawConn) (err error) { +// controlErr := c.Control(func(fd uintptr) { +// unix.SetsockoptInet4Addr(int(fd), unix.IPPROTO_IP, unix.IP_ADD_MEMBERSHIP) +// +// err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) +// if err != nil { +// return +// } +// err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) +// }) +// if controlErr != nil { +// err = controlErr +// } +// return +//} + +func walkInterfaces() { + ift, err := net.Interfaces() + if err != nil { + log.Fatal(err) + } + for _, ifi := range ift { + isLoopBack := ifi.Flags&net.FlagLoopback != 0 + isPtP := ifi.Flags&net.FlagPointToPoint != 0 + + fmt.Printf("iface %s: lo(%t) ptp(%t)\n", ifi.Name, isLoopBack, isPtP) + } +} + +// loopbackInterface returns an available logical network interface +// for loopback tests. It returns nil if no suitable interface is +// found. +func loopbackInterface() *net.Interface { + ift, err := net.Interfaces() + if err != nil { + return nil + } + for _, ifi := range ift { + if ifi.Flags&net.FlagLoopback != 0 && ifi.Flags&net.FlagUp != 0 { + return &ifi + } + } + return nil +} + +func main() { + // this code is specific to macos, for now + + walkInterfaces() + + IP := "224.0.0.251:5353" + //IP := "[ff02::fb]:5353" + + ua := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(IP)) + + iface, err := net.InterfaceByName("lo0") + + bind, err := net.ListenMulticastUDP("udp4", iface, ua) + + if err != nil { + log.Fatal(err) + } + + fmt.Println("got multicast udp") + + store, err := memorystore.New(&memorystore.Config{ + // Number of tokens allowed per interval. + Tokens: 1, + + // Interval until tokens reset. + Interval: 20 * time.Second, + + SweepInterval: 1 * time.Minute, + SweepMinTTL: 1 * time.Minute, + }) + if err != nil { + log.Fatal(err) + } + + buf := make([]byte, 1<<16) + + QUBit := uint16(1 << 15) + + for { + n, ap, err := bind.ReadFromUDPAddrPort(buf) + + if err != nil { + log.Fatal(err) + } + + fmt.Printf("read %d bytes from %s\n", n, ap.String()) + + data := buf[:n] + + msg := dnsmessage.Message{} + if err = msg.Unpack(data); err != nil { + log.Printf("Error unpacking DNS message: %s\n", err) + continue + } + + _, _, _, ok, err := store.Take(context.Background(), msg.GoString()) + + if err != nil { + log.Fatal(err) + } + + if !ok { + log.Println("message rate limited") + continue + } + + questions := msg.Questions + + msg.Questions = []dnsmessage.Question{} + + fmt.Printf("got mdns: %s\n", msg.GoString()) + + for _, q := range questions { + isQU := uint16(q.Class)&QUBit != 0 + + if isQU { + fmt.Printf("found QU: %s\n", q.GoString()) + } else { + fmt.Printf("found QM: %s\n", q.GoString()) + } + } + } +} + +func dataToB64Hash(b []byte) string { + h := sha256.Sum256(b) + + return base64.StdEncoding.EncodeToString(h[:]) +} diff --git a/go.mod b/go.mod index 92f24f9..eac7946 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,17 @@ module github.com/edup2p/common go 1.22 require ( - github.com/LukaGiorgadze/gonull v1.2.0 github.com/abiosoft/ishell/v2 v2.0.2 github.com/dblohm7/wingoes v0.0.0-20240801171404-fc12d7c70140 github.com/go-ole/go-ole v1.3.0 + github.com/google/gopacket v1.1.19 + github.com/sethvargo/go-limiter v1.0.0 github.com/stretchr/testify v1.9.0 go4.org/mem v0.0.0-20220726221520-4f986261bf13 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/crypto v0.33.0 golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f + golang.org/x/net v0.33.0 golang.org/x/sys v0.30.0 golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 @@ -24,7 +26,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.12.0 // indirect github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/josharian/native v1.1.0 // indirect github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-isatty v0.0.12 // indirect @@ -32,7 +34,6 @@ require ( github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.4.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.33.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index be36655..097b178 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/LukaGiorgadze/gonull v1.2.0 h1:I+/pHqr9dySqf6A4agJazrFA8XlrUohqdb10nFIaxJU= -github.com/LukaGiorgadze/gonull v1.2.0/go.mod h1:iGbXOBV6y4VkT14x//F3yZiIxe1ylZYor05pZb0/9TM= github.com/abiosoft/ishell v2.0.0+incompatible h1:zpwIuEHc37EzrsIYah3cpevrIc8Oma7oZPxr03tlmmw= github.com/abiosoft/ishell v2.0.0+incompatible/go.mod h1:HQR9AqF2R3P4XXpMpI0NAzgHf/aS6+zVXRj14cVk9qg= github.com/abiosoft/ishell/v2 v2.0.2 h1:5qVfGiQISaYM8TkbBl7RFO6MddABoXpATrsFbVI+SNo= @@ -23,8 +21,10 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= @@ -43,6 +43,8 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sethvargo/go-limiter v1.0.0 h1:JqW13eWEMn0VFv86OKn8wiYJY/m250WoXdrjRV0kLe4= +github.com/sethvargo/go-limiter v1.0.0/go.mod h1:01b6tW25Ap+MeLYBuD4aHunMrJoNO5PVUFdS9rac3II= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= @@ -53,23 +55,35 @@ go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ= golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo= diff --git a/toversok/actors/a_mman.go b/toversok/actors/a_mman.go new file mode 100644 index 0000000..4f7146d --- /dev/null +++ b/toversok/actors/a_mman.go @@ -0,0 +1,183 @@ +package actors + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "github.com/edup2p/common/types" + "github.com/edup2p/common/types/ifaces" + "github.com/edup2p/common/types/msgactor" + "github.com/sethvargo/go-limiter" + "github.com/sethvargo/go-limiter/memorystore" + "net" + "net/netip" + "time" +) + +type MDNSManager struct { + *ActorCommon + s *Stage + + rlStore limiter.Store + + sock *SockRecv + inj ifaces.Injectable + + working bool +} + +func (s *Stage) makeMM(inj ifaces.Injectable) *MDNSManager { + c := MakeCommon(s.Ctx, MdnsManInboxChLen) + + store, err := memorystore.New(&memorystore.Config{ + // Number of tokens allowed per interval. + Tokens: 1, + + // Interval until tokens reset. + Interval: 20 * time.Second, + + SweepInterval: 1 * time.Minute, + SweepMinTTL: 1 * time.Minute, + }) + if err != nil { + panic(err) + } + + m := &MDNSManager{ + ActorCommon: c, + s: s, + rlStore: store, + } + + if inj == nil { + L(m).Error("could not start MDNS Manager; injector is non-present") + return m + } + m.inj = inj + + bind, err := makeMDNSListener() + if err != nil { + L(m).Error("could not start MDNS Manager; MDNS listener creation failed", "err", err) + + return m + } + m.sock = MakeSockRecv(c.ctx, bind) + + m.working = true + + return m +} + +var ( + MDNSPort uint16 = 5353 + ip4MDNSBroadcastAddress = netip.AddrPortFrom(netip.MustParseAddr("224.0.0.251"), MDNSPort) +) + +// loopbackInterface returns an available logical network interface +// for loopback tests. It returns nil if no suitable interface is +// found. +func loopbackInterface() *net.Interface { + ift, err := net.Interfaces() + if err != nil { + return nil + } + for _, ifi := range ift { + if ifi.Flags&net.FlagLoopback != 0 && ifi.Flags&net.FlagUp != 0 { + return &ifi + } + } + return nil +} + +func makeMDNSListener() (types.UDPConn, error) { + // TODO this only catches ipv4 traffic, which may be a bit "eh", + // it may be worth considering firing up one for each stack. + ua := net.UDPAddrFromAddrPort(ip4MDNSBroadcastAddress) + + return net.ListenMulticastUDP("udp4", loopbackInterface(), ua) +} + +func dataToB64Hash(b []byte) string { + h := sha256.Sum256(b) + + return base64.StdEncoding.EncodeToString(h[:]) +} + +func (mm *MDNSManager) Run() { + if !mm.working { + mm.deadRun() + return + } + + go mm.sock.Run() + + for { + select { + case msg := <-mm.inbox: + // got MDNS message from external; inject + switch msg := msg.(type) { + case *msgactor.MManReceivedPacket: + pi := mm.s.GetPeerInfo(msg.From) + if pi == nil { + L(mm).Warn("ignoring MDNS packet due to nonexistent peerinfo", "from", msg.From.Debug()) + continue + } + + if !mm.inj.Available() { + L(mm).Debug("dropping MDNS packet due to unavailable injector", "from", msg.From.Debug()) + continue + } + + if _, _, _, ok, _ := mm.rlStore.Take(context.Background(), dataToB64Hash(msg.Data)); !ok { + // some rudimentary filtering to prevent true loop storms + continue + } + + L(mm).Log(context.Background(), types.LevelTrace, "injecting external MDNS packet", "len", len(msg.Data), "from", msg.From.Debug()) + + err := mm.inj.InjectPacket(netip.AddrPortFrom(pi.IPv4, MDNSPort), ip4MDNSBroadcastAddress, msg.Data) + if err != nil { + L(mm).Error("failed to inject MDNS packet", "from", msg.From.Debug(), "err", err) + } + default: + mm.logUnknownMessage(msg) + } + case frame := <-mm.sock.outCh: + // got MDNS message from system; forward + + if _, _, _, ok, _ := mm.rlStore.Take(context.Background(), dataToB64Hash(frame.pkt)); !ok { + // some rudimentary filtering to prevent true loop storms + continue + } + + // TODO proper filtering + + //if !frame.src.Addr().IsLoopback() { + // // drop non-loopback, is from LAN + // continue + //} + + L(mm).Log(context.Background(), types.LevelTrace, "spreading local MDNS packet to peers", "len", len(frame.pkt), "from", frame.src.String()) + + SendMessage(mm.s.TMan.Inbox(), &msgactor.TManSpreadMDNSPacket{Pkt: frame.pkt}) + case <-mm.s.Ctx.Done(): + mm.Close() + return + } + } +} + +func (mm *MDNSManager) deadRun() { + for { + select { + case <-mm.inbox: + case <-mm.s.Ctx.Done(): + mm.Close() + return + } + } +} + +func (mm *MDNSManager) Close() { + mm.rlStore.Close(context.Background()) +} diff --git a/toversok/actors/a_sman_test.go b/toversok/actors/a_sman_test.go index f15bcc1..6ea6808 100644 --- a/toversok/actors/a_sman_test.go +++ b/toversok/actors/a_sman_test.go @@ -11,18 +11,22 @@ import ( // Mock Session Message used in this test type MockSessionMessage struct { - marshalSessionMessage func() []byte - debug func() string + marshal func() []byte + debug func() string } -func (m *MockSessionMessage) MarshalSessionMessage() []byte { - return m.marshalSessionMessage() +func (m *MockSessionMessage) Marshal() []byte { + return m.marshal() } func (m *MockSessionMessage) Debug() string { return m.debug() } +func (m *MockSessionMessage) Parse([]byte) error { + panic("implement me") +} + func assertEncryptedPacket(t *testing.T, pkt []byte, sm *SessionManager, expectedDecryption *msgsess.ClearMessage, failMsg string) { // We cannot predict the encryption with a random nonce, so we unpack the packet in receivedReq to test if it is correct unpacked, ok := sm.Unpack(pkt) @@ -66,7 +70,7 @@ func TestSessionManager(t *testing.T) { // Pack the test ping message mockSessionMsg := &MockSessionMessage{ - marshalSessionMessage: func() []byte { + marshal: func() []byte { return clearBytes }, } diff --git a/toversok/actors/a_sockrecv.go b/toversok/actors/a_sockrecv.go index 4cb9a50..af75e27 100644 --- a/toversok/actors/a_sockrecv.go +++ b/toversok/actors/a_sockrecv.go @@ -56,7 +56,7 @@ func (r *SockRecv) Run() { buf := make([]byte, 1<<16) for { - if context.Cause(r.ctx) != nil { + if r.ctx.Err() != nil { return } diff --git a/toversok/actors/a_tman.go b/toversok/actors/a_tman.go index 40a0258..aca782c 100644 --- a/toversok/actors/a_tman.go +++ b/toversok/actors/a_tman.go @@ -111,13 +111,25 @@ func (tm *TrafficManager) Handle(m msgactor.ActorMessage) { case *msgactor.TManSessionMessageFromDirect: n := tm.NodeForSess(m.Msg.Session) - if n != nil { - tm.forState(*n, func(s peerstate.PeerState) peerstate.PeerState { - return s.OnDirect(types.NormaliseAddrPort(m.AddrPort), m.Msg) - }) - } else { + if n == nil { L(tm).Warn("got message from direct for unknown session", "session", m.Msg.Session.Debug()) + return } + + node := *n + + if tm.isMDNS(m.Msg) { + if !tm.mdnsAllowed(node) { + L(tm).Warn("got direct MDNS packet from peer where it is not allowed", "peer", node.Debug()) + return + } + tm.sendMDNS(node, m.Msg) + return + } + + tm.forState(node, func(s peerstate.PeerState) peerstate.PeerState { + return s.OnDirect(types.NormaliseAddrPort(m.AddrPort), m.Msg) + }) case *msgactor.TManSessionMessageFromRelay: if !tm.ValidKeys(m.Peer, m.Msg.Session) { L(tm).Warn("got message from relay for peer with incorrect session", @@ -125,6 +137,15 @@ func (tm *TrafficManager) Handle(m msgactor.ActorMessage) { return } + if tm.isMDNS(m.Msg) { + if !tm.mdnsAllowed(m.Peer) { + L(tm).Warn("got relay MDNS packet from peer where it is not allowed", "peer", m.Peer.Debug()) + return + } + tm.sendMDNS(m.Peer, m.Msg) + return + } + tm.forState(m.Peer, func(s peerstate.PeerState) peerstate.PeerState { return s.OnRelay(m.Relay, m.Peer, m.Msg) }) @@ -146,11 +167,70 @@ func (tm *TrafficManager) Handle(m msgactor.ActorMessage) { return key == m.Peer }) } + case *msgactor.TManSpreadMDNSPacket: + tm.spreadMDNS(m.Pkt) default: tm.logUnknownMessage(m) } } +func (tm *TrafficManager) isMDNS(msg *msgsess.ClearMessage) bool { + sbd, ok := msg.Message.(*msgsess.SideBandData) + + return ok && sbd.Type == msgsess.MDNSType +} + +func (tm *TrafficManager) mdnsAllowed(node key.NodePublic) bool { + pi := tm.s.GetPeerInfo(node) + + if pi == nil { + return false + } + + return pi.MDNS +} + +func (tm *TrafficManager) sendMDNS(peer key.NodePublic, msg *msgsess.ClearMessage) { + sbd := msg.Message.(*msgsess.SideBandData) + + go SendMessage(tm.s.MMan.Inbox(), &msgactor.MManReceivedPacket{ + From: peer, + Data: sbd.Data, + }) +} + +func (tm *TrafficManager) spreadMDNS(pkt []byte) { + peers := tm.s.GetPeersWhere(func(_ key.NodePublic, info *stage.PeerInfo) bool { + return info.MDNS + }) + + for _, peer := range peers { + tm.opportunisticSendTo(peer, &msgsess.SideBandData{ + Type: msgsess.MDNSType, + Data: pkt, + }) + } +} + +func (tm *TrafficManager) opportunisticSendTo(to key.NodePublic, msg msgsess.SessionMessage) { + pi := tm.s.GetPeerInfo(to) + + if pi == nil { + L(tm).Warn("trying to send an opportunistic session message to a node without peerinfo", "to", to.Debug()) + return + } + + tm.forState(to, func(s peerstate.PeerState) peerstate.PeerState { + if e, ok := s.(*peerstate.Established); ok { + tm.SendMsgToDirect(e.GetEndpoint(), pi.Session, msg) + } else { + tm.SendMsgToRelay(pi.HomeRelay, to, pi.Session, msg) + } + + return nil + }) +} + func (tm *TrafficManager) DoStateTick() { // We explicitly range over a slice of the keys we already got, // since golang likes to complain when we mutate while we iterate. @@ -345,8 +425,10 @@ func (tm *TrafficManager) ValidKeys(peer key.NodePublic, session key.SessionPubl } func (tm *TrafficManager) SendPingDirect(endpoint netip.AddrPort, peer key.NodePublic, session key.SessionPublic) { - txid := msgsess.NewTxID() + tm.SendPingDirectWithID(endpoint, peer, session, msgsess.NewTxID()) +} +func (tm *TrafficManager) SendPingDirectWithID(endpoint netip.AddrPort, peer key.NodePublic, session key.SessionPublic, txid msgsess.TxID) { nep := types.NormaliseAddrPort(endpoint) tm.SendMsgToDirect(nep, session, &msgsess.Ping{ diff --git a/toversok/actors/consts.go b/toversok/actors/consts.go index 8bb4ac5..2e6aceb 100644 --- a/toversok/actors/consts.go +++ b/toversok/actors/consts.go @@ -15,6 +15,7 @@ const ( TrafficManInboxChLen = 16 RelayManInboxChLen = 4 DirectRouterInboxChLen = 4 + MdnsManInboxChLen = 32 // Frame SockRecvFrameChanBuffer = 256 diff --git a/toversok/actors/peerstate/pingtracker.go b/toversok/actors/peerstate/pingtracker.go index 95a8d67..3810581 100644 --- a/toversok/actors/peerstate/pingtracker.go +++ b/toversok/actors/peerstate/pingtracker.go @@ -40,6 +40,14 @@ func (pt *PingTracker) GotPong(ap netip.AddrPort) { pt.gotPong[nap] = true } +func (pt *PingTracker) Has(ap netip.AddrPort) bool { + pt.rw.Lock() + defer pt.rw.Unlock() + + nap := types.NormaliseAddrPort(ap) + return pt.gotPong[nap] +} + func (pt *PingTracker) BestAddrPort() (netip.AddrPort, error) { pt.rw.RLock() defer pt.rw.RUnlock() diff --git a/toversok/actors/peerstate/s_established.go b/toversok/actors/peerstate/s_established.go index 4f01897..5e6ee49 100644 --- a/toversok/actors/peerstate/s_established.go +++ b/toversok/actors/peerstate/s_established.go @@ -80,6 +80,8 @@ func (e *Established) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage return s } + ap = types.NormaliseAddrPort(ap) + LogDirectMessage(e, ap, clearMsg) // TODO check if endpoint is same as current used one @@ -101,7 +103,7 @@ func (e *Established) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage e.lastPingRecv = time.Now() e.replyWithPongDirect(ap, clearMsg.Session, m) - if ap != e.currentOutEndpoint { + if ap != e.currentOutEndpoint && !e.tracker.Has(ap) { // We're not sending pings to this, yet we may want to, to prevent asymmetric glare pi := e.getPeerInfo() if pi == nil { @@ -109,7 +111,10 @@ func (e *Established) OnDirect(ap netip.AddrPort, clearMsg *msgsess.ClearMessage return nil } - e.tm.SendPingDirect(ap, e.peer, pi.Session) + L(e).Log(context.Background(), types.LevelTrace, "sending ping to ping to prevent assymetric glare", "ap", ap.String(), "current", e.currentOutEndpoint.String()) + + // Send ping with ID, so that it eventually blackholes + e.tm.SendPingDirectWithID(ap, e.peer, pi.Session, m.TxID) } return nil @@ -164,6 +169,10 @@ func (e *Established) OnRelay(relay int64, peer key.NodePublic, clearMsg *msgses } } +func (e *Established) GetEndpoint() netip.AddrPort { + return e.currentOutEndpoint +} + // canTrustEndpoint returns true if the endpoint that has been given corresponds to the peer. // this will check the current knownInEndpoints, and if it does not exist, will check peerInfo to see if the peer // sent this endpoint in the past with rendezvous. If so, adds it to the knownInEndpoints, and sends a SetAKA. @@ -207,6 +216,7 @@ func (e *Established) checkChangedPreferredEndpoint() { } if bap != e.currentOutEndpoint { + L(e).Log(context.Background(), types.LevelTrace, "switching bestaddrport", "bap", bap.String(), "current", e.currentOutEndpoint.String()) // not the best one, switch e.switchToEndpoint(bap) } diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index 9083e1f..5e7b84a 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -47,6 +47,8 @@ func MakeStage( controlSession ifaces.ControlInterface, dialRelayFunc relayhttp.RelayDialFunc, + + injectable ifaces.Injectable, ) ifaces.Stage { // FIXME ??? why the fuck did we ever decide on this // ctx := context.WithoutCancel(pCtx) @@ -86,6 +88,7 @@ func MakeStage( s.TMan = s.makeTM() s.SMan = s.makeSM(sessPriv) s.EMan = s.makeEM() + s.MMan = s.makeMM(injectable) return s } @@ -111,6 +114,8 @@ type Stage struct { SMan ifaces.SessionManagerActor // The EndpointManager EMan ifaces.EndpointManagerActor + // The MDNSManager + MMan ifaces.MDNSManagerActor connMutex sync.RWMutex inConn map[key.NodePublic]InConnActor @@ -153,6 +158,7 @@ func (s *Stage) Start() { go s.TMan.Run() go s.SMan.Run() go s.EMan.Run() + go s.MMan.Run() go s.DMan.Run() go s.DRouter.Run() @@ -463,7 +469,7 @@ func (s *Stage) notifyEndpointChanged() { } } -func (s *Stage) AddPeer(peer key.NodePublic, homeRelay int64, endpoints []netip.AddrPort, session key.SessionPublic, _, _ netip.Addr, _ msgcontrol.Properties) error { +func (s *Stage) AddPeer(peer key.NodePublic, homeRelay int64, endpoints []netip.AddrPort, session key.SessionPublic, ip4, ip6 netip.Addr, prop msgcontrol.Properties) error { s.peerInfoMutex.Lock() defer func() { @@ -482,6 +488,9 @@ func (s *Stage) AddPeer(peer key.NodePublic, homeRelay int64, endpoints []netip. Endpoints: types.NormaliseAddrPortSlice(endpoints), RendezvousEndpoints: make([]netip.AddrPort, 0), Session: session, + IPv4: ip4, + IPv6: ip6, + MDNS: prop.MDNS, } return nil @@ -489,7 +498,7 @@ func (s *Stage) AddPeer(peer key.NodePublic, homeRelay int64, endpoints []netip. var errNoPeerInfo = errors.New("could not find peer info to update") -func (s *Stage) UpdatePeer(peer key.NodePublic, homeRelay *int64, endpoints []netip.AddrPort, session *key.SessionPublic, _ *msgcontrol.Properties) error { +func (s *Stage) UpdatePeer(peer key.NodePublic, homeRelay *int64, endpoints []netip.AddrPort, session *key.SessionPublic, prop *msgcontrol.Properties) error { return s.updatePeerInfo(peer, func(info *stage.PeerInfo) { if homeRelay != nil { info.HomeRelay = *homeRelay @@ -500,6 +509,9 @@ func (s *Stage) UpdatePeer(peer key.NodePublic, homeRelay *int64, endpoints []ne if session != nil { info.Session = *session } + if prop != nil { + info.MDNS = prop.MDNS + } }) } @@ -549,6 +561,19 @@ func (s *Stage) GetPeerInfo(peer key.NodePublic) *stage.PeerInfo { return s.peerInfo[peer] } +func (s *Stage) GetPeersWhere(f func(key.NodePublic, *stage.PeerInfo) bool) []key.NodePublic { + s.peerInfoMutex.RLock() + defer s.peerInfoMutex.RUnlock() + + var peers []key.NodePublic + for peer, info := range s.peerInfo { + if f(peer, info) { + peers = append(peers, peer) + } + } + return peers +} + func (s *Stage) RemovePeer(peer key.NodePublic) error { s.peerInfoMutex.Lock() delete(s.peerInfo, peer) diff --git a/toversok/engine.go b/toversok/engine.go index 33ff634..cc86cae 100644 --- a/toversok/engine.go +++ b/toversok/engine.go @@ -200,7 +200,7 @@ func (e *Engine) installSession(allowLogon bool) error { // WillRestart says whether the engine strives to be in a running state. func (e *Engine) WillRestart() bool { - return e.doAutoRestart + return e.doAutoRestart && e.ctx.Err() != nil } func (e *Engine) slog() *slog.Logger { diff --git a/toversok/session.go b/toversok/session.go index cb18e61..5d902a2 100644 --- a/toversok/session.go +++ b/toversok/session.go @@ -76,7 +76,22 @@ func SetupSession( return nil, err } - sess.stage = actors.MakeStage(sess.ctx, getNodePriv, sess.getPriv, getExtSock, sess.wg.ConnFor, sess.cs, nil) + var inj ifaces.Injectable + // TODO have fallbacks, try to acquire system injection, etc. + if i, ok := sess.wg.(ifaces.Injectable); ok { + inj = i + } + + sess.stage = actors.MakeStage( + sess.ctx, + getNodePriv, + sess.getPriv, + getExtSock, + sess.wg.ConnFor, + sess.cs, + nil, + inj, + ) sess.cs.InstallCallbacks(sess) context.AfterFunc(sess.cs.Context(), func() { diff --git a/types/ifaces/actor.go b/types/ifaces/actor.go index 6006c55..9fdaf28 100644 --- a/types/ifaces/actor.go +++ b/types/ifaces/actor.go @@ -77,6 +77,7 @@ type TrafficManagerActor interface { SendMsgToDirect(ap netip.AddrPort, sess key.SessionPublic, m msgsess.SessionMessage) SendMsgToRelay(relay int64, node key.NodePublic, sess key.SessionPublic, m msgsess.SessionMessage) SendPingDirect(ap netip.AddrPort, peer key.NodePublic, session key.SessionPublic) + SendPingDirectWithID(ap netip.AddrPort, peer key.NodePublic, session key.SessionPublic, txid msgsess.TxID) OutConnUseAddrPort(peer key.NodePublic, ap netip.AddrPort) OutConnTrackHome(peer key.NodePublic) @@ -104,3 +105,9 @@ type SessionManagerActor interface { type EndpointManagerActor interface { Actor } + +// === + +type MDNSManagerActor interface { + Actor +} diff --git a/types/ifaces/injectable.go b/types/ifaces/injectable.go new file mode 100644 index 0000000..b12f4ec --- /dev/null +++ b/types/ifaces/injectable.go @@ -0,0 +1,9 @@ +package ifaces + +import "net/netip" + +type Injectable interface { + Available() bool + + InjectPacket(from, to netip.AddrPort, pkt []byte) error +} diff --git a/types/msgactor/msg.go b/types/msgactor/msg.go index 5cef33a..1be49b4 100644 --- a/types/msgactor/msg.go +++ b/types/msgactor/msg.go @@ -45,6 +45,10 @@ type TManSessionMessageFromDirect struct { Msg *msgsess.ClearMessage } +type TManSpreadMDNSPacket struct { + Pkt []byte +} + // ====================================================================================================== // SessionManager msgs @@ -107,6 +111,15 @@ type RManRelayLatencyResults struct { RelayLatency map[int64]time.Duration } +// ====================================================================================================== +// MDNSManager msgs + +type MManReceivedPacket struct { + From key.NodePublic + + Data []byte +} + // ====================================================================================================== // DirectRouter msgs diff --git a/types/msgactor/msg_iface.go b/types/msgactor/msg_iface.go index d8e5d4b..1e2afd6 100644 --- a/types/msgactor/msg_iface.go +++ b/types/msgactor/msg_iface.go @@ -10,6 +10,7 @@ func (o *TManConnActivity) amsg() {} func (o *TManConnGoodBye) amsg() {} func (o *TManSessionMessageFromRelay) amsg() {} func (o *TManSessionMessageFromDirect) amsg() {} +func (o *TManSpreadMDNSPacket) amsg() {} func (o *SManSessionFrameFromRelay) amsg() {} func (o *SManSessionFrameFromAddrPort) amsg() {} @@ -19,6 +20,8 @@ func (o *OutConnUse) amsg() {} func (o *RManRelayLatencyResults) amsg() {} +func (o *MManReceivedPacket) amsg() {} + func (o *DManSetMTU) amsg() {} func (o *DRouterPeerClearKnownAs) amsg() {} func (o *DRouterPeerAddKnownAs) amsg() {} diff --git a/types/msgsess/consts.go b/types/msgsess/consts.go index 04b3a6b..a137956 100644 --- a/types/msgsess/consts.go +++ b/types/msgsess/consts.go @@ -15,8 +15,9 @@ const v1 = VersionMarker(0x1) type MessageType byte const ( - PingMessage = MessageType(0x00) - PongMessage = MessageType(0x01) + PingMessage = MessageType(iota) + PongMessage + SideBandDataMessage RendezvousMessage = MessageType(0xFF) ) diff --git a/types/stage/stage.go b/types/stage/stage.go index 617d90b..2787325 100644 --- a/types/stage/stage.go +++ b/types/stage/stage.go @@ -21,4 +21,6 @@ type PeerInfo struct { Endpoints []netip.AddrPort RendezvousEndpoints []netip.AddrPort Session key.SessionPublic + IPv4, IPv6 netip.Addr + MDNS bool } diff --git a/usrwg/wgusp.go b/usrwg/wgusp.go index d10156f..8dfad8d 100644 --- a/usrwg/wgusp.go +++ b/usrwg/wgusp.go @@ -2,13 +2,18 @@ package usrwg import ( "fmt" + "github.com/google/gopacket/layers" + "golang.zx2c4.com/wireguard/tun" "log/slog" "net/netip" + "slices" + "syscall" "github.com/edup2p/common/toversok" "github.com/edup2p/common/types" "github.com/edup2p/common/types/key" "github.com/edup2p/common/usrwg/router" + "github.com/google/gopacket" "golang.zx2c4.com/wireguard/device" ) @@ -32,8 +37,7 @@ func (u *UserSpaceWireGuardHost) Reset() error { return nil } -const WGGOIPCDevSetup = `private_key=%s -` +const WGGOIPCDevSetup = "private_key=%s\n" func (u *UserSpaceWireGuardHost) Controller(privateKey key.NodePrivate, addr4, addr6 netip.Prefix) (toversok.WireGuardController, error) { if u.running != nil { @@ -97,6 +101,7 @@ func (u *UserSpaceWireGuardHost) Controller(privateKey key.NodePrivate, addr4, a usrwgc := &UserSpaceWireGuardController{ wgDev: wgDev, bind: bind, + tunDev: tunDev, router: r, } @@ -108,9 +113,52 @@ func (u *UserSpaceWireGuardHost) Controller(privateKey key.NodePrivate, addr4, a type UserSpaceWireGuardController struct { wgDev *device.Device bind *ToverSokBind + tunDev tun.Device router router.Router } +func (u *UserSpaceWireGuardController) Available() bool { + return true +} + +func (u *UserSpaceWireGuardController) InjectPacket(from, to netip.AddrPort, pkt []byte) error { + buf := gopacket.NewSerializeBuffer() + opts := gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + } + ipv4 := &layers.IPv4{ + Version: 0x4, + TTL: 255, + Protocol: syscall.IPPROTO_UDP, + DstIP: to.Addr().AsSlice(), + SrcIP: from.Addr().AsSlice(), + } + udp := &layers.UDP{ + DstPort: layers.UDPPort(to.Port()), + SrcPort: layers.UDPPort(from.Port()), + } + udp.SetNetworkLayerForChecksum(ipv4) + + err := gopacket.SerializeLayers(buf, opts, + ipv4, + udp, + gopacket.Payload(pkt), + ) + + if err != nil { + return fmt.Errorf("failed to serialize packet: %w", err) + } + + packetData := slices.Concat(make([]byte, 16), buf.Bytes()) + + if _, err = u.tunDev.Write([][]byte{packetData}, 16); err != nil { + return fmt.Errorf("failed to inject packet: %w", err) + } + + return nil +} + const WGGOIPCAddPeer = `public_key=%s replace_allowed_ips=true allowed_ip=%s/32 From ff7f1a0a9529c872cd028512413661d55cdf841f Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 6 Mar 2025 11:07:03 +0100 Subject: [PATCH 51/82] remove injectable from mdns manager --- toversok/actors/a_mman.go | 22 +++------------------- toversok/actors/stage.go | 4 +--- toversok/session.go | 7 ------- 3 files changed, 4 insertions(+), 29 deletions(-) diff --git a/toversok/actors/a_mman.go b/toversok/actors/a_mman.go index 4f7146d..0f7be96 100644 --- a/toversok/actors/a_mman.go +++ b/toversok/actors/a_mman.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "encoding/base64" "github.com/edup2p/common/types" - "github.com/edup2p/common/types/ifaces" "github.com/edup2p/common/types/msgactor" "github.com/sethvargo/go-limiter" "github.com/sethvargo/go-limiter/memorystore" @@ -21,12 +20,11 @@ type MDNSManager struct { rlStore limiter.Store sock *SockRecv - inj ifaces.Injectable working bool } -func (s *Stage) makeMM(inj ifaces.Injectable) *MDNSManager { +func (s *Stage) makeMM() *MDNSManager { c := MakeCommon(s.Ctx, MdnsManInboxChLen) store, err := memorystore.New(&memorystore.Config{ @@ -49,12 +47,6 @@ func (s *Stage) makeMM(inj ifaces.Injectable) *MDNSManager { rlStore: store, } - if inj == nil { - L(m).Error("could not start MDNS Manager; injector is non-present") - return m - } - m.inj = inj - bind, err := makeMDNSListener() if err != nil { L(m).Error("could not start MDNS Manager; MDNS listener creation failed", "err", err) @@ -123,22 +115,14 @@ func (mm *MDNSManager) Run() { continue } - if !mm.inj.Available() { - L(mm).Debug("dropping MDNS packet due to unavailable injector", "from", msg.From.Debug()) - continue - } - if _, _, _, ok, _ := mm.rlStore.Take(context.Background(), dataToB64Hash(msg.Data)); !ok { // some rudimentary filtering to prevent true loop storms continue } - L(mm).Log(context.Background(), types.LevelTrace, "injecting external MDNS packet", "len", len(msg.Data), "from", msg.From.Debug()) + L(mm).Log(context.Background(), types.LevelTrace, "processing external MDNS packet", "len", len(msg.Data), "from", msg.From.Debug()) - err := mm.inj.InjectPacket(netip.AddrPortFrom(pi.IPv4, MDNSPort), ip4MDNSBroadcastAddress, msg.Data) - if err != nil { - L(mm).Error("failed to inject MDNS packet", "from", msg.From.Debug(), "err", err) - } + // TODO process external mDNS packet default: mm.logUnknownMessage(msg) } diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index 5e7b84a..6d5ba0d 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -47,8 +47,6 @@ func MakeStage( controlSession ifaces.ControlInterface, dialRelayFunc relayhttp.RelayDialFunc, - - injectable ifaces.Injectable, ) ifaces.Stage { // FIXME ??? why the fuck did we ever decide on this // ctx := context.WithoutCancel(pCtx) @@ -88,7 +86,7 @@ func MakeStage( s.TMan = s.makeTM() s.SMan = s.makeSM(sessPriv) s.EMan = s.makeEM() - s.MMan = s.makeMM(injectable) + s.MMan = s.makeMM() return s } diff --git a/toversok/session.go b/toversok/session.go index 5d902a2..6c793e3 100644 --- a/toversok/session.go +++ b/toversok/session.go @@ -76,12 +76,6 @@ func SetupSession( return nil, err } - var inj ifaces.Injectable - // TODO have fallbacks, try to acquire system injection, etc. - if i, ok := sess.wg.(ifaces.Injectable); ok { - inj = i - } - sess.stage = actors.MakeStage( sess.ctx, getNodePriv, @@ -90,7 +84,6 @@ func SetupSession( sess.wg.ConnFor, sess.cs, nil, - inj, ) sess.cs.InstallCallbacks(sess) From 87ba66a1df2420f93ed1609d78e8fed025c3b301 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 7 Mar 2025 11:39:37 +0100 Subject: [PATCH 52/82] eman: skip ppp and down interfaces for local endpoints --- toversok/actors/a_eman.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/toversok/actors/a_eman.go b/toversok/actors/a_eman.go index 3435bac..b86a3af 100644 --- a/toversok/actors/a_eman.go +++ b/toversok/actors/a_eman.go @@ -292,6 +292,12 @@ func (em *EndpointManager) collectLocalEndpoints() []netip.Addr { // handle err for _, i := range ifaces { + + if i.Flags&net.FlagUp == 0 || i.Flags&net.FlagPointToPoint != 0 { + // Skip interfaces that are down, or are also PPP (such as tailscale) + continue + } + addrs, err := i.Addrs() if err != nil { L(em).Warn("collectLocalEndpoints: could not get addresses from interface", "error", err, "iface", i.Name) From e4a4abf77dcff9a7dadb8d0ad78638843ecbf67b Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 7 Mar 2025 11:41:09 +0100 Subject: [PATCH 53/82] wg.GetInterface() --- extwg/wgctrl.go | 10 ++++++++++ toversok/actors/stage.go | 6 ++++++ toversok/interface.go | 3 +++ toversok/session.go | 1 + usrwg/wgusp.go | 15 +++++++++++++++ 5 files changed, 35 insertions(+) diff --git a/extwg/wgctrl.go b/extwg/wgctrl.go index c0e9706..5737d14 100644 --- a/extwg/wgctrl.go +++ b/extwg/wgctrl.go @@ -3,6 +3,7 @@ package extwg import ( "errors" "fmt" + "log" "log/slog" "net" "net/netip" @@ -226,6 +227,15 @@ func (w *WGCtrl) GetStats(publicKey key.NodePublic) (*toversok.WGStats, error) { }, nil } +func (w *WGCtrl) GetInterface() *net.Interface { + i, err := net.InterfaceByName(w.name) + if err != nil { + log.Println("cannot find interface ", w.name, ":", err) + return nil + } + return i +} + func (w *WGCtrl) ensureLocalConn(peer key.NodePublic) *mapping { m, ok := w.localMapping[peer] diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index 6d5ba0d..7853d20 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -47,6 +47,8 @@ func MakeStage( controlSession ifaces.ControlInterface, dialRelayFunc relayhttp.RelayDialFunc, + + wgIf *net.Interface, ) ifaces.Stage { // FIXME ??? why the fuck did we ever decide on this // ctx := context.WithoutCancel(pCtx) @@ -74,6 +76,8 @@ func MakeStage( bindLocal: bindLocal, control: controlSession, + wgIf: wgIf, + dialRelayFunc: dialRelayFunc, } @@ -131,6 +135,8 @@ type Stage struct { control ifaces.ControlInterface + wgIf *net.Interface + //// A repeatable function to an outside context to acquire a new UDPconn, //// once a peer conn has died for whatever reason. // TODO rework this? diff --git a/toversok/interface.go b/toversok/interface.go index 8f6168d..df61aa0 100644 --- a/toversok/interface.go +++ b/toversok/interface.go @@ -2,6 +2,7 @@ package toversok import ( "context" + "net" "net/netip" "time" @@ -73,6 +74,8 @@ type WireGuardController interface { // // Can possibly return nil, when the peer has been removed, or not yet known to the controller. ConnFor(node key.NodePublic) types.UDPConn + + GetInterface() *net.Interface } type FirewallHost interface { diff --git a/toversok/session.go b/toversok/session.go index 6c793e3..ecb6422 100644 --- a/toversok/session.go +++ b/toversok/session.go @@ -84,6 +84,7 @@ func SetupSession( sess.wg.ConnFor, sess.cs, nil, + sess.wg.GetInterface(), ) sess.cs.InstallCallbacks(sess) diff --git a/usrwg/wgusp.go b/usrwg/wgusp.go index 8dfad8d..3392a41 100644 --- a/usrwg/wgusp.go +++ b/usrwg/wgusp.go @@ -5,6 +5,7 @@ import ( "github.com/google/gopacket/layers" "golang.zx2c4.com/wireguard/tun" "log/slog" + "net" "net/netip" "slices" "syscall" @@ -198,6 +199,20 @@ func (u *UserSpaceWireGuardController) ConnFor(node key.NodePublic) types.UDPCon return u.bind.GetConn(node) } +func (u *UserSpaceWireGuardController) GetInterface() *net.Interface { + name, err := u.tunDev.Name() + if err != nil { + slog.Warn("failed to get tun device name", "err", err) + return nil + } + i, err := net.InterfaceByName(name) + if err != nil { + slog.Warn("failed to get interface", "name", name, "err", err) + return nil + } + return i +} + func (u *UserSpaceWireGuardController) Close() { if err := u.bind.Cancel(); err != nil { slog.Error("Failed to close wireguard bind", "err", err) From cf22bbb783b2ec873b2feb4b72f84d489cf79213 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 7 Mar 2025 11:41:26 +0100 Subject: [PATCH 54/82] stage.getLocalEndpoints() --- toversok/actors/stage.go | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index 7853d20..980bbeb 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -123,8 +123,10 @@ type Stage struct { inConn map[key.NodePublic]InConnActor outConn map[key.NodePublic]OutConnActor - getNodePriv func() *key.NodePrivate - getSessPriv func() *key.SessionPrivate + getNodePriv func() *key.NodePrivate + getSessPriv func() *key.SessionPrivate + + endpointMutex sync.RWMutex localEndpoints []netip.AddrPort stunEndpoints []netip.AddrPort @@ -381,15 +383,15 @@ func (s *Stage) addConnLocked(peer key.NodePublic, udp types.UDPConn) { } func (s *Stage) GetEndpoints() []netip.AddrPort { - s.connMutex.RLock() - defer s.connMutex.RUnlock() + s.endpointMutex.RLock() + defer s.endpointMutex.RUnlock() return slices.Concat(s.localEndpoints, s.stunEndpoints) } func (s *Stage) setSTUNEndpoints(endpoints []netip.AddrPort) { - s.connMutex.Lock() - defer s.connMutex.Unlock() + s.endpointMutex.Lock() + defer s.endpointMutex.Unlock() sortEndpointSlice(endpoints) @@ -404,8 +406,8 @@ func (s *Stage) setSTUNEndpoints(endpoints []netip.AddrPort) { } func (s *Stage) setLocalEndpoints(addrs []netip.Addr) { - s.connMutex.RLock() - defer s.connMutex.RUnlock() + s.endpointMutex.Lock() + defer s.endpointMutex.Unlock() localPort := s.getLocalPort() @@ -440,6 +442,15 @@ func (s *Stage) setLocalEndpoints(addrs []netip.Addr) { s.notifyEndpointChanged() } +func (s *Stage) getLocalEndpoints() []netip.Addr { + s.endpointMutex.RLock() + defer s.endpointMutex.RUnlock() + + return types.Map(s.localEndpoints, func(t netip.AddrPort) netip.Addr { + return t.Addr() + }) +} + func (s *Stage) getLocalPort() uint16 { type HasLocalAddr interface { LocalAddr() net.Addr From e3b00019a2e791b77fa4c8f77184127ca1e02e0d Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 7 Mar 2025 11:41:57 +0100 Subject: [PATCH 55/82] ensurePeerState() in forState() --- toversok/actors/a_tman.go | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/toversok/actors/a_tman.go b/toversok/actors/a_tman.go index aca782c..46ce057 100644 --- a/toversok/actors/a_tman.go +++ b/toversok/actors/a_tman.go @@ -1,6 +1,7 @@ package actors import ( + "context" "maps" "net/netip" "runtime/debug" @@ -204,6 +205,10 @@ func (tm *TrafficManager) spreadMDNS(pkt []byte) { return info.MDNS }) + peersDebug := types.Map(peers, func(t key.NodePublic) string { + return t.Debug() + }) + L(tm).Log(context.Background(), types.LevelTrace, "sending mdns packet to peers", "peers", peersDebug) for _, peer := range peers { tm.opportunisticSendTo(peer, &msgsess.SideBandData{ Type: msgsess.MDNSType, @@ -221,6 +226,8 @@ func (tm *TrafficManager) opportunisticSendTo(to key.NodePublic, msg msgsess.Ses } tm.forState(to, func(s peerstate.PeerState) peerstate.PeerState { + L(tm).Log(context.Background(), types.LevelTrace, "sending opportunistic session message to peer", "peer", to.Debug()) + if e, ok := s.(*peerstate.Established); ok { tm.SendMsgToDirect(e.GetEndpoint(), pi.Session, msg) } else { @@ -344,19 +351,9 @@ func (tm *TrafficManager) forState(peer key.NodePublic, fn StateForState) { // A state for a state, perfectly balanced, as all things should be. // - Thanos, while writing this code. - state, ok := tm.peerState[peer] - - if !ok { - return - } - - if state == nil { - L(tm).Error("found nil state when running update for peer, recovering...", "peer", peer.Debug()) - tm.ensurePeerState(peer) - state = tm.peerState[peer] - } + tm.ensurePeerState(peer) - newState := fn(state) + newState := fn(tm.peerState[peer]) if newState != nil { // state transitions have happened, store the new state From 462ed4a336d77bf6985552981426e790fc1d99e6 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 7 Mar 2025 11:42:44 +0100 Subject: [PATCH 56/82] mDNS work make it work on; - windows - macos - linux --- toversok/actors/a_mman.go | 237 ++++++++++++++++++++++++++++++++++---- 1 file changed, 215 insertions(+), 22 deletions(-) diff --git a/toversok/actors/a_mman.go b/toversok/actors/a_mman.go index 0f7be96..bf87deb 100644 --- a/toversok/actors/a_mman.go +++ b/toversok/actors/a_mman.go @@ -4,12 +4,17 @@ import ( "context" "crypto/sha256" "encoding/base64" + "fmt" "github.com/edup2p/common/types" "github.com/edup2p/common/types/msgactor" "github.com/sethvargo/go-limiter" "github.com/sethvargo/go-limiter/memorystore" + "golang.org/x/net/dns/dnsmessage" + "golang.org/x/net/ipv4" "net" "net/netip" + "runtime" + "slices" "time" ) @@ -19,7 +24,8 @@ type MDNSManager struct { rlStore limiter.Store - sock *SockRecv + broadSock *SockRecv + querySock *SockRecv working bool } @@ -47,13 +53,22 @@ func (s *Stage) makeMM() *MDNSManager { rlStore: store, } - bind, err := makeMDNSListener() + bind, err := m.makeMDNSListener() if err != nil { L(m).Error("could not start MDNS Manager; MDNS listener creation failed", "err", err) return m } - m.sock = MakeSockRecv(c.ctx, bind) + m.broadSock = MakeSockRecv(c.ctx, bind) + + queryBind, err := m.makeQueryListener() + if err != nil { + L(m).Error("could not start MDNS Manager; MDNS sender creation failed", "err", err) + + return m + } + + m.querySock = MakeSockRecv(c.ctx, queryBind) m.working = true @@ -62,6 +77,8 @@ func (s *Stage) makeMM() *MDNSManager { var ( MDNSPort uint16 = 5353 + ip4uaMDNSBare = net.UDPAddr{IP: net.IPv4(224, 0, 0, 251)} + ip4MDNSLoopBackAP = netip.AddrPortFrom(netip.MustParseAddr("127.0.0.1"), MDNSPort) ip4MDNSBroadcastAddress = netip.AddrPortFrom(netip.MustParseAddr("224.0.0.251"), MDNSPort) ) @@ -81,12 +98,53 @@ func loopbackInterface() *net.Interface { return nil } -func makeMDNSListener() (types.UDPConn, error) { +func (mm *MDNSManager) makeMDNSListener() (types.UDPConn, error) { // TODO this only catches ipv4 traffic, which may be a bit "eh", // it may be worth considering firing up one for each stack. ua := net.UDPAddrFromAddrPort(ip4MDNSBroadcastAddress) - return net.ListenMulticastUDP("udp4", loopbackInterface(), ua) + conn, err := net.ListenUDP("udp4", ua) + if err != nil { + return nil, fmt.Errorf("ListenUDP error: %w", err) + } + + pc := ipv4.NewPacketConn(conn) + + ift, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("cannot get interfaces: %w", err) + } + for _, ifi := range ift { + if ifi.Flags&net.FlagUp != 0 && ifi.Flags&net.FlagPointToPoint == 0 { + if err := pc.JoinGroup(&ifi, &ip4uaMDNSBare); err != nil { + L(mm).Warn("Multicast JoinGroup failed", "err", err, "iface", ifi.Name) + } + } + } + + if loop, err := pc.MulticastLoopback(); err == nil { + if !loop { + if err := pc.SetMulticastLoopback(true); err != nil { + return nil, fmt.Errorf("cannot set multicast loopback: %w", err) + } + } + } + + return conn, nil +} + +func (mm *MDNSManager) makeQueryListener() (types.UDPConn, error) { + var laddr *net.UDPAddr + addr := ip4MDNSLoopBackAP + + if runtime.GOOS == "windows" { + laddr = net.UDPAddrFromAddrPort( + netip.AddrPortFrom(mm.s.control.IPv4().Addr(), 0), + ) + addr = ip4MDNSBroadcastAddress + } + + return net.DialUDP("udp4", laddr, net.UDPAddrFromAddrPort(addr)) } func dataToB64Hash(b []byte) string { @@ -101,7 +159,8 @@ func (mm *MDNSManager) Run() { return } - go mm.sock.Run() + go mm.broadSock.Run() + go mm.querySock.Run() for { select { @@ -120,35 +179,169 @@ func (mm *MDNSManager) Run() { continue } - L(mm).Log(context.Background(), types.LevelTrace, "processing external MDNS packet", "len", len(msg.Data), "from", msg.From.Debug()) + L(mm).Debug("processing external MDNS packet", "len", len(msg.Data), "from", msg.From.Debug()) + + pkt := mm.processMDNS(msg.Data, false) + + var err error // TODO process external mDNS packet + + if runtime.GOOS == "darwin" || runtime.GOOS == "windows" { + // On macOS, we can't use the broadsock's WriteTo, since it just doesn't generate a packet. + // However, we can use our specialised query sock to poke responses in unicast, even if they're QM. + _, err = mm.querySock.Conn.Write(pkt) + } else { + _, err = mm.broadSock.Conn.WriteToUDPAddrPort(pkt, ip4MDNSBroadcastAddress) + } + if err != nil { + L(mm).Warn("failed to process external MDNS packet", "err", err) + } default: mm.logUnknownMessage(msg) } - case frame := <-mm.sock.outCh: - // got MDNS message from system; forward + case frame := <-mm.broadSock.outCh: + mm.handleSystemFrame(frame) + case frame := <-mm.querySock.outCh: + mm.handleSystemFrame(frame) + case <-mm.s.Ctx.Done(): + mm.Close() + return + } + } +} - if _, _, _, ok, _ := mm.rlStore.Take(context.Background(), dataToB64Hash(frame.pkt)); !ok { - // some rudimentary filtering to prevent true loop storms - continue +func (mm *MDNSManager) handleSystemFrame(frame RecvFrame) { + // got MDNS message from system; forward + + if _, _, _, ok, _ := mm.rlStore.Take(context.Background(), dataToB64Hash(frame.pkt)); !ok { + // some rudimentary filtering to prevent true loop storms + return + } + + // TODO proper filtering + + //if !frame.src.Addr().IsLoopback() { + // // drop non-loopback, is from LAN + // continue + //} + + L(mm).Debug("spreading local MDNS packet to peers", "len", len(frame.pkt), "from", frame.src.String()) + + pkt := mm.processMDNS(frame.pkt, true) + + SendMessage(mm.s.TMan.Inbox(), &msgactor.TManSpreadMDNSPacket{Pkt: pkt}) +} + +func (mm *MDNSManager) debugMDNS(msg *dnsmessage.Message) { + L(mm).Debug("debugMDNS: TXID", "txid", msg.ID) + + for _, q := range msg.Questions { + L(mm).Debug( + "debugMDNS: Q", + "txid", msg.ID, + "name", q.Name, + "type", q.Type.GoString(), + "class", q.Class.GoString(), + ) + } + for _, a := range msg.Answers { + L(mm).Debug( + "debugMDNS: A", + "txid", msg.ID, + "header", a.Header.GoString(), + "body", a.Body.GoString(), + ) + } +} + +func (mm *MDNSManager) fixResource(res *dnsmessage.Resource) (dirty bool) { + switch res.Header.Type { + case dnsmessage.TypeA: + ar := res.Body.(*dnsmessage.AResource) + if mm.isLocal(netip.AddrFrom4(ar.A)) { + ar.A = mm.s.control.IPv4().Addr().As4() + res.Header.Class |= 32768 + dirty = true + } + case dnsmessage.TypeAAAA: + a4r := res.Body.(*dnsmessage.AAAAResource) + + if mm.isLocal(netip.AddrFrom16(a4r.AAAA)) { + a4r.AAAA = mm.s.control.IPv6().Addr().As16() + res.Header.Class |= 32768 + dirty = true + } + } + + return +} + +func (mm *MDNSManager) isLocal(addr netip.Addr) bool { + return addr.IsLoopback() || slices.IndexFunc(mm.s.getLocalEndpoints(), func(cAddr netip.Addr) bool { + return cAddr == addr + }) != -1 +} + +func (mm *MDNSManager) processMDNS(pkt []byte, local bool) []byte { + msg := dnsmessage.Message{} + if err := msg.Unpack(pkt); err != nil { + L(mm).Warn("failed to unpack MDNS packet", "err", err) + return pkt + } + + mm.debugMDNS(&msg) + + var dirty bool + + if local { + for _, ans := range msg.Answers { + if mm.fixResource(&ans) { + dirty = true } + } - // TODO proper filtering + for _, add := range msg.Additionals { + if mm.fixResource(&add) { + dirty = true + } + } + } else { + // RFC 6762: + // Multicast DNS responses MUST NOT contain any questions in the + // Question Section. Any questions in the Question Section of a + // received Multicast DNS response MUST be silently ignored. Multicast + // DNS queriers receiving Multicast DNS responses do not care what + // question elicited the response; they care only that the information + // in the response is true and accurate. + // + // f.e. avahi doesn't properly work if the questions section is filled out, so we need to process that. + // + // The likes of Apple's mDNSResponder haven't gotten this above message, so we need to check for this. + + if msg.Response { + if len(msg.Questions) != 0 { + msg.Questions = []dnsmessage.Question{} + dirty = true + } + } + } - //if !frame.src.Addr().IsLoopback() { - // // drop non-loopback, is from LAN - // continue - //} + if dirty { + L(mm).Debug("processMDNS: rewritten request") - L(mm).Log(context.Background(), types.LevelTrace, "spreading local MDNS packet to peers", "len", len(frame.pkt), "from", frame.src.String()) + mm.debugMDNS(&msg) - SendMessage(mm.s.TMan.Inbox(), &msgactor.TManSpreadMDNSPacket{Pkt: frame.pkt}) - case <-mm.s.Ctx.Done(): - mm.Close() - return + ret, err := msg.Pack() + if err != nil { + L(mm).Warn("failed to pack MDNS packet", "err", err) + return pkt } + + return ret } + + return pkt } func (mm *MDNSManager) deadRun() { From ca162562ddfe850432f915fd56c16f4b77dd2574 Mon Sep 17 00:00:00 2001 From: Henk Date: Fri, 7 Mar 2025 11:59:43 +0100 Subject: [PATCH 57/82] Document new performance test -r and modified -b flag, and rewrite + remeasure peerformance test results accordingly --- test_suite/README.md | 42 +++++++++--------- .../ext_wg_x_bitrate_y_bitrate.png | Bin 42077 -> 40702 bytes .../ext_wg_x_bitrate_y_packet_loss.png | Bin 39219 -> 39155 bytes .../usr_wg_x_bitrate_y_bitrate.png | Bin 43733 -> 43190 bytes .../usr_wg_x_bitrate_y_packet_loss.png | Bin 38804 -> 39569 bytes 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/test_suite/README.md b/test_suite/README.md index 8db5b8d..c6ab18e 100644 --- a/test_suite/README.md +++ b/test_suite/README.md @@ -455,12 +455,10 @@ configuring the following parameters: [\[8\]](#ref-man_netem)). - **Performance test baseline**: with this optional parameter, a - ‘baseline’ is added to the performance test results, created by - repeating the performance tests for: - - 1. two peers that use WireGuard; - 2. two peers that use their physical IP addresses in the simulated - network setup. + ‘baseline’ is added to the performance test results. This baseline is + created by repeating the performance tests for two peers that use + WireGuard, and/or two peers that use their physical IP addresses in + the simulated network setup. With this baseline, it is easier to investigate whether any performance deficiencies in eduP2P are truly the result of a problem @@ -474,6 +472,12 @@ configuring the following parameters: amount of values assigned to the independent variable. If the baseline parameter is used, the duration is additionally multiplied by 3. +- **Performance test repetition**: the result of the performance tests + may be affected by external factors such as other processes running on + the same machine. To mitigate these undesirable external influences, + this parameter allows the performance tests to be repeated multiple + times in order to improve the reliability of their results. + To run performance tests manually, [system_tests.sh](system_tests.sh) can be used with the `-f` option to specify a file containing system tests, which may use the above parameters to also execute a performance @@ -486,7 +490,7 @@ The following command runs tests from a file named Suppose `performance_test.txt` contains the following line: - run_system_test -k bitrate -v 100,200 -d 5 TS_PASS_DIRECT router1-router2 : : + run_system_test -k bitrate -v 100,200 -d 5 -b wireguard -r 3 TS_PASS_DIRECT router1-router2 : : Then, running the system tests with the `-f performance_test.txt` option will execute a performance test with the following parameters: @@ -494,6 +498,9 @@ will execute a performance test with the following parameters: - the independent variable to be tested is bitrate; - the values it should take are 100 and 200 Mbps; - the duration of the test for each value is 5 seconds. +- the performance test is executed for two peers using WireGuard, + besides the standard execution for two peers using eduP2P. +- the performance test is executed 3 times. The other parameters in `performance_test.txt` are not relevant to the performance test itself, but are necessary to run the system test in the @@ -1151,7 +1158,7 @@ reproducibility. Command used: - run_system_test -k bitrate -v 800,1600,2400,3200,4000 -d 3 -b TS_PASS_DIRECT router1-router2 : wg0:wg0 + run_system_test -k bitrate -v 800,1600,2400,3200,4000 -d 3 -b both -r 5 TS_PASS_DIRECT router1-router2 : wg0:wg0 With this command, we compare the performance of eduP2P, WireGuard and a direct connection between two peers in the test suite’s network setup. @@ -1162,8 +1169,8 @@ limit, as seen in the graph below: ![](./images/performance_tests/ext_wg_x_bitrate_y_bitrate.png) With the direct connection, the maximum bitrate that can be reached on -my machine is approximately 3600 Mbps, whereas eduP2P and WireGuard both -end at a bitrate of approximately 2800 Mbps. +my machine is approximately 3400 Mbps, whereas eduP2P and WireGuard both +end at a bitrate of approximately 2700 Mbps. As the measured bitrate increases, it becomes clear that there are large differences between the packet loss the three connections suffer, as @@ -1172,9 +1179,10 @@ seen in the following graph: ![](./images/performance_tests/ext_wg_x_bitrate_y_packet_loss.png) The direct connection does not suffer any packet loss, whereas -WireGuard’s packet loss slowly climbs up to approximately 3%, and the -packet loss of eduP2P quickly increases to end at over 50%. +WireGuard’s packet reaches a maximum of approximately 5%, and the packet +loss of eduP2P quickly increases to end at over 70%. +The root cause of eduP2P’s packet loss has yet to be determined. Although the final amount of packet loss in eduP2P seems very alarming, it must be noted that the maximum bitrate used in this performance test is very high, and when eduP2P would be used in the real world it is @@ -1182,12 +1190,6 @@ unlikely that the network bandwidth limits would allow for such a high bitrate. However, the packet loss of eduP2P is also quite sizeable even for lower bitrates, which would be a problem in the real world. -The reason that eduP2P suffers so much packet loss probably has to do -with the fact that it uses Go channels internally to pass packets -between its isolated components. For such high bitrates, these channels -may become full, and consequently packets sent to these channels are -dropped. - It must also be noted that although the direct connection and WireGuard do not suffer much packet loss on my machine, this is not the case on every machine I tried this performance test on. When repeating this test @@ -1209,7 +1211,7 @@ differ on other machines. Command used: - run_system_test -k bitrate -v 800,1600,2400,3200,4000 -d 3 -b TS_PASS_DIRECT router1-router2 : : + run_system_test -k bitrate -v 800,1600,2400,3200,4000 -d 3 -b both -r 5 TS_PASS_DIRECT router1-router2 : : This command repeats the performance test of the previous section, with the only difference being that now both peers use userspace WireGuard @@ -1230,7 +1232,7 @@ further, however: Command used: - run_system_test -k delay -v 0,1,2,3 -d 3 -b TS_PASS_DIRECT router1-router2 : : + run_system_test -k delay -v 0,1,2,3 -d 3 -b both TS_PASS_DIRECT router1-router2 : : ![](./images/performance_tests/x_ow_delay_y_http_latency.png) diff --git a/test_suite/images/performance_tests/ext_wg_x_bitrate_y_bitrate.png b/test_suite/images/performance_tests/ext_wg_x_bitrate_y_bitrate.png index 0c481dbe9b465fe428727abfa0dfadab6581b776..d29f3e093dcec77aa27379e71f566533be9eb67a 100644 GIT binary patch literal 40702 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
-^R6B#d_YiXf4CoSy- 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ÍLeTQ};&gXC*g@90*LSJyPs25~4t;*PP1?3A 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 K_53}y!T$?xe#kNa diff --git a/test_suite/images/performance_tests/usr_wg_x_bitrate_y_bitrate.png b/test_suite/images/performance_tests/usr_wg_x_bitrate_y_bitrate.png index 433eec8908ab7ed8c5019aafe40411a4c1915551..f1e72c3f6060cc6f18ecaf88e29e8c4cd8ba0846 100644 GIT binary patch literal 43190 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

e5d4x6Fe<YTkNZ=d0x_<=O3O?+{- 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 Zx|XaPZ8Y62&B~W}=<|#6&gPm9{|D_VmKFd2 literal 43733 zcmeFZg;!N;_&<110YwoNkP;CQN$E~SLRwl{xo+rNezWEun7fu&?gh@-``z#JJfC`x|4SJ$Y%F3d6bgkc{z6zDg}UU4LZMAw#eo0A z-ZMH2e{tK2ys}lWGPHHjd24`@*0HrVx3V=i(S2ZV@Ycq}%953Vi-C>)fw8TvwG9s= zqs9OF0S2qLMvP>X6n5|;*Q{Tt+MrN4I>;Zi48e306zaQ>xbQPY$1kg64vw9JWOZ8y zot@(kC7b;4bumQ#XcdfnmrM5Rk0cfeHZ2jsPc1Gj4Zo>g@@L#dQGE4D)6Y9Zg<&qW zwSwF2Idso|1`BUH@yp&{=Uzy#so~@-PyDi{L=%wo!0VRKRq}uv$*1d{$X8yD<&RK! z$QRVQ|Nj^NpNo)$G8xDw>WpP~G31VrXwKJZOi;CK4#3Y)q_v!=i9hL@D_R-M>r0a+ zSzTRynW=cXH>iCymKyb)$&K-PQlUY6Z-HK`Q6#VRB#Jg7;4~00{Ftt z-=)fDa=o2>;o~YIN3HZ7k2ZoK*v5ly!K1vayr0j)`-h z$)xwurAwE#Q{3fji546%g{O=xExX`HjQ?bO);g~LfLZRaao^C;@aRubL!NA+!0uwt z^9^>bV&mH(AtAdfgWZ#BniaPGzrv{cvQ=5zBA=M$JLBHD!{vU;mFTvw8X_uiJ-MSM z%Bl0$FaHVm6KO_^{ng>oN{5ddH75+uhu`b}Ew!N6`0;!4+3T>!(l1`TsQdFrF{;$G zpO!)6N8QiQ6xbtoQ_wxbsASX3hYSCFS8AW!3rv!{^z`$>NSRgl@!m=dr`7$ImW%2+ zFNB!!nO@d{jbVda#{V1r>F1VYEDf}p@#ncCCTnOttoA4 z5|)ydW@c#{7M8NCjbdzQYYP~!c3DZyQ7!WM`Sa(EB}SpF{?IR9g38OCa#x&HgMxzI z_D?LF)m;CqGD}LmtQ;Vj*gBpv9cz+9&Nks^{W`O5m&z*%+pSnjAHmvQK4&T z8B$rv%kRAVfQN_p(CRx~QKnpkL@H+h^1u{n@!{DgspM(l(J4z=+3p-nc%;k3^SpZX zYI1V&>Bsy0KU@wheVjc`x5V0_7(aS@qwg*E&umULcDp`FEPXpC!N#)8#Z_D8bp;naN--X=!QcD|Pk0LIX*eFI=CORz3}v@IPT=!_(8#dn}vq zg?wYVKMQK=F8wQ1U9#9mLRP~Y{aGrlBO}D>>gppUX05|TMtccSHJ0X3i&F3sp_G!k zCMM!fpF&;HHX0Mx!8=0zWouQdZ4f88cJ>YCkaEBEa^73kv9y#ae>-;wmC5?YVSSW4 zmct?d?rwd3T`Ha@Vy-=kME`jIuT^cTPNNSVqt?Ca*RL=C$!zq&CSeJe)UK?6#r&?E z6A%+a#B0CmxPH#R;`S#`TktM}Mur*d86B1V+K5PwS_u;;XKQ+4_UEW`(kf^FEH>$TiHGi)S9>1Y zEqL+8Vcg00*$0f<_wRR|9_>KSxpsbjUU54cHrQ4@CfVcey7#F8CXLap0-Pb_<-` zKoJlSRI`w|ACk++$hg8~H#Ik-Sug*g2_fYbg$l5Ca9G%1`%AA?6>ZoV`(~+^lK#mP z$Rmy zMp@g~^tqg?XuEl5X42@j1d-Fw__syS?HsS<6~S&34JJ-iv7c*;7#bSd98kA180og< zv>g3vHkf|(yEXR0Ym-{3qQ^1pR%RQf)b=!sKw>epV z*?wa@UI&Yp7im~72kSTP^AW=PMlx#4lv$1q73g8Y!|mGTh5D>-T~VhkoCbMtRTt+cY?h;hp73tVTJ2CUa_KUpHK*H>&~0DUxN<*H zE1rbj^(RY(v2~Ck;DLxAU!m*KTlAOEzEbI55Su4!4r{# zmru4`{Qc~p3bqZMXCbUz^|Afx&}VY~>KZDy&3ep$)YR0_=;#dUfTY8d6ZjE$ZuBR_ zgoGKWw(Yrg)>*VRq}W;vP1sT^83RIA}|H`U1%YPiQdhPAGE-hlG*@@;4+E4{t7nD5@bt0;zH zYd9k6JPZ$(CNneB*(s*FtMwz93Tkdp+oL}yjH$4>^NM~$n)Oe->RWppwa;Kd%hBfHk0l~q+&~(G@X{s9{I}H70SE68E z)cV|9YO!&T&;b`Svs{lc8_}_;p<$X-42wuqsmFyo`b*euQBx&SRx7Xy|K?hAPmkQz z&Q7~EwyZm2J1i`!?b~x-TN51}9cX-MxML8y6Rse#VX} zkL}`DX#1j!YfEMlGBV0=zi9_*G!i2x13!PhD0kdS(sGtLIpEt!hL@!6-nkuhu67PD zSW&#VXq+nSeBwnak0v3t!RT?61NYw+Wl$ac;Y1#kAVEVznIk0$q{j!9kTC z(~^qa!$ZgB#>VF+P+K8+nt0MI==53vNKf+ex>9OB+%VJnaTq2|WM3^{v(~Pz5Gc=` z&8g>ODo`)^rKN@!=Vuj#ckkY;3UY54IPK2`o=bFRO*Fc<2!%tFah3 z_p=WiVbB?usEs=r8NsSCE(Y^Yi;E+coipN+t}DB z=W1k1{w=qm!XXpr=;7gIWMmxsc`L=DWpj6T7M81`n3UIka%LvE%)hR_{++{w+d<6U zLQ~iv9ubkatt}f&xvpJy0CWIuiA)DN568#`s~;aZbL*^*lWGSFEB6}S`Q$Ut2xRi4Q_Y66Q$4sqZ$B7 z0@N!2hzL&wwy*8l2zy^4^dc(2N=8NtQagZG1iN? zb9N36d9GHV+q^r_K3;BvNk*1jKU1Sf+cd#`rQTq}fzb7i35bi!Itf+_w__ldqAd*HQ?PZOtQ%7efx6S+w zsJg+E6$ilnz!^Tmj}({@8H6 zdXh1rUs64I;Hg$(ip(*!Qgi?01(;xpd0O!>M|NPS&l{&3D^tYEVk|i=!5BiuCYwpGmq_4UIVd`q{K$;T zQqF06dG1t$DN9Gak-QKw%r5wE84aK@N6c+8<+W|x`z(kxFB?c5KJI>EA#gZ(8Nlv0 z&XWCNXtG5{-S@Qs5ts|o-@M25R9N^538y7A?#C6ob#--8?Xc?c0P$GgObY|PX<5mu z?E;J&&13g{J#RK6gSvVl<>SDd@X_|193DCj4$fG{Gs9koVy#+tO{+%~l$73ebx%zP zvSX6>d&N)*NWlOaD&h}&l3q!kSgg~Cae8_h&F@mm@PwI}xp5^zX$+7rX5H{`3{)Dw z!%57Ww{IH&erBoU8UCzSdL5(E`4LIX^$YI$Ew^ z)z<>cf<0cl;6zsa^W+qM2B!4ED4-lPeV5$1z;mg&+3KC_L>iy~AI_xwlSBq!dm<g|PZNVWa;P1lavTN$*0QMF*&J~R}a!0&?mjB@s?XTbC_<&s;F#RWurPtaXs!}1OQ z;wG6cQ=KN0Yu0pPicwk2{=EI{^@LB*4!bd$m8i!>QGPh*T*K=ctGe0@_7(cA9LW z43IC2k>dKMCNC(dBKI@r`L1|L*d!)H`Q)IHG!r1tOe7!0q^DrM-po?&7G%$EIh76@X+SAl zfi}RD-f6@U{1hJE)Dgp)SXq;92lyIvh$86yaCNNo>Wf1K6bfll-#jkP+02JN?JRTy z6=+*H7D!uxo(0vW2U`JQFXiheVNffr8?_fQD+4)108ek-Up74?L+3wrPo)G;E&2HL+BQ2tqFF z2cQ`=G&g%EdYs4n{{7o1#Bd-}!MDV0kOd~ImdhG@c1}*w83n=H^KlyO8a~**^s0qd zNJvPsv$Hd_YX$D#zfZ4O5srZT5>ut)gALhVp_Jtw7Xl{-8?7)kYA?=t{|fj$Wboi1 z3rw2)rT4mUXP232$sMGORM>sM&AoN?^pc@JYZ7ru&Al&%8yu8gVEuu~f z@ebkn`T6u8-(ADO>E3ldKb*lyg{Kq{5fPzlV34YqtD#E7W1I1IZ&|7OZQ$J}@_c-J zc&x0fslYoBM2sN*`9s2J1j)j!RQ^Mz9UW3z`}51X+v}sIBcpBdj!9uCPrucWw zqoW$I%2mA}I8@J=vxN0CGX*438+Cn=Lw>@{%&fV$SKeg2s{HNhP=S=TuTeEY;$2+a zWDts|J-0!hGWsbiC8a2EeyH~kXv5-e1I?rZ`q91VCM9_DQDw3p9_50nhlNarOXr`0Q=L+*8Jczb`p1u!e1zMAlTM2El;Jbh|zzA9PUsUd^6dwy+; zHjLd<=&Bs*6xQd;l`Fr2@$qE}l9NB%cmwUf_H3Udn3$dRR=v`*jl^5t!9gf2a$$iA z>#0lWvWbQOaQIvgKS2`%Wyx~W9=fOquU)_yUqP54ZFX36h02 zR!`>+YQEtECeB;CA}8egvx(51rq|aKCdPge!1vw<2V;NQ$7t)6Uhsx@+}dtxYI^xy zk!EbcQSK4iYX&~24{xub;9Gtwl5$!#N^Dd$$j^}nF#Y?#@CD)j`a<<`bSs_+IXgRk zuxQwX=4~h89A!OobB&b2P{e;Id4Y=d4GjyscJtc2xs=%<#`(%FfvprAlRPU{7)%gr6LxoHi6ITK{B&-{vtSIL{Qo?oit zLquHVtW=PEUa6_gJ2(j6FU-%cgXsh;4CXi@@VYyQ!zKKTyrDG9OoUBKJbOa#b!%Gb zVWJx1h};s9dWVBD>WF?Pk*W(Iw4xZe0f?ItFfR&9OVdEO_44sq=4Yg)_5;cDwTX#; zQ4y4>hBA(!l&_3GXcnF+z!TM|ydGl^Z7u_(xyNfC zr0ss>4X?flI^JR%47y(E_pzK-$T+4D3(<%93GiwsRa0Az<3A;tHUbNDRuJyn;r0E3 zgV85fhl_Sshr@soz;t*PL8}za>yWEO3Nr8SejR;%FA$~?cn!@~kO5gSw6i6TkDT*l zI!&e?Y-*IXl?sb^L(&gGl|kgC)7?H}fsDBNHM@Irbi6^bnck9`ow`)$;d7;nk z%*6)A!YnQX3=J3$Q8q7Q+;|`$kO1o7yS{YUnbp+}WFBV9r*he^62g}PFt$O@U0TjB zDVYOY4Fn!$x+Kg{r_ITCSGHk~2S~ga*qocco_%s~A4PSUjM-|#PP8O9yqlj+1Irx6 zWxMzigpH?w`*$zZ!JCAIh6=%d*qQ*wO#_V!fY%ocY+~!v!#Cem@=yK1K+9U>22rZ?msOC%?ROTAN6!~-@5WAp1ujm`u5RAWBfOtrg?NvYD>}e zPXa-=CT!p7Hz0)n24tN-4{yVKfZsHEqgLrmY>cQOCR+E z1jVi|+>Nk*S!TVxyk47{22@v9YgGgWUI*Q(f<+d?W9ewgX*j|kWN#Knz)L(}@wk68 zs~|omy!e_y;Jn_>;dX56esSgyNv|pdh>_cN@fNi07Qo(cojv6?Mo@iia9`apY1>=L zJj_4YKcT08AtTepA_z(VCZ?~^z_)%=*6ahYH0APhE)E#9%`09ANwn}9(O|h2b9h=% z5YOoAMdDP$MQflOVF3k>7b!s9)ST>6h-1>Z1i)f|Kg8ev8|>7hA&N#x2?^biViTwk zpx0y%EOytxp+eR#{E2!bfYFt-E`EM~ZjW=m3D+Iz+!21avv`2&Vh`zr=h@Y7Ux97#PlpIf|aubXfpQtMMRcOY8Pd zvXvl*Jy9>6Jv*2%nr`wZ+O{w^=K`Suh;2BvoY278HS#99r*e1$Un<&DItj!j`VCcc z)x_2B?q*A?ie*I&RVIC_L-z!_p_rvC%?L~{0P1?B7xU)5p8&PN)~EwGJJskrGc)u2 z0|qwAxGxPGoiIvoNt~Wv(1wb*Ns(>I@UTlSt2=&Iq#>#&a z@}@s|yEQGMR&4wZ>Sk>*E!~R>3UoT6nS+Sgv2v6plSPC5LPF+Yoe=-T zar3odcS0wqE=0ic07Z3Kd)fnNx{8Bi1XKmoiU^=!`EDon@a%0yKVBzb_?;wj9a=vT zzjLwWP6&Vin0}w4ka0~}ZY>zAlhtL-`%s{rg=4;@V^#ixeZOQ?QvyCe?c7p1&u2X# z{X@gUw@FFk$$yATNj+j>x`l)D@lsjLoGPA(4wyp-gF_&km6hz;NXZdQFvK}hFE!VJ zyBn=`i2_L41G^XF1|j000NHA&f7B|1(!qK{aVe4hF;7@(?>c8 zT1QVm=yO;AJ5?X7Ki#JFsP(?}FpnmnVT0~|&XQ&8X+jB&<|vEoN!Zwe zgB&?PEW82KOFqFP_v_@s^SXBwqBmQeN)vFpN=H2h`43Yx4Wxp1&L11xLDeKF)cwnE z9CnQb>JHI*yuWY2KWAp7(@-{M+B?!t+0N3i`9IN)XYj@WdSx-CYb-Re*;K^Ul7Cri z(>d3hx9!_h^dF&pMKIir$2zT2Y^U^G49^9 z*?)dTjLJ|o?MDR7gk7FUn8aH@k7p16yW)gE<*fYK-_nK~6+VySXeKb`?nVD?Bq31e zCM)^+`M=BIzI!{aY0=Jz+wlosI}0s-kg?q7+fV3{IOMCF#{Iv9`a6C$gGzpyDv$4{ zUN{#4DjuQ|p04`;Ls@fgyW%$AwDE3)VvxhBpwTIj~0iQ%MC} z2~|{75W84++T*$#xPG!ry{TKfyG{N5_z|=_wY%UMs96kfUdW_VV^urv>4GC%Y&DUf zS!s{>qhS8|l$UdZqtJPVE|7diby*v(j6T4NHAnL#U^wQ|#SMp>gD9~$n3uS|1;Ugl7(dYC>-+m7w5pwN-`$^J zMqfG`uW{oAai_kcBM4ydEdl~Dumup<4*n8g%9C!p<7 zU9dlH28%YHpPeA_AQ-M+_?$w~E@SAciMhHWzyKRsM5^P~G>kM>^C2dfculeFX5jnI z0M1GQt@MkaM*^%+q4S<8BsH8s*I~_J&AQ!^osyEW*{`BKwX`Ju>J=fNlQivGcffzr zd86>=4WLpeAIgYQrz`oH0xpihaLn$p4@fIZp=+Mc|;{42oWD9{I}z?Giw zjI#le>4qI#Rz7G|pfF#13J(%wdMaw_tDwZ7T7rlg0sJ5mISj5Dz(CL&m-|^E*aYJN z%d@(gAF?y;TXuXzUn1#LLY#Kyw?@qJZj+D%mXwq*Sb>RKY&k~!LVe?h`7jHD20_ce zRtGTTePyLf$X|TmoT|oB56N=27MgOzR3qg`G70-y!f;~48()Z?X^DgV$AZ}MZ{EE5 z!ebWzf`dY)JSAujU{7UQl1{=VrB=zk2h^jh_$Kg3(?1#CTwEbc1;qj&yJ2KR?Z9qx z@|op$Rg8hQz{x5r#H9|W0~vuTx#g~KxgE1vRc&Fw@3%nNgDP>!q<%3B5?EO$7cij# zp{0TSSHT8o^v1i)TS9oEzPa9g?(0+LA8PN|&u6>UqCJm|h??BMe@x-|@B^YcuCI5& z-ti9#`hzY5_&c0dDFB{k0XVZT%0C;Bf;Z``Pzrk-)LfZFfnc?q0lGK=Vij;6bG53! zM=i|GdI2Kn>N@7c=44?Jax&F=NJ%MjaPi{@7Yxmy z=sX|^^x9kPyJ-8DvB6ii4^{+!qNyp{&;PY#r!h1*4t0LK9RTI7W4$tg{`gg*9%0Gry9JkgF1K56BMIcw{O=&J3yjup5tBf*`Pb$$Hm2k z{+3BH1~6t8_EN>uS69ig!X_85h-U9E{8{@+-%d!9iOfR`j%~;Fy`iMnp(rN7S93i_ zFO-|Hm6f310OX>kp`mepCkt0V#9>Ydw_ov^jtWR+Q{_et;!MWyyKsz;kAv4&1WFC; zh-bbxNdc^2BBqSk$NN&zOlTm}X=rHtuvxeX6s(_w+Z?oCs7FJn2SgzU&ufuX7^G_x z;I1)(9s(e*%){LwS+DNZbV!gu+)p1=xr$CYBqkv$S_mqG+~I2Not8yAG~t$Xebuy- zYV_;QPBWSnLy}8jN!0h;tnoZWDM_lsIRYPELy4pzyk1D?5-`q|=4O$oaj4Myg6`S8 z4D(JPw|xQfg&6bT61d$XB{li|6%An2m0tTJFzzA~-H-jj<6*U#mx8K>v`jkKkRU&c zsSJ9eUFLGxr-uv(sB6(6e=eb?K*%QhRl$2%!Lx8EQ8|#dYtHun(mN0eLXT5zYH8VS zA+dU`r`HHPOv-k5XCv*dkl%Y+k>t6*@hV4CFI%KtWld-18nScQ3yRpBXwCwpLku<( z>c3zMN}9y=nlBe$)1$biwEbSf$(cOjX91WHkeLLK*fKDn%*Y5F0~{<6TbVec;$9Qj zZ=c;2s-j&NX<>CO5j=k;ry9vDmvva|hJVaXf6|-oin)I8ry%(SYSW_6YU{}gq#+AW zP8nz)W#0u55#Xmu@SL!6>R!(^RoSCR`+WUpj8>*U{w_FqSc*-gWoxQd7vq?@{Y=LF zNGNY}HK(n(ZASo438p*DLvM|@-y@0s=56lu)S0*2WoW%$()_MxmQbcpW7OEqGT|YR zE+n%xsO~YPv+%7wYewEzaQyT#MMQRWm*n>HzgHL<6Wg*?e@p*&vir}FUmlN=-ZRDx zKAdxujQ5vm{Y~-WfJjQCDE*&NlZ+d`t|BN*?uCUQS;r5-t}QNR_I8&0Af8`N)G>`mIvp=54{ZEV&ddw`+SZon>w09V9-d?TL9-KFk8&aY= z?sYkef}x1|_3hv>lAVwR-U?efT)N03hQ`LO=k`bQxK90umM1j3-^k^Y%hTA5$qehu z%Ava*r?hmgmdW>b&V_3J=7zcW^dLp1@aQ9t5H zbbH!*Bvy!h-o~q1U^29_o1wxZ`_Yj`Pu(YN{bKCJq>R*l^5^Nf(A$V`IgK9`outzi zDVfbf6mC)8RU*0Fuil@}vKhXSlc5f~KU_e|ZMWCg31?<6G_NmJm!P^w@bkrg;89D5 zvr&(zV*7@Q(0wD-mImR@@Uri{IhHgZ$}TzuC-S$qCGhZ;%Lf|!>J_3?hQF~ykkXP= zNq=@h@*&X@snZZ&gRCepqtAQk7yRSV9*_P`=7k2b$N5$#P}P%pRtEE8nQ-Et?Dgh3 z_4+XJK>=a_MtPnp|4lxViJ7?`ToEKn0(~#J-_&jnpdKXhG!ym>Ab|sV7M<*0L?D$G zD)f0n(#2xBraB$Qt{S5yt) z#AK)zJ%;X#=$kQ-N3&tF;ASH|yyC%TAY%=pVSwZ|v$4?)SsUMo2z+;UcW7F@&@n+W z_ys`bjh)>bv_DV+D;zeuATI;M4AW(Q6|Js|VMFFS0n*JFXd=2VD(XZpPKG00%G^x^ zHWt0h{drW@_}vylVDnF_4@6NMeEdCDsgBNibTGbd%Zy%-}ab}RMQ)yZ$QNqgW`A>5=-@FUF z8M+&W8a`p1W|uHRD%vvpP5N-}-gWAe1t1*3FC#V8 zC&;*YFpN1IxiXkfk2|8$(!u4RQ1huYq&x3BnfCpYJJ^?8m+2n_PbQgpTLxYmN_6o zMR|H*&bK79gyAfB+v*lEfdpr_C6^jk&C9Xr{s(AZe=34b13Hj%Yn@c0Ks>qFG$U3@$#^bbk^?j9FsQINfZoUAki*keGrfLMX<@$M2xUza>QJir**+}ipE zfqQTe?!aRM$sd$1P@v|fBA`moU*dUB$c|2BJ&6(@(C431?;8t2uQpX^tKaq>8Fn#Z zOe8%E`Vr3chnD?=+0B8u7{VB)lz~qK-&LyAutu4g0tpz?Kr;mKN7u|O5HvnGsW3Rl z2}If9(Tn3j;4OR0SFx~?z~%?HW*YQ+s77&V>3%w!$vQM>MZu7_#-~;MG$WEwsMmTE z*yb}(rA#0&4ECrIC^*1-#5vvKa6x&~1s06JA<+2|4g(p9Z*ig=?Cb{6oq>V%fy)L8 zeHUO&TMG~|y-O|r_5%3#?=D4lidY zX5aD8^haga6vpC7w_P8k)Y7Dd9O~(MQ`q_Wi527*z>N`>!*f5HBLEe_8;;~CWhtjZ z{u@a-KuBsaK@Y5=9E%ZWZfNU3(5&t33;#9!P~Z zh}rx=2xkEfRcbjFke6o_azc!Y`vIOWq-`tq`zRkjM!Yc)liq_G3Jfw+TJda1)wb!j!1az2%@f?ox~k45F%lrK1}wMj3qJP%B(e6y5#cn!!a zyEl@OKby zuXxojM0V=Kgbb@edn&LqoY+XcHB5nS{8O|HB~y-xQ7Kz_Hlv0vYnIs*EqA$_=fbg< z%CF5rokJe&Q*^W+fO?PfBPx(;QM=3ifh-1XlMs1?3?HGOJ0B#t48Sc%f&agV?%}!q z23(v{16^HR`1|L!ap?c)0I?+WQNh*dw+d2+>SUdklSS->TQN)G0zVI&kFc@dPf+gB zO*yub60=A#e9=kn`t}f)CvwHFvcx6#PB_fowvolofQ;1@caG2LY(gxVa^xRxC zm?e31DzK}Of&p(Hg2Io11a$YgOGCqeGdv3>Bmj`HsB)BbhCe^88GhrfXfcrNo)__? zr7B@OJl|!XI}^6M`H#|f>?(ts>Xj4JhFVflgK1JERRJz?Eu}%vjv3(+xY5l`O(kFg zWhkWI1a~YQb_}S(oPC8$m^UVVyt#}5sY>Yi^S!ndL7Ce4SIPz;?H2JzJ3@j?SWIlC zW_`T+M3(>z?xYv~APWA@yV%|cblDJ!}Wv_5A5+ ztZKfJ&uZ@lKc~VH2uNr3FH2Vo&^&m6^1MUYd$VzOYikM~mY4ULLuD}}VfD<+vf@Pd zx3+XPw9XnM9X7^u%DR|X0rFJLnDo{r%;68|QQUZ1(=Fteed=;@du@T#a3Z3^YUbL@ z(YA1H%s+ZTc~mXSUp&|jzK>qVrNBi!yz}twn2MY&-Vd6|Yq*agAjvBc#n1`aM^!LA zK?!>egM75gDGWM5544>C0>&<6d$6100c-vS9T5cqMGS~FKr3zpGy=+t4ww@+NO=?; zJfZSGsOG;$gI%Oo=ZUuZx9lTC(F&lrK4|*-i;v*wySi45u4i9;G2s_}?nWR4sac(0 z-{LyP?Xaae6lw(K0$9t$o_w(=5_tANB(5QiB#zoZA>f$uys{)MD}pYO^YC8Yw3Z`} zBXRLXk~N(*+H265fCW-=aS=j726%8icuo+kW{0&hfWhGkF=se-AqP(H; zs~^UOH~DiP2nc`p4{13sU^qKE=EIf3EjEvq+q`jbFoLe!ZHF;g1R}=j+L|53=g7#& z!jv(Ok^=#$?=3(*xhV`!K010!DB2$GOCFtR3$x>;q2cyG{d>2lm9uj1JhH8F_y-4rLFXu;=+#>Hue$#c;#O9EodmZpiJsw!Ox2BDrWdG;M(fA? zU23kb>4a&6dG5G_GnzAI@I23Ei#&P&jE znRG9%qV8hSr`atHWCWk9Q)soNDURE!`KABF%ZK*;`En-l6IKU}`i%t-;GAkoZwy!b zT8Q2YdX99JY}xN))Ja*03MY+LKcB@RLPL(ZCXjRSyt|!s#%;&T($3N%N-JQZ67w#Y z55;vp$u;;eZ%Ulq>=(I~tNB z>iJ)zILA{1zfogR><3TQRbM8|3Nn$a@E-q@`(=g->r%`mM1ElToYcc&@z($&`;ZeJ zOxgGAe?{3^Ds7n`ScKT2yjVk&{mr$q)FdDQ1)A(`tR3p?KN+FI~#2r#_&ibr9aC3c%5px_S?b! z_^I2DRkf?Sg1U04wm9!cd*@15Z))oQ_JHTC;*+q8nx7}N@sA&a-0+svH2ihDAK0Q8 zVr2KwCw*-MKP5%D)C8*dN}*i`j=BB*;WSa{@r{Lb5rD0KUQGpt^g97Pf@6v$oo0Q% zbI74TZ@LE`$dU6pH$xf+A^RdD`#bePf2~7W#bZ7m6UCq_WY4;5(ai{LV36)@r_~7H zyRf=Q{dvrooFzmU^w0bWNNK{u?km-tADnS0q=^V{E1!FW+gNZ5pEKD~e!QlNVenTk z%?2;6@cmCnf{Q%hOP%iZ!Ebv@IbQNX49QnYYayBC^rjaa}IC> zO@0r_L1LM$Wh^*O{CdObadU|7d?9*-#5W&F5%(n?EXls&k1r%~C7qR)_6jTMgcCv$ z`$_nl11n$TH@a?X%Goz*N3n;68H!jawH4e~(LaaXlC1w(X2Re0L52`G*}1g!ix{QY zBKNiTYYN_AM5eKJ3@s(Ee1G}9>hHTksi72_!c&j`U5pS7xoRUp97F#3ZgugM91*`>SYLcJBq z3`JrobB5&7n}%ww_Ekz*`sa3vey;!AH9w)}z}pgQTH$RzMTcJED~Ob3Cg)D#HkA2W zAZ=1VeZH2VD*s+kj_to!f9WSFF}G98kl%B0A67?a&f4pTdh%1|{s?h@=ioBhV!h=! zY^JE-`$<>--E|Y)##?I$55!L?JKi(wQfGSNj)&^gZ54O_gNHI>)ibo+O4GwGJ6-*L z3;q4GOLY)^M?%Xt;hYL22O%xc`s>#h2DTGH@UV%RfXo(P7+<05k~rMLLXKaIFg9WXF+(LM$w_Li`;<3~j|yAm1Vf0g=Ov$WR4~yxc?{dC7(|W1k$J z#N{!@cj@WPP2z!j8U7AL7= z!F+^+r7USJF0Y##F=mIH%$-)#e1Vmo(vf1iZNb0_j1JeaONcy$AJyUZBa<#8l>|WZkxxD z%m-54_OcVv7lpvA2b5aDz}-^4xOwA9{F_ziEjGUq%J#ywwN&6nYugrT%K;-<+I4!J zG~n8YxX6VWjtC~X_Kd_^+y?oHcOhE~$EQ$3C1wP0uNZZsqewpLIXeZE)KxBAJaJzq zu^~4aJlqoxCY$l#Hfr?1(5yP@HFH5sI)%ssk6_smrN~KHzXkpL+jSrfup0kOunD*VweBu8+(ErRNsAJ>voi%JVh2;pl z*IKex8fzUp<*hB$E89tVMvI1EMx*ucpsc#{O=M!IpaljMDH05J5a7> z-qX9Ye$e#u(H#pRC!ey6IGHm0J&3SVF;0cyqLE05P)625=Ott*x2# zS)m%St^q9Fs*md!cEfJoy%~AG_O<>!Uy1k^4Vr?zQQuF(>AFVmmspoZ&}ft2sDC}P z0NzUyK+29d&X1s3JfxsN>=Y1p;9S_x|JLWx{g%g5Wv66@YgGZFSzl8|F4LldI^N3> zwZ2Yy4*`O$lds26fI3ex;x2(h#sk4pNO%yIs9}48=LFIFS&$dNa~;3ciAT-i1u+uF z`NPhu0%|j|Lo<)6ZA<#?qykAPnSYRkMBeOJnF^M5Dh~{2qCYz2c#SfKj3k6B6A}`f zmw@D@!hsp@ZE8ZN%Mc*i=`qH?yINU4T0QtO1~W;V8asnE+)ShAA1Le@S)Fr`$8+93 zvZ$k(1UGAC;GBO6LS%p7gA`i#kPHeOLVgVb2}A_olFcV-G@}xuqksPX_MAqmiVIQ$ z$X15K!__+{08|JZt#t-N zhYsI!`oBKC0&6S zl8>`E(BspI&nq}Y_Hz_&=^%`KhpSXq+h#~J3L7S>`N z!pr|GxBtN(vaTX>@Ie5^Y_SX9|3VBLWSE{3GbDnJ#8M#$fj$Y@dnA?$qz#D~ARlD_ zas|Xf(HIy^4!ktL{)TT+@$uaQv$zFv@=(0x)>H3ugW=HqEqwfDkV4*$G7<@a|FxQG zTHG<_5|4gjwKl_e-TE zIIuL3r!A|YaStB*7Xi1}@Nhgh$(Bl|Q2y2c7-i0N)JHNw893>KjGO;$sx;^xH_PAi zN0gMG!*?g%&w-uRsF48@qY!xyKy=6%yu)#FZkF3lZb@lTZlUgvxVFaUl=p@^C`j?B zPmVm`!3T6t$SW}`DnVV`hpgirTwI7t%tJt-2|mXF&JlvISx{CsNVhRPEd+s76e2v< zYdgC;YuW#gtWnp{@bG^r3nbHbyl*AFRIW!6Lasq!Epa0F*eG*HziaOgmMjq z^dR~uq)QdJuB0Xx@AZMg6+C;G`5-qvO`PxXAOk67jco)QtM! z4d+|Ckd}vzP&5AQH<{LvnnAGMAg5Uj1n4}B(S{wC5w+`w^J zgADHjDR~scIKk+>3(ALP3~1C#B?XZ5)Fg#*kAQ_;<0jt-3cs?Kx=!_4cRu89w0c&g$Q@;PWeRDrRXMpm|64Ch=}A#?=7IouWW zPce`xoK6p!!C#q%(?XD+#|1eA<9`$MOy+e4t(F78VNbd&nPJx#ai|RB3^Fw5ik?cO zox#U%Am_7SA`ZcUH%JTU!5<$dX21=8FJ$^zY+CUn?_X;gn!hKS#$X(lNSM3C0P<1R9h{XRy_2i4Izy~T!n}WoWeFXFI@vx0kJSFvx!bayeBvDECwv>qc7)S zckpdAkbT-*Y|f?BzEq;|Cr;BNFakZw)a2b)e}j>z%()9P^|Ppign8wY<`eQlgM1Y7 zxg_@lTwih8#jQiK89tVQt98%I+q(yp=>M{7(C~?21;U{1UEUmXn1BNT!jRd0AtBKK z9~OdmeF^`&i0H29V}3rqp$a=XcmZAb^obvpl|{Bo^4yn!5G6E%BYs1Dt$WyID!SaV zzIaHS&7YxZ;y#wd$cTVK$!%O-*sIKQ3>5*GF1{FZuhaebWS`Xqf3cWXg^N_KjDvt&dT z${Nxj6&JN|o;Qe0Fahw<3h9+<2{R_>O%8d}jzai!1OpbSEdlss zAVWw)E&R}i&3RrlBXo6!;|t|uW_(2~)4;!|jLzrG1^ki{yJq1umcJUbN-~vu88&!R zsI4t9??s=lmYAuc3(zX5Jq0K;Vai*$~iaB;j1Pg{vnsQ7~VXDWO z;KU!E6=!w#^Y>@?c>x-vSI)8<6YYYl>ZZE$E^)t*zbf|!=uC9Y8Wvs1*nlJ_3wC2t z2hJn!5=tNnq-H;ut8MJeJ9Zq~4xBF4+O8Q2o#C)_pOoHd@-a+^BBGeNc+ZfC(--pO zS4U6zr&}@1SKFBi_Z3k_<_UMBu|9-$Z)k*|OiPBdk)u3v8KeGwdJre=61<8@fnIzO zAMjP${_%Q?qg4OAluIQo|4KyLUm9(ZcV@g7WQXYbnlcv07q6H7AGD7OtZYlSrskk` zEK$eu_@Y@Vzx#IUsOjqh5~ra4QkrmmK1{IdcKT>*;VNzMudYYq8|bG5vk^A*Ey(i8 zPg4eEm@ncGTy*f%I-(o%-2ZMTuKqVlArHIFZNkeDeU|4Rs<%;OC?ah@@MS5e1PWzZ z(+9ZEzDhKIKV=D$(tR1{o8-?cE}=LQyHQpx$x@=gp539<`$NcEIR?}7=G?{g(?2XN zs^SoULL8Dv5 z#1b3h)yOe)IPAtGTnm6^Hdz|&;K+8yX*zU|b2C*SSG1uzA*IDxt|yc5`b3U2B>_pJ z!&AA*mizgP*fKGrGKvZUOI>sa�^eW?NdT`zBxN|3h~P;0 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 diff --git a/test_suite/images/performance_tests/usr_wg_x_bitrate_y_packet_loss.png b/test_suite/images/performance_tests/usr_wg_x_bitrate_y_packet_loss.png index 8922c6adc2ea13e91253f03f539cf48db959f826..64dc020aadaa95c43ac5fbbb2eeb5130ec30eec4 100644 GIT binary patch literal 39569 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 E0P9`mkN^Mx literal 38804 zcmdSBg;!Ty)HQmLl)q#Gn8MWjW#yQLdZ5mXc@=~6;aIu#W}8j%*HK|1f+ z&->nQ+;PYK2d?9ZG5pmzd+)W@TyxHKqBPVL$cUMVF$^P9Qk1=oVJCtx3}>8(0RHCM zk6+X9KVdgHeK##fOE*tbm;2Z)Q#U6&M>jibGj@;rF0R&&4p(@Ec&}b!w{mlHauwm@ zv;RMD;B|C)z{hrm%N@Stq?6(uR}7;tMgQXDO5|8$STc^1tdzD_>f)%UzSis*!K&y< z?#0q%QYxxnxO92Asa8dOHVefc>=$zP`}PX@`fV!UkAYr6|GlarorOxC$)A4*m zTf~o2w<(luxQV_fvTx7Ue0cg#OnCRt^rK(i--Xp>gV~ALIpI$TN54CcEd0?fW5l8& zBO^&kaoMS;s1hHVV%+e$AU|mkdYK>`=MlWx(ZG*U!^=#J1d{0ExXqX%ylP?c|N9UB zs1FVap<-bvTo5c5@mOS(<)o^#A9?#^pCL*%<(lm?wdMW$TPtG;@^Q?KEiHJQiA0}2 zf3D%plO{VY#@JV&H{i~sePVlMEJBgH>BTi$qhZHyFRoEW$qEG?`IRm7&JJAa2_y~D z(Qb^1gm-*?a*m{eBpCDb^o*3{{IuDBS0wk5oAuwG?AAJ}zaJlTy-xRB`OSarTI=I8 zDy0siA}KFlF4SB?7f70@h{wjpRnvuY=7#y zT@d`;yZ7(iweR(94;TkpADpJ42^o6-Ak$}a-l{iO^ZD2qT#fV6@U>VriMp=?x9AeM zf^>epbgYXix?9D`$ER+-3vZZMS)q9F-~p?Su7{wYpxeC@eLZav%2nTZX~vC*I#%CHvfb36v5v13dN~Yr{aEm+guoE9vfqO`}S>V zMTOj0o&VbEM6>k5y)rX=xGhr~o9I8^UQVslAM<+L6*vBwB4A!>+Dx#sx7P&wcXV_l ze|{v*zLu|#qNBai*bhn*RRvdgp)pmU$GEmGBh;o zuJ!dS8p)G%a^jbkk-@>>{D&xV_b`6@RpVaZBT-sa)!y(BcWr0QJkFgIf`*=+{)6u} z?{B|7l3|1URD(z9w1RQv!SI%@M%WzXq3|H7`B z-DA^bA|_!&nX4|^7#r34T!Vmx8z3GX_*L!7k*L_4Fw^RaksLj%+x>HDQ=Iq7=kCHg zS5ieii@WzK9Y&)?+~*r$SvTkVn&Au??|y266Jpx=>@qV~V*T-vsNj_=KR$Xo7FBs4 z>>cdo-iu(#+gL;b;ejC@*ZD$CbYVH((%N1ZeLH*P2_^cOjA%>9sF82RLV z<>2tp)Y|&w#fukvON`mZ0*`BJYEACe=ZWzt7tnXeJs_7^rj;*jBY_$4GO zCpXc~76=srPwkZmzPY(Mw#)Y#Ha?N^73}}7D{lY>mwsVLnYHS zOcn>roIE_*rrHzM;0xG=g_&Hc4=PR%_^#9vLiYRmN|Yg(9mDDl)>ZuW{^rb$e!LM* z!4@AHig(HA6QlibxsPt(Dyl zJlJuz8+wO7)fN|`dbYVITjfW!>x0yrzVvW1a32Lmu5bMPIX6i+ zMC9aS*KFx+IQG=DO3Zda_Qw7Wa?7>AN-lBU1s+JfGX~ojZ5te%FeA zPm@THi6GB2-cecqb=jbt$h=jH2*8??7}!?vIF-=A+X zhK4M4$49=YBJRrO5?IiuPhz2=p>K2wba(!|bnNZ=98=@5L`*_LaR;b zKFNH4Q9;0Oiqm3|zT3;DaOTjiL82=&Yb8e6<-zyV3$(IEzmmuNx9`QCzR3(lLlPGI z+MdH;DNbW!W3Fz|uu57QtS9H?%VdyxgP}YI?obKZ?!{*8u+k4AzO=afo-NpArHxD5#pN@}@58G>RZB3Vpp`WL?O-@ce zSmnqTPIfB1w^w^}q80vIXi!0XxVPCHNyUYVU0UD~69)%}kndLE9_1`V+K&=riE{WV z2tPvX;9r-7ko!EX_|eXkp3fzGPA)F=Iy&iG^07gXh9Ol@(b9%O=$b>xTU}jk`1>)WkAxKf*)@G`uvK3%d{do6c{?hx%*2sn*PZjSjY8FfDRn(`0= zQOsw92o{#rM-0da@g0es#27A6SdpFX@)E9FA@c_d-?bf!)J8HmZ3t?V??l# z-TGrtLP;+3>5u(`WOOFw`SX)QLqmWqr6Kw-$-x?T+r$caIk}TqD5MSs8(en3t)U0= zV-ov|S8#&5VHx8BpmH&AQ%9}NBpcgYs`uHj)NN6yk`lG!9pCMQ|e5@&+ zRg`#BFHak;sR;tIZ$#ko8M)~&EOWln9g@KZ^@wvJ4(=#)9$v5FxP{y=2K^PVry=ks4G}e06p8%@zDW6%P*&X*077pWWrr zk8pdi6~#un^#T5k-@e^aR8i656ZKe3!6eOJ$(@9ogd0O!ch!DaoAJ6`3OmtmSDrQx zU4QYtg0eAs^jG7PlbmAyd)nsy{`*^W(=OFM`!h2$)(3I)g(IIoD=~_=<@yZ4p|}K9 z##)Szk7uwS{*B1U$ms19yLl5X0ZzcRy@0h=hT!VzYNv(%%W*S5-`=8uEu~~-y$rv; zI9#EEv0u_Jl^i_>fX`~Q#yzpbb=YQodz-<<#ih5C;gUW(M(MHgyU2*1mNsk6(+^-K z=L*;P^NR4fyOo6+cj1WROHo1s%6A{ruX1?BP9GBnRr;>^?z{V)cvw@OR*uouBP9hd zqCsq9dGr*d+?}Ze2^8HDM?0Av&GmPy9KsBF724xj*EarYZ6?&$OF#j-N?s4eH~%;d z-Uv7;!+oK@nUw{F z{|B$Nj2ip>ttG3$QjXu=v#+GL|4p7dgdldC?>%|L@plVkwX13y0ZFsPSDz}MIoey` z19X9V&8Am_&(qmi>iFo;d3{+RbU9TE~NkoG_L?^pJ_KY#H8(C;MRfG~2$+W6qv zZG;w~L-6q)D84r_I~!5!yG>v7ErKmTKf`?+66@O9nh=~CpCQ1+Z*J#x=cu!7zMDfB!xW zWo2dZDHq}^^Z0D~@=;3W=H|vaBveIR|EO;6N^fm#!7oUdLQBgoCh#$(d63$v=7lUgl(Yf3Bub}8UtdYAf4i^^}^gT&dqpw6g z!wq@cpIyG&+gWKh6bzLhUAM?!{yv1jMIEiEA-q1z`-cR(`WWs%`C`pO{Er^Oa(}L^Omyl5Y9!MVLc@{T*lG?|(<(imuxY z237s`QL%k>^5jWHHMKUV3&cyA_>I%Mhi*&56;|DE9_}4z!j>$Ke$+D;4cPdhkgHQb zU*q=o#Ely_NLfW__qSI9XWCcA?7Kcgk5d+Sd?fA8VEFLpz&17}CPu!LpnT;C-@i%Q zSiok`))@IX*g5Cx6ujNcNPPV1S^Zb|kTa~8^vK)sm5f3o-(0&j7D?P6^?`x0i~=WM zo#p^VChDod0fq{eKjvjLJwA@HA9K&XdBYBAbOP#n1T>RSxSZA|klPn_FJZ*@RM$VvN|e#}UZ##JbsQ5{FvN5bI>1eGZklb-IKHV z`Ic}mI9SMv40Ykj8xu%!szCuY88g9nfi_yf1) zUw0P%9ZlAePA4!T;07IC3oMlLeD8&pmX_SHr!kpJj#$v1-_W~1las;FwOlg%K$)ZV zstLM`imi)Y2ariGt*3r~o)+tnAQWrDx2n%iC@3gEMnTsItclLHr<+?#wo0{Uqc(7ND41ER^75FOn!To1Vqebv!+{qY2>iq+7&GY~4_!mfXE70wxbQeJNT z@gjL`Yb%_cXmx$v=PyZFPR_ZY1NB1u2>72&xu&KjlnB$PW%Ut`ZxGUrkOx@H5q;6! ztpQ76<=pv~tuK#k}|d$gTZUm#{9(;qGG9*f?dT1VPyF z^($#&Vq(~^!|zWg%k&G^f*oybV;~cvjuEvp0CxCo`!Bg;mva4kvXoM;T2niW)x|+r zn@%)^>B9|O?9*L6fDCIRR*&CFJZ!9&ChlMAv1Gs^?#Fn1@Q(sds$1ybCE)bt2nutc z4pM|&-}-oKMl={(LZhnx!HFZ__|Ow=4WRQda+SK`;3cQo@2{loho2?MLSYl|+i}vr zdw1&Fjj`V0_QpmW$PAEqTfdKfuphY&AR8B8PWPZO;K=vV{@J%LKn)3p6DyT{djhIm z)A~&3+TWbC49FhLB?4W6JN|tGDH|IbW(!*z8!Z#f5s9x~7q2yHjf{16{;KmoWj|7> zbNu@C>v+J*wAJBt!A4W7}N&T z8eWb260_j*{Pj&=Y0g9wSg0_0VLT}f^~N7S3cyyG486PG@%gjn_8sVQF5db{Nl*XQ z`E(DgD#{Xo6O?N0xv7=X4e0rd46S<8^-7FB`0WaMK&Z_Hl*bLCVhw#cE;?_=`}N0K ze0+ah39&za{*)Y6SW`#gy%KS60w8&fT%onKHSZj2)w_2mf~2+5^!4@M!0pY?ieI_% zzFQV5*6-5ta=)U#TraNM6GOBCDpQ@>LV*RvR%_n-FO*2^?%lhnmA+ePCk6fFHF5@< z{g2qH!Pslqblj^}-5BtXD42g#IT|xEC4GC~p{J*Zs0je<7khsG?CVTmlZY?SE2--j zLG2>y6=40W6J38QaZ_*1_mPZLIS%Zl=ZtSe1=j`rsU)SKP|(*;X|p%_)!t%O=0jU% z!gl_H^ug{xN^C6YCXuNU!h)uzrjW%X(<^};+g$jMdxYLLj^crGHZ^q4ylehAyIMLk zfBxhL9>bw|l$@ON`kU0tV8E%0C$J!~Ojih2YPV0LLJwD0c(+RJP+r4iXLVv{zMzC3;sHQB?K!p72H;JV z4+eoS6nOBRMKw*F6&ff7AaqTvt)p^t&oBO}rr}bEZ<+r5#MI6%7SfCf#EaEHG3(yO z-w=31p;7H#(s!Ify^_))N=x}!MBIp=-(X&N z0ht^n97Hq%gAoSUnVN>C>_OLQbPg7Br-7<`^x{P{Pz;j1$x~0%#V@SEIRk2J2Nskc zt`mV?)R@bEHGw#7u1a1x}=2gfnR^@l^+{x=Q|N8Bl9AKbT;IHX~1&varLI_OP zr$1vFlg;&Ku6ZsSB4~2?Ze!ND=;vc9CzGvLC0>~0P? z0Wo->a}YpvcU$NugGN+5V1Ma`-)bYFkk7_>sEH@DZ%4hn;W!B#gjQFcqR-n&KC!pa zZ|tK*!F73jjfc4jK%+=c;E> z-wbWJZ2ASd=o6BRH+{-W0}g!}+uIc$eYTJW7OO9zS`w53ev1xD$dN{+rKOY*Q=xFz z#86G7t*mn8$KcSNG;fQg0tk6lG5a=Z)QQN*bkvj5?M29>C_@2!5M$_2Y1S5UcipqI zE9?eAqp_#wo$SxCPdj2n(nsao&@Ti7R|u5j*>d|4T_OLya``lfZ6fGY8DOjM0bnOqidq@czRF&M%MZ`DOUS&7LdDY>UKHNxfQBe`U zNdq0i8wz5V5m8JGXKAu}^G zBX48+&;x5rX!tb>bT1bz$=$-uv^QY2@0tMoiVw|#)Y&H|Az)~rz;QtqS6Th{&v#dz z@@me*3(A`N_J)S3{5~62jiaOY_(4-hef25~5+?}|l^WZC!FV@nnz;Q>P7}eDl$39l|1DLG(F>EQYHBfLXJ?-T(SyOJ zzwiS30PGY>#i5RMyYl8=o}mqb<6CEP|Na?tUx(Xcdg@U+J=P}iY!bY&EW)=-%gU&L zEC|KHB_Qg0ee>sB!O}HbfYs6vNqEo)#qIkJ{nL3o-~!tTePBAY6u}i0?&*Jnf!_$- z{WnVjyaJtt;B(0STs!;wBp{*DL-S~~m!fn-HdsUL0X~h6Ce(vWg@m>QiKE8f%cCiS%;N5We)sSo)5CsVFQ;+ArXT89#|45f1BxMnzEG{no z;Qj9`AO!*t;)YZ?b<<6$#Ank=X=re;0TMIRaX98DAaNq?4K)^F zyxN=@`Tt!L5{F;o{(q{%6LRd?Krm3!(BP`5s018yP*G`~;UGHE)6+9PKTljX$luqq2qr#b=&_-r|!bJ^dOj>9oPlvg$J@VgcB9H`Jph( zxhlcWx|m{@6ac+>txul>cGvwLaS~{WS3Z3BAjy+7Uqj2unPDb?J);4h5GpFP9v^CI zfOK&Q%_XMB!+{SfDk@59Yt#Dq`H_0u*5u^O7h}Xal|Xicj^o$psHvsp+Xa_g7T{`{ z1_v2|#*~EDpdoa-2kQC{73bWHROhQ5{Hxw)IWxY5t{D8^61?&fY%U07(E0YeUjWq} z|77r+1?qej0?D2zK#5Y^)bLLu)W-iUe2-qgCdUvc1b&DRge73RGN7*P9PO`M>o2qX z#=I%gVkQ79jB*$-SqqbBQb9L_^~9=TV^5O*Ms)I6>zHzcjJDjATah>5jxlirkD>EMt9G0rN)2#)g903hxy_ zzt++M{2H!^pM|XKM*i#Ui%ydsC*psrHjGu#JbL=Pz<~zO#H`&wq_BA26f0!^S!pa~ zVff z)uj9kuCrVg?v%cR?j+IKIx-UWkLipcXgP((^{1hph9o3VsHF-$1WB+7syonpl5i0M zkQgBYQO|azwPz_k`7#}F>(;HrCr`92e3g}nq3Ffji%`y%HZn4L-V0~s{y70I!gCHY zgmqI@R~2@nT_^jsJ}s>1#`iJN!wGq#ah@OW48nB}A3ns^6~>S#0o#G(`qiziHrPQN ztgf!^gU=?>MxOb_ILJ0apNB9;=0=B~q)HJ6&B|x<5FZ zTin0T!Okw3S;T(6E+5c8>aPHrLln&oS6mD@SZkL)EWYT367EcjDUOH>&A{ zpvt;vfuDn7@679Xmfm2q>s3aFL10yvjbzFUhw^gaH3 zWr`bPWnqD{O(I3vI~I6wfX{)Sb^8L;u{#!*2TF_;K6)%sGBZc>)gRopWh>J^=y9i{ z(j24p=pGPu>#({NW;(XpJI8kGx#yxM~{sj7T7{hog(Hfcvdw9 zgJlH9j^oT3d|2FI$e`jo;WX7!Z z85U|+@$=VjX~}$or_VW@8MpTNE5BVA=IHB8zZ!1pokO0I%h#^a z0jYc8!i6yTDsa3|(b3_9gqo7#^gAOzzUu~+jt5Ut&?)uX&X)vFD{#?JQoYii*A!P5 zy;8pEF8ini_k``>-*4x~I)B%b;VU4g6{)teCGSz)lhqZAuP41;6rb4QZB#Q8Vf>!?vud3H(4!{hqdTQ5QZ^~t1buyb?cA^XmgCr`-p&v0_` z1O`6}H1 zBprg$3wlosLkkO7xy*e*7l06INg)7Q0HC_*2trkOuJZXy>*o>mf9K5Wl7d3ZdlJ_1 zy@oQD>h%_VhF%st@O$$m^9w-(LkaC6^sM7Roj_X*jQ%(XUHv>F}%zpghaO^x&ne7Ivy%g@V84vLdpu`SdmXuLA^wxKWj@bP1^$|#uc2EqAc zVaSY1XIY1r;rS5FI-CBt^A`+$uU3Xs7k+8Sc%+j8{{TBg2Gk9(YHD+9dk=PzZbFQhspB}0{awEVOd*gK2g3QDS za&inD3h7xLT{_4n;1Z*vzig1Ta62-Wmfb@CLLtRHskJL&e(k90e3kd;0Y0)y+-+zamM>XTZPIVz#ET ze=;}-+IDlO13S}C)K^D6#~Sa{lvl5`vw1$t2 zti8zetNzjxw_e?Gt9N$2V){H=ZFq7$m)35F*WR8)^&P%DEAN{QjT6DJ0Br?v+=e^Q z8UP$19An$oAxR?1@PD8s0S3vQ00g(e_YWw548jGeY;Ml#JT0;6YcVERv5@L&;S(oL zTwQ(u$OTa<2o3;H^6az;Ov2nY-nx82YtkEW=T;t{IX19M167l z_K8D{*Pxk;j&jf3AGH|NgeoE-yNl(0`c?msIE925 zK~E;4pcwG+V#S~z5CBgn@{j;Ih5#?HUrtkPWOlzlF+upFZ4&WZjzvhOy`2!)bY#^- zrlX<-zdjjeP{|QfJPz;z{5dG0g%DD-f$*32!x4TPnQb&O<;eyI2Ti^_y#P))AyCFq z;QFn9J_o8XC&1ASdm%w#lFj(;ZVrq=@WTX^qOtveMokcGn0-= z3#MoQd;vW)dJ0BHrva-%)ASIsGV=5RcEf^z=7ViT?}2{yVtM?pUxt_N)zFfix{im* zDkz{GjEs-h?P`Y&u~;0dXM@{ngO1!8Hc>j_33y%-fHuOxfP+{A3WZuCezw^f8r9Wi z3W+z+kU#)L0C7>X1b7^Y?>N(GD+rsTQfvim9%cffos+BU#kE7~MAMG1pPT=RcJ)3H zkWdTwO!b1ApE_oI#X7`f?4ZQ(pZmM%YCmlUMlA0Us6!SO7UOUsipt8;MFth)5e`uE8NlhykAYzmZ7K8#)*D*qX-nU~zxeJMaF!-u!-2C; z_~-kpedPqy#-v=crRO!Si_Oa7&eh&cYs<{c)C985y&l{g`Nx6V%lA-j2GCmjwZFe| z@E=Q5?tCZPOs7U#r{{H^vffNTHC@3?+RK;y7fo9=#q-%ogR~XF4>|GYPdGYJ7cQKX zl9IwO=p!JDBe4u|W`JV!rbWQic}=|fZBC91XW}?q3p7y7{v`5K&`W|zDDTG~5EDV( z1ZmOc5(9#(tDnDp(>a#l;=%=%i0I_WMo7PCIUr&Iim?O5H{}78f2%z;r=Z4jVQ-?3 z`=srhG)`9qPbWN9zVYRpX2SYR;EaJiL;QN813xz3^z>s%;N8C}O<(3*F=fyDcOC}E zh(3P&m>(44&2=#UnSl2Q+)|gs4_2G0OG=!l+F*?%6B3XUu43!&KnWX|UK5ae762UI zy)-~`uf@DGj_v_d0XJziQh6C02YlzXwa8SrH-B-nsv=b)ux?PK7HqIUTCc7;d3kwZ z82lzOl(feIFE@hUlvgL0kGz`{K^P>mfNhW)SItEf=fW!@J1@6)M-K*STIJ=l2IvS zzG@W&okj)-r|3KYuN(tTE-rvsYHDhTnT0f$A>c9yAQ#0g+*}wk1%pVqsndatPfXzC zX-BulFd#cf1EAs6Rt96*yE&jEql$Pz>uoNd(Q^TF`7*6RO3j(1q)get+MIdKx`?Q3 zwi(+=(>OTiG9TRy3x&QY_(b45Dz|}^f}jN&j}Yvqo9hTqWoPH*#CX%8I0PM%-@1oe zEZ~3LWbnm{rkUd9cL$Q7h`Xby2F)ZV0g;o&OCO`& zYetpSlKYZe7(O_)GrTj{JgjOEPDlNh0u4Rn_b#{Om^L0M9=LNYCpliE#)%s}{%BV9 zx*#>v&w>&SLge0#R{GxLN$6<0->RTd+cXpF@#i*PpNk~)0mC~&4Q=S1UjOq>r7s&zBe$7{_J$$)ec{?F$xvrcF2V_Vqm$d3$KRTeqsJ9+}#G zd@!`0J(xGkT{dU;LHTq)KLCq@(%wP;7iZd-UB4eOxmpn z{-QV}TMEytcqPh+=7w6xXhiy@ui5@2&W+DqpStl&ivvnnxFR=LH-pQ{1dwZ_%v|9B zxM+>MBhiUHn07#(i0Xbp(3P)3Vm4d}iQH&UQuxeRI#2rhc-Jnf$+%raZBU|)tj@cH zqnYi;Mc82LKCt3~T*VA*fU~pnV11y3_|cBJ-AE-Ffa`~eiBtfnjB8NMVm$S);BjQG z*EV%e*=ot%LElZKsJ$B{4l&0IlO%!aBQC|Uxi*vVm1mxOM&@yl+0vDsoJ9z$6r@yy zJHWP#^C*zW3xF3E1RfD^P#`-vcm=^cbrO6h7^YugLx)U4VENzK-8F@dlX-h{?g!V# z!T<$`RmcPmxCTKyz-UO%1>)_UkL6NR@AG8?1Ui);`jlAr1`q{%q&Ila?J* zU+_diF0IfB#WUc_6TNCTNC`Ys1N7V#ThFiCGt=`LMuOS6KxQ2LjB8+N%YbYPgE$S~ zT?P3f9#A*LC0ON%0G2g_5`^v!R88sZ+v)0Q;vYPgn9FNupzNN8W>Wk3Xy`#2@)6A62keRFBcAaYqr0&C^??edVQ5V4?Egu88z{V^ z^34Rmr-*lcgOqAd}ZOM!grVM+@i#bl$8tIa~Lkq z+dF1rVnUM$C`nUs$>%1|f`+fK4i5KFwDiqvR64i3mFX&t{X*p;9Hx|%J<1NhWmA(n z-oxb@?hOiyb<~l)9~Y=2V&Y&&u5d*G3)0RI)=Sdo)~&juR75=5uWMQ05SW;km{;?y zH`lK?d>xAyBDF{=KwbrBAqJKx6`>2B-c1qG}R3o&XtZ(>_1$yu}R02rz(ouV&!osMb0Jzs2go9hYW!?P-2gsi=&>)L4gKp>1Drdjb zcBe^n2p#ng-`#hd>}j4h+mDu&Opx#6PnWsuHTXO!&{>us9_rKW^Rz(o^raA3{{H0c zwNO+Ac>N7^$pbk0$iDO=3 z21)G_BUY#iAKd;@10@ZL_Q~h5nI~lT7JXES=@%1PpsPPKKh{5hqa}Ty(O$8tNx{j( zMjDjW2OZPIzdF|5=if0Vvm^Fc=0^c*X< z@w-=B8A%Vu3Ulh&0{o0(ezyJCeM_eE#ny~5 z3sur{o>Lf*@n}W|Dw9F&;_gZ*9Ucx66ONCzC6HABTU{urZ-O~8U}vQPDXX|2gVhGQ zX+X$?7k4=jDe-^@=M)z|4KK{E2%XKhqrT#@I+ZA@ANU8yBGGT(m76Ru#)neA0-Jv*BrOcf$jCq{2sx`rt|M1+Clw_n zGV)!G68#k&mpXv=C^(lynYc-eWZAjIImu%1*0Q|0V<6f0j550#3)PS zJm?F(^*L|f)|C^$kRk&E12id|H*QotO^Dn`0$`wUl-u+9vzF<)a2yVJ#a75d7pO#at%iKG><_uDrMn0$I0Uh`4=PvxBWG4@!x)V zgRAH0ar|FaPYpgbQ4M^5y_rpuPV2q`28O37WF5E0#x&$K=2-U4XX_vxi4YoTw8wVxT_0QLb2?+^4%#6@VLQ0jXYNeq{dMLW~ zcju2J_m=TMfYHAgpnhA?88)YQIVnn`nXP*t*8~h@rLMDT$Ug+sgD@B#O09lyk}~p3 zjy?d!2b|UoFf)Sk9@GX?p!u_31pg1Ry!73>9(Q3`i}Blnu4NlLtcAkvY|)=4XV?FgUTe-$tLQFzc(KKd zKthnk@3^>$3*c@z=S796zMRa`#ld@7mJAZfX|-XrQ^Wgj9IsF)#^o-in>-gG(0jj6-h;!YkH;t7G|&Wy51jFnC`q*vvgI(~q%4w$$b@ zwQeOVw=fPi(PWPJG=C4i!bC|09fD@I%iedbuVaBU$!yuQBYNB?ep?%o(TMg3zl^(s zudN`Pgpb~J#%al!_|HmxRK26bWzA#q3RVH(7KVWD;oiZbRJAPdmc-K^kFVm69neKd z6pLNZJbwH7`c6G>fR*lOC2VWP7nGwFDOz*xPen9WmF+IdOedKL)*GnNr!Y@OY*nxd zC$mw}#h^DOWLeIdwXXT@>{`&ib4hC5DzUVWKFMl3@v5zleZnw6R$Ce^gAY3(BX?7Z z(&<4~?gxM9N51w`OMjLCr%k0%uby$p=D zg3~M%B)<%xvTAsfB6Ag^#m-z0Q!rmk6!m(RsAU&eohKkpyd^((s?XHyD^%g!$vH6$ z8EudiX96@3u%%9bBko~A!9}2U5>rwlL35b^!-+g-ZD3Ui0>YpfEJXwNX{ho*K=d#U zh=t~?DKo1O_J}b?AS;_e+-i!fnrU{ODN$A$r*vPT>rt_0@Zty)&uzd%cj z8YMt)fb@&r%p39kAHC{-Lr}XBPYUD#JaooynE#Ak;-iY`{+K5U%kl6@k`mhf;nl;t z6empXx3NZcINkGl^j~ta(>9CR^DJ5kj(TO&IMRrcQ^q8EBRLMknHx74P|mxZ7YY=o zh2VETb=ewEQC>0e=MMZoTj!+l4@$K*x;ypX1FV*&G>%%SsA@TDiGo1*H(LwTVuPXb z(`cX&MvgHP$c;Nt?LnE#&LocN&^U8{+T?GUsQ#%fOxPRhsb19;NI7+L24nbU07oP48}(S zjhB}Y3<3c~6Ata`U--KGN!Zha!$fH29LoH&<;_9~d?qu;Uz7?Y>q@$V9YA5_?Zay5 zY>=D_lLw}2lda&U>vLC0QiN5)zmP+3|u;zEvIB;j07HQ9^FdtQyv_x zD&xks)j7`ntK08dw7-+F*{n-!zekjQD1I&Tn+H#9D5He3Ni=mDw;gX*6UkEI zZHff}GUK!C0dFQWA<_)lRGM^2PjZ^tUvibM7!>w|c|b_4!U`$OMY<&lpSy zI*3oZe_o=wEi^|7`$Ru)Zq~%7b}r*x;;Vv|IWh+49m`Ms9(jq3odnmVL?v>Fi(?~m z&E{G;OfY1vKqms?w;2q312a#FA|6HYq!qH9kuMfafWp!@>;O>wi`1=MItN)D#E@V}Fi@fyZ{Rt7`j?;(4|cH#&oQ4j-p00TmEWAzfKS4va_ zXw=IU0;5)KK!6Cq7VFh8X2BxEl*OYxPp-`4)5~DyEro$#J22jZ9uLM!DX_`F5J}!U zUnkit*RCEqujASGAJ({aY(ExN(=J5dzlkp}eN70+LdlW$1P<~#z~C&S-7t11prCnQUilFyps7{*)zT{k<6j~pH)NpOfGK0m<9d)zz&jcQ zxEA?)Q(n~lPX5pZlQ!UCLDQPh9p9Y-hX>(gTn2AmY%bK2Q&Q>R zi_{Z;$N+&o6JX>A*I9DF#b93w0V{r-IBZlj;$iS{vkP))XSEHd51r@a z&mX?t`iT==BaJL~kQ6&c)kYO*All8 zpeYf-4Ur(Ls!A#jGs<9JLEIi3G~~Z-iJ~#JwIzXPI>fSy(!&;h!a{SYYqo^&aRbQ} zTr7+Gw|x2vOC78*yRX?OUnBlOl^-|1C?f<2bg7K`cq|ei*MWdmX_cKklPbnx$r+9-JC~H0fCMLgXcw{JwZi& z9QFXFsp=eJxHxN92R8L%hZrBSWGmbH0)4zw7HE#hK$NLx()DmwVH8LjI3{?^iVPY! zG;=<}?t?ao1`=5H6l7~Ex4+Q%eG)$5&9jul>uEn8Ea%VMsGp%ncmpCJ-sm3QIw$Cb zNXY&9lji#M>ovSNl&@u#wVgfW#hS9lcrzmxBVNZ9m^OYSGiiZb+B&{p_4*Yqy6yvb zYyh_2Yz&Ct#Z~JQKo_uxdZ<>lQYC&r@L1+JTgQE{Y5ynwe@msdG)&-E!)O!QD{OUt zrJjq2hZtEEAl>nsw~_vLxuv{|;%^=5gEQiF5M%$#b6$p%UdPT9dYlv^{05j`&aaQz zh9^<9z*90JK;|@sCsIJY$&W7p;8;1o;=ALvy~#PEP`=f)ShXPi6)t&0d4mTN5f+|+ z5&$9|;@Dv#jtGM(=up|Fi+hozpC4y>@;_LS01nssE=}5}JEu6QV26Gb^b`~XMUXND zLwRY2cm!JxJTVBvLL(wjVQcTB(v+jb!x>|(iDM&YxVgKZ6<@($S*wn(D<`X6F{gv2 zg2)Q2WJCl7+ax?^06ZtS$S(#{3b)XQy=m}pjF;-8X4+TihGw7KIOKm9=lS1MU4X#PRtz2z-5gaUZv z`Vj!od&zRLGm6>N{N@7{oXu&9cwhZ<#NoCynSK;t@US2sx4CW{;0zIs2}?r=$pSqQ z0^+FG-CI_h{Qsa8tD+?{s?~2F$$m&QWTNtxU$Ze!=>$QBp4$LVAo1z zjyTrGfzOy#TUK`F%sKV2vQu2qq=9YJRL?dXn_v^Q$zc;~KuCtF8TO0~?48TY%Obgv zkzZsyimBNO>w^vl%quEY;#%~N>Lf%4T#HeTCk-*Wg?k0`Lu6%5=DpW+%JCBph8}eR z&X*8qP>j$v-0AqDX*ON+Z)bl?t7z$Ln^_@qFKLnA;>8bwoSe-l%79=CegFOnQYyv* z_Q@oVydRPa2KgP1-3OH-yl3SuOAPNA4` z$s4L>e7DEKE2h{p>;SO%Mf$gi6>BY6ki_%}*vvSLTC?wI>hln(`qb7Wg$o8FGjH4U zZxjkMf4HNEf`CAI7N@(d?QM(kpVuk5how|he@h#ULIKN`_yJ56@^9hai zK7NTX8mhm+-*Lym01Qu?qDp$QQ*KA9&lEqh_Say9_0s)8Au&u`?b-iansb?hA&PaMMFuEa|n*(GH3vn6Ky=_D?&-md>?3NGmi7Gc%`IdSB!pZqkSn~ z9`v#34#H-#81&akW&7$^x&vpJTCyx`?Mt9wqVMP%E=S*yaROR(ZA(xX(~rM6wd7nV zuN-hIZva%`R4$l@c5TiZz2hw0QS?J7x|g^VY(J*GKhsY^5mXyi_}_Oh{O$9^p@2ze zYisMUXAdka(35G;1N4Rj>X93tXAW4&X5wNYj8CVhRhJ&c!{re}4kN zdY=?TX=>ygH@uzT^YdzhkyF0<=7+1;V2a9rTh9}1#BxII)~zshB8YPMC4Pb3l>>Ij zmuUPRX1zoTA4Qf6pGu14DCpwkQpl^Qtxmoi!1RlhUa;0HkApKK@Na9uyNU|*;3oJ7 zS{j|)d!V_2b|8`)8yW2amN`;m-_v)hhntNHUToLT6~gE7+5 z;$eu8kBCr+sy~$y$@ANIz+Av*N^Y75zi0^vcvI)&wnt0?2XK%s*47ZUKoLOCQUTBU z8a#<*9EJh0AQB0UzrqU5fPzb$-eDD5vN2kf+-cb#RrFm(9^pBSV)7-%lgZ? z{CAb9F>NjXQ?2U0{aS-u#X3QYkE`$RcR%<2`n{g#kEd7v;2P&R&d>1~ z@6kU{!U-{{mW||E-#wZZsd+9NOjZ}JiPeRX6EEubu{}@1m0Bg-@p(S zqzwZ72CIxDa>{X>1Y8V+h}~fuR0TCC?!9m`wcXGXzigNaMW1(8gCy;K|6ZzXBOv}e z42;e1pw=Gzyn4nnW!Ty8%!}JD4%^id*dGBPT7ShhlKTGbn?p^6kiqut)c}r=^bcAO zp8K5qnEdjGz8gL3idcVSEta17Q9RZd zr2sn)JsSdFQt{{%K`wP6JX{B9=EXK|lpdgbBx4W`>dRR)VnBGhgzHBGCIo_1+CaS| zav1gW^kArL{ngh=SqMOHm4FNB?El$z1A1(u{ndwT1*F@bOidO6USNlvNFTT{5Lbm8 z0Qw<3e$2{aPHrLK6U@-OezwSt?!=f}36!(Zd5mdDpM2B}vPwLnqUaG+Dxco08xss7 z!PVPskH7!*&lr&6QOr8z&xsaSq7QJ$lT8o3De%m9lDmPCVl~-cZ`=?hC^i7ss(~yr zdLjmRKDfm@9piviMBkvbA@qMjr8Z*+Q1;)hJGJ8hucA-KV%LfZgCnI@On!fCb^f~c zSYZ1>DuSG#7l{`br0~C<2qTQ%Nxwo z6-NpiCT9QBionxdwQr&IzZkkxN^6CSujQv=pZ+C#3*95phu9B!9AC9px{&t5_!KmU zlj;)Yn>L=3kN9+qhb8mKJhKRz5*lM3XD^rNB?uVlnhA~2YfU#-*F5GYSfexG_5j%Fw#&sg|^Ic8D2KRFup#IS|7P3ARytF1zHn&Ld_M`kyE!C_? zLRGi&aWBmnz~T7xzTF&z)p2gugH?IfskzS27=syVEyhq>oN1rj8bUatgIxj7ytn0gu zVMT0%*N|;@R$>W?QV`jEOi(i{Nr;auU-_C}Xij9c(|hIg1!S){gPZ>z(uSnhNYo`1 zO7RW?!pExH7aGH3uCDZAPj7Ayr+e-FBW@>RZrV4bX|^8Adqj3?gEl-5YapKj03K+M z=P>-msKo?QT!_=%m^e2hn~bR;mAI`~NE#{iKe-uaJDEv5`L!`;hwgz3fBQr<|A{t! zLC=CPxv-BkgHVsfuaAbQxv=TZwTkxtNhij0*4Kt0S;$w-pnttx?>Eb*=1XSeg<965 zrfv#!+R*cce{aW;O?&OX3{xBS#?ku;Q|E@dd{r4(DIg&T;xs8a8C806GnhXcM9uY5 z=Ko*P4JrR*OPZpT6a(m#%K(l=Elz_H08tJ?{tSHV0ZK-`>g`s!lk>5jm{7R5W!V{D z@_80dW^?56!fuQ#5Hyq^bb2I#5`aJ^;RuLrK1qQeY|Er=c0Rm$TW>|*_UP`ci=pdO zBUnk^BDD_N49Q!8MuQ#{+DDi_eg^Yw0mqWh=(v+h>$`lWbM2D~d6B~(PVMcudHO?7 zn*;5!vJyQ3Hc0qsPN}G>hFIOQyONwNjkBhGwG*8|xoC4;Kjp-Z&VaC{w}+ymcTe>5 z(eCC)ZF$Fan2RIM_V!OL3|=&|I)#MSyGLM32}&>v0G&5BdM_jDe@F@vQLvY+i8AmI zDHDn^?N^XkzB4tyECVI3a4$DlK=el{<_sb~rm6y%Oj1E$@$jO^Bt7hC@KqMCwCF9` zBsEK~a;;C4*9{BoYGZ5Y@~B6~udCOyIViRA)cLfeSua`UFYE2J?-))exD|$EP86Kb z5j6VSmn+H>ZA%hoQHC{(#Kog9p`?Nm62#HeI%driJvdM_!FsjOR-fkscUgQ9sbM8} z!sQEaC)1Dpb-5t{H9kV>25mV6utTUg5!47ISab~y!!jGBTjE|FN>=(Q+nr_SeZub* zIVRZ@OPVJFKk4qGBYp-=9xyGiYt2;x46X#>6r0$sDSQUEy&B22ljv17z9K%)Hs8$H+(ya-M9Cyl+>q5pey|Tv}Is(&wqFk12mG*HLra0 zP^WT!x@%q${O;=J&pp z{ET|=3+|f93Vt-}C<2q#S$37M!jp%P3iy~NF>^`o-h=Y>&)BExFuw7yUwf;audXadxA*6yru{n0)-W%y$SO(#am29YAx~v}NPHF$as^e+o8BZ4@r_L%7Em^-iUD0QH@vou@E3$>H`t@ni zkjim-KQAqPKPA(WXr0Q=S72QwZh6PqZ9Q-Juzy!gZ+m4by=q9uXwqOS|0%`|^Aw7%R;#n-iN?+UwTpyk|Dbt-9VR=75n0!DM_Ju(3O3Q9iAP>{ zyZ)y@d>E>-733ItqR)BpHKRSW1Z#!_6l`??VfXX*r%`0)q$_o%qM~9=y|nyVz}Wag`_n&SZ0BT2Yi?0eY2=EQh;Hj2evt#5x^mQ4Ei_;&aB%wlf_pqpsv zO0Zg$9$!s>wIFWZMd%|BK9OT3+>3v$?K4)ZYQVlS^b@~jaK*6aa0vsWW$>4SuJP}V zt9i>I3N`;L^xmTgqM#2OU-kzV#o1^u8HumM|r#(5IbIkFx`w47MaQqcx~ z>-(b(Q`7e3E~5zf9y&?S#=8Qe)cvP`?PtMJ2`2~rS1Y{`Qp^XGKdfxi3i;x$y=f|A z!?_8S;CIKxw{o}&icwovUZtO5uZFJ=*JoUdrgHz&1P@R4Ze;5W6f_|Pk=&T|jXpG! z$4Q|mdj^I&$`6Y|J$-#Cz-G|+PAwN#-u^teqJ53U`^`D@mH+b=iZV97WtDg}q)8Ol zP7i%d!!g&R;kHWSOUBX+_J^MG9FUS~*;2NNX$B37Zj|BRjl8iM{PG3pW3eYKB|Trh5REUW zt}@~IStzW6m#`2dGFACMcF}WdwL_Be1=Y`lkA$d(#sCCCnlvf5aVFSuBBLu9AYqr~ zH~0xX)tQ##fc>u8lfwjX;HlJ6-Yr>p>!Xr|7TollVD?@?U_4}St(2CJAp4yB$}-X%*Fd++CuN3 z4_QE+kj{?lZIZ-F5SZfnj0UR3)|hC?mD?uvt-VG+AMP^WT1ayWlSVgH1dZuXF+N6IZ&f)df?D_8@)9?*|uwD)bPB_1C;-?VXH zj}}`k6p|0oKswU*w@>tuX|ld>1q3d703;`2BzF$8+9Pe z0f@HrTz9}Zj3aQ4{>ge~VcrxAm&gUXfJwbJCzHiXMpcsE4;Nl1ho64IkcWC!Rb>xZ z)qdr)Fv4djUs*+NK%IT>TeZEIFRlv^V5053v-v6V7$PjzmnNTHPk+UtpKnX!t=F_o z*sNOe4D!kZOa;tHN^&~3T#}^fq*@NSVubL{hwXp_{VkV&zTFsvy{8&mQENp4q^3d7 zA|eLItQMQElVuA$5~AXWw3FOwM8TblNg^-UTeQ5+GXsI1)wSvsJSr)MJZv%M*4X?i+5%*e8v(a`qo5g9Ecn8>|&a%gjxyg zmfn)hE#BmE>B^G22=UV6$p|x5+1g6lu~ZO3w>UU~El!71e$X$L;f^eZyXc=oQHw@l zUnF}pC4upe@9}KqpV2*4c8+9?OD#k@rz8JJC{XkiMM5Nk?Ojl}d*9sWhJLIl*27eq zO!tuGe_p+g`>tW?S^SSSoqPU*^r;YZyAdq$Bmu(NSEOxxYpX%wpvEgrvJuJzlPhL; z35DPiYUR>F&5~y%Xk>-s7spuzA?7vhL4;xu{Z9hOOWC7O0Zw4brIphDPsuCIHsJt) zyuAd3E%3cKVg_vP)a5Hz&H^G)-cmvc{ScLdvJo%Z!-(}P==rRhI&kQaBbs&SSeye< z3eheYGArl-nT`FsJmi9d==2!8UVkoi{ z&lqX}E6SCVn_n>!s|&Fk5m8YuXvLG36GQ;eKdVF;Faiw(28zHr2&5DmA>hBRIDUI` z=-r;pC$9(Qdw6v>hLP(UMldH!X~!Urjtqr;wFc;|zyz44`v~}5y^xUsKHR?^B>_OU z`p?YFfK7G=hOHdg@g{4&W?3pBVmf^WUo^gnlksQ+Q zbQ$nbgz||8)ild?3#IwL@c>hp^&uOw==a6#U@5lFobx(yB5)u+?Ew91*WZcGmNDhct2M{+HsuQhz7-^L{TDX9<9x+Bu2WgkC$5CZgyXxa!afClTi+}zxE zJ}(X^*B!`d%M3Qm#70|pn_R|TpP#6|81T>$jWwi~c4{*qKSLeP9%<2rqhDFkT~TSD zh4}(#gg_HKjQxKkE$In{_wj(VIna+X%Sb_=!qwiJ6hPABi)|axJtr&_WPKr!jGB6) zUTn>UBn+%I&ZVlHINI~LQRsjw9{*(pmDTIL?Dk+EkVpO7PKA4Cc*38PF*Wk)k({GM zgR#daOjjzS0SRg(;saw{0;@0dG~eeP=7ZQBI%AI^7$C(>tGEyw9h(8ZTa(9gp=uIk zOgED0B~v8?xg608!>1DMIYpxOer)wrgqq0*2eLneRbf$WWbu4)MV91@U@V6rEQ@0H zN9+)|L^ce!@MxtA({HT&b=wH4ycfq|m3wd@0o*}~oOlJKmhcum8RpadN#CsXzTk`T zLW5OHz<+`QJlsxfVs*Zy;1*n_hg56i3uY_$$ypHTR3f^AA?^ekqG;b{#3l!tZE9{* z4qq79|8GGB+iqw4)4WyO0e8#u#EDuk53?cn`@W+Cem3&U@!IASdV(Y)h`!YOd}9#g zR1gH6@kmC$DWodV5tVZgNWFjI7@)58BJ`sRaHLX{&0QzFuab?2=cgc}L`)oSaD@YnehizSFy++a zAVpql>hzQ{NF7sD;`6tp?;7SyKmeAfzb=NGQKC$hen|BfX_sv|=x_M;oHEFIgo_+R zF0w(E64kO&4jC5xxKi41T-Ef;na&J_6dwA!)6dZ^hdrl}Fm5y#|oy zm=!Y_;{V=ku1E=QpkLf)&nV|gGUn_ur2nk5+llcrqCgXBO(KMaAs&ie;Q?SBfrj+- z@;dY2fgA=gWDwSW&q@?2U(^-)>2MkYW`v$2!O}rFAt~P<$ugntkZ4bU1dtv?GrjKh z>&(xFE5+#d!C`HS5A7Sut~I2g{7zMC5kaoc3ratbH6>O$Uw9822$`}av7s;?_KDGAn3vc%K`QjsK^ z#D@9#%NIr{)DZ1BU_SMr5PHn=m=HZDO-eHRK%IniJtig=v(u={XcSPHBr=d7f)ayh zPNA|SMF;4E+^BqPVyqAT*Txy$&HFEDtC#=p+|1Wo-5%L!e%!x;wuO6YV$PgvYh&kyia(r@J7z4$2+KlPSM(F>{zwj7N1vh;LCH}yH*N&hm_J@(Wi8Tv9ydj!!V zhJUNzx*#=5f1r6{)-h{t8QZXtW7fhmT>$q+ejjI$Z@R>c-tvPkZijHY%3?>%+7>S| z<-vOvVey&!Zj7sJ(fr(g+{RK{cQ2^&aX4mC0XzliOeqI73y02pat!MAw6WCDRRA4P z8eeIaI#I@!m%!#a$Re|~zeHHd_Y$_)yMEbnA3y_D{Z3wTk;#v`@qxDFbxG#=3wNIJ zXl`+Ea$!1DeDNI#6fH&@<1}Y9G{35AMrW2q88E5zE)*OY4L3;eWHQ-#V_W3og9d9k zZT-0pCG);*K-e-k#6&ZQV!1NN2r|puX>o&@@%f+G^P~QU7BoGMa_^H#vaF%fw%T%@ zAzz-z>rNAR=NwXIJ#asjKj#pq#0kdq!d0~l7s+Moi#xk$!+2w$;)7%bT!ENN5>(v9 z#^90_cc1spJUnK1ZIW|C?$^=5t#OR5LKLSO)BCnn<}Y;3=5Wa_$F;J0`?kUvP1kJC z3hzHzIjfJQjdcZjx2Q}{A75~*kKZ=Rc(koFkmLSL$2(T$mjkvQqBtNzjF!K7ZENMo zg8AUGYg>cv%1;hl9X!Gvv}8w6G~4CajACo41&_AS_GQi7J+xc&vD`$(a*1_;1EJcw zC5XJbDcsjiY4&}Y*S6Wu-{hw|cgm;T+Io6wJcd8jzu1aXW$7Jph6HYmh$h{1&727L z+x*(E)_JbR@{{In^*Wj{_dj&~SvWvzYLN{z+Yz_>_wcRn>+yYIg(KAb0-o}bQaFmYw4?&O4v zrtV+)qsl=kEb*Z(OGjDK*U5csdoNOy{cyO>cAM-Hue2ppd}Lih(znUShg|;nj_c?$ z6RHJkop{FXly59gZvCC{C@_eW^D9!_k~!=q>X*-=d67rrgu>JS z=lhmXL&w|OCL_5oPyQY4gF(PC>e`+sotw_YvS`jf4YF&S=X$gEG^L-@R-qu3q)ZNH z-t>Ff0e)kn3lH1)(!OW;+i*NN^GHV1q}*3@EI*7ZXxG}i=eu@T80^q*c5AWZ3|jJf zK6q19P}20 zWjmW`CNbl_-P|jdrR^!l5zV-nfAvk^2-06Rd3EW{4~K2H?elyZoPU2TYZowEGB`!~ zDtu_Tu=$^U_PU3Q%l&KC+S_oyDlb-G+oq6{^;`!mK< zX~V%Dw-;^u&D~|V`cKD!C6n`ptLJ9rtPawOROEfjwpdb9UIBp@NBZ9y>zZv-q^>yB zzk02qq_HzvBhLL!PuKiPmDmUK=9}A=v#?G`%&&ZGdu<7etI)7Vmn)>CWrY^z{&^=x z$<}E8ifIdcqPJjO^a>5ZvI(1P*-?2d<1%X_#oa8b-kJyFG-yXneKVLOrm`xcAGxlH zSIk{Ky9v!EwsYJ;=vX}^0DPcF2*42ZaD8NBJ~kHLma)z=-*=TzxPQT9RQqPvzM$y5 z;gUO@wAb@jy6hgD98_(boz5vRt^K28o;cdcaJ?{Kw$`fj<`~6FJ9%tyv~6FjWPM9O zTVKQ3_+PTLrntx)&X0cM0`J#Ka zR`=%Cvj`U1OglGdR4}S(yd0XAx7%rMUvE=cS3dkZPw?~0E0%V4XEq0qeM>E3-_0hW zuw1nM<90!#2!~a3L!OC-uIKObvYAm4Otdlim>M^}=lc(Y|0zt^eTVnTkMZj7VArCHZ-Hk#TXw{9!n)$08R1cexD4tid>@ob{_+1(E{2MrXJ z7+b`uTHf=%TlP-4F!HA|w_%#Z_4wrYCpCf6-ZkG1UK?+1ujp>-d?|KM|6X6mH+k<7 znJ;XYg%_@MT>Ji}i+bSBRie?!^<4s+R}*zZemR5s9rbotb*t{E&F^@yOKQ_PPM+8}jpd0qgKyS2 zzB2#bW6`pj3QBF|6|)eF?>8Z_R#~oV}1Qmv3an01tIHUT@v|xb_Dfpssv7 z-#}5`j@}dRC!8OxmzQOi3*)CW-AdapZ=0J|Gcry2?EV{K4B^Eb)~i(PlGi-9JG;xI zN_YR+lCgO4RY|*(-$vyZxVc5n%5GUMIxjQFNO`JwpT8gToh7${EjF=kccp>r`DHue zz1OL43p{%xLhypb*~0w78db9ui;Ln;@@dX_|LO4^O8ux~;eOqBY1p09HrwlWXj>l? z(OvZP!^b_2Wsnf)eJ!|*kt=l50cRHyr z&lo%i+Avh1c(1-eE8dWnsw|*?iruPg(b1ll1D7}cR%FuB^f~2oL2o{?Xfww&jXTPp zwpe$#t|7SWcSTSVlV^P^lVLRL(NuP0{`mea9#YS~+-bOrUc#;|x?duHIaKcz*|Ld| zAxNKnfv~)a&S&=G#W~L>`xhF&6>)3#a1Zf*rIV&0=Q_ z&cBP>b8@+jx6F}{veo+|Bkb?(a`1Zcy;Wl0>Q~vRzK$y*Qzr7B-xpSxQ;Momny9p2 zLCb4+Sru_;n{c7%;I9h}%5r9lx@XRXo>c$sUXWKAJ2n~0@uMI<+`aQttJ_bZ5!R0g zh>I@AM4D%h2=~vvZx-A7@=sw$^YRyu*DLQ%$USZ#|CGsV&hbXC29HkP!sa5kT)UeR z`|9*oueEgbP}=Hvz}=!+%C#bwk9wTZp&I$vQ*w}jc}Sd-65Mryb(!kzwCv6u9hW!CqouNlAccvoBRP3suMq8YbAx$&v2)N#kdjlzWk(_8JV z1CNXHMl;1S+q5|E3v}G_J>e!dWz}?$R&vtEv@mtWt|k}nm6T?`OGb9yvt5xL0VjKP zZu$A~Ql2?O+alMum5q<>Z||B6EPWHd^I1?kql$aSa9fB8n|y2T!%$-nw-)D{UPqb7 zK7L}PCdz7u($-{Ux^slHJ*zt>O{1ijMpVDAaa`xV^ogusV@^UMp0r;>`F6=}-IKvq z-u??Id$%9?!qW~1_RDn6&e~9wr}I0Xy{wEAF7!V%u2yu_j|H)5gWBgu`W*^w0Z&f| zlrvJR*lo{`-5Gf6HWD}DT(NF`oJ&k}*rsDh#a(Tt((`<62qX2HYr{jHUne>{FUH;f z(|=vC;uA@uj74tRyT#@U_uoG1$3E887&yb?E7~-%Pkp49Nnc!S zAtm_6nNxqANUB%IdeyQsdAC%|3ezujbxqnNySYzTyOzYw`?>X%u#NtjzbP^8Qf2;k zK39XX*BqeTcxU}5e>&r~N4HiVqt&(y&-5<2$O5O>Scey>Gu$_f+7+~x7OM6YY&{&+ z0yrhoc`tixmJAauLPpbH?rOxtCk z2tkA!`jv;vi27ttvB|ag_`U96kGqfK%_@IMVhWFTT-F)kNNol^W z=SP_HcRotlP}=WCRTk5JsHnfXU3c9(6H+$^j z!IK|c51(OlNTAgPcKfPRoekTn+Kx}!>8w*#DmJ04r3QWW^TvIqyF!%0&AQ>r_ktGnRX^IcTs2q}cSqxjnPN_Iz$0GA z%}VuMv%4cwf*!cdrQMC@yj*u#`jr-(9B#{J*&9R{Qy2LauV~#G=d61@&t>tARqaVzU$cJc52<+IN}OZyI{ zZ%e%{UELFS#Nwjvq~#IeZM2J=idVE<<#%aZ5$KQEd}3nTH*4{8<|orebALtWGuwKb9Snx%$(Gs75mj5)vViLz7gQD>|90cZn@G?voqi!?uyck5 zQ6oKbspP3a&x>E4IXe6Lua=S8mZ)<^Uw_xJ;a)1*;MzP}$Ngme(~hxAF3+8I9-q_! z8|J|G^RM4Rw))9e+u#C+`+c`BKYSUS8|tv!PK1?kx zaZ`&rZeStvtfgr9uY7p{$gs&Psme~>KiPW)_kGR~_MlP1n^adF>v-p$VX9-R^QFm( zrcf-a?&`H_?5VMl4MGkR6)=yD(|AYc}zud3z2?j5g_ zMP1rknJB@FDqYM^GRl-5k7-gtQTg8`ZFzK0R=l2`9Dm@nw7}~K^yA+iIogt%C4B=X9ht`yJB|5b@QG!o>R4d?n_cZxpBEuL(Lr}!^N*ex2Q>3#&B4Aw`sfi{OU{NZrmq$ zWUveRSWKlXKe|Fsb+-wm$8(M?@&pl$Ag}-emR3|WDf0u-k&H9Y=i&II-@$s9bfGvU zhqi1GS~@gvMzHdI$^n(efWd(0Qro>BhYD(*b6uc+&-s$>oK#`;r+eolmKZ5Y+lSP! ze$EfCupbH*m*CmfMEmY6lRdG1dz|KFLC3g0Ihp;^8ar0L2SsIbD;r8vI&`ci* zRaLzoeYe-WHZ?MMa*pG~xR$F-aH09all$6CqV7F42=m^Vr6tSzOxGnOge%@bQdN4% zi?9PoxI$w>Yr4}!oVe~dZ8>Kp6B3sl_x+R?mGe7C;w-5r|QGz0pCg^w(?Q%@| zF2O2R68ed-vzD8yA+L(Lro~c85EcN^z=2H{0~Ab!&cnb^phWRJ8gOayO7Fd?28KpS z2~4Q{Cgw6W=CYuv#Un%!kvJ(SMj~&3p{YITW`JzNiFN@&2oO;nOso}A#o`}x2Ej)poB^XUjVs zU}!;5kRbc^XFJ?kSj$tn3liuASK%Y%R9{~orl7gkvy?xwLto83Hp#2QxppAQ=9HU& zj_Bk{`y1J@tT_QTzBGuE2j^ zKC9qc8@w1WW;_S!1YrC@(oAh$!^!{yAvS2s5Dg)aW{K&genNG%E@27+xJCSQK?Q*C zL1yz0ARP!*9ePXu!G&HM*I60UQ_uh?Ei!YJ0DvVPHQ$$akAGxt zqUfUjKcK(X>`2}T7!WZ-#^?|VHv}~ZA%i!)-oM@}sT^)$0zm-!Z`Bw`)K7QE7;Y;t z#<(zX_B$~N?cM#h;MISiW@*d6FZ?fF$Q(Sruqvt52Rf#kOb9o<4Pq8JEDZ zmhM$Ifk9|64u_Z??=HNblER98u?p&6=OIPGhG{e}FvY>}0kCX+=I8X>Ef#ugiJ0tH zf#=Yw0St_=e|pbxsI&1v=y|nu>2YJ=tF8<~sg@nH8f{~i z3VF`W+JrdXIsgobHs~nlp-jYzby5Y0gdD~{9EfR_YO0-PvHD`Amn{LHfkj0 zn3_=tRHvg0mCg@Hv7QCyR)JbXEM(x|GlzNPDsa}#KSBC!j)!6eR_(0B*U%%`R;vj5 zbef>^AiN*ryN`GvLDVC&#((tZo2}4ul^Y5*J81h+_3Y*Okhz(g@54ErIsk5E1x>hT zE=lic0T0ul0wTj%2myIQ2qDt#>8$<lL-$F*1YZ|eFFCmfXT^*^Km$z>)9-Ew7ISONECL*6 zccD=Iama4bD2yB!sQ-ZbBK>RrA^U`BAInXfX$~LlRo@^<*c?k z4fhnt5w;E%K{5c9H`M(%0XP2E&D_5O<3>V@#y^ZZzRvx=B~WNg)9JL+j}nW-$azV6 z!gS=I;Lj2&T@d%1x;w&RHhfxZA1*GAa9w+3M1(zvV9m7+4G)_(vA5$s)u+z=ZiS8{ z0ZkFbL868+`IV1owc{QDi^-gAgTP6AQ$eyy!tqeQ`>~G@7EkYVk9x9Og1qzTJqb7q z7*)WJDsQvcuDIp`H|j3%i2vwjD>JQ&&k}<@9gwMx|9Vfm+vk20m>(%c#YKo9kl`Xe znS_D^X@M7?$77QU7TOn0b5P`50c@=XK!QDg#7F;xAob$&=s<_%>^w%Z$yl_+m4n!u zV-ybu6)$ofh*et}5jcz}~wIwt(Xd&TqQv4-3@|aS6`VP?z zC{;&}K^#wJ*)o^E%>|c!3?=70G)-oPi?$amJt*_QQeqbnI5WZmBQ$n6t&Sf3-wO80 z^WoFmy`oCf)DfV4=N~*tb4xbH=#9cNZSK{9HAkqqAoLSW1O%-M0G{8O<_POlZ5OPc zJNo}YQ&OIo)whIG?|G0bATL0t6XoD`VSf0q(UgsoQ}^gmWvtP=gWjO~L*#ul+}z$? zf|#uUlIoKHzH@lhF}IT!=XPijIB@YXUq3*u?&?1A{jKur@(Q+TZO>1O49i#?%bxssSM+Muutm&y zdMgc<@^E%c&Nff9)GFScl`D8TDKj(Dxn=0%w7c!+ujRpgeJ&*^fgu7Ejl~8~)AqV% z@gGjWsj0x$t^UOe9{>2H;^I9Q_LTXq`_SIr0%*OQhDHM1lU~5^$vJTD6myKB9{yd6 ziP2*ZPU>h}3o_g8Q>QkjrlidDYcw`>d^EGLXaQo4yf6LXL%FqUuOdvmXlO7Z-*pg3 zjg+@84@yen4Gj&Q@smunK`+oMx(A-LahNuGJ3eX)m$d1+p!&W4yKxW0WAPWSUoWG4 zHSi$m{%yN&4_D>7_V#!{ZeQJ2iv%|rKii&PP zvd&mJE&@-mF+!)mPuRS{&Oa?5KW>+g2?HKvp~!~I0GT^~tB*-)T)1eo@zhWE9hAx5 zz4416Po<5AJ{7W-(IB&}wtaG=udh${)Ui#Xlhe~zt=BBa__!Tb%K7>E7xx1QeHa;u zOpM`+=4R8@+6q$-@7??M86o(oz((@Qiv!TX)`jAJ(ohM{Xhks_H#ZDq)vg$(Lc!0O z@EAw7{vQ{BL({R`y;Fvb$FwVBy>gA9V?_4)$OPJf2&EdqwaV5M{Z~RlYbYT{{3?P| zAc*|MQ9p!3le}0-`q=^r@|PRY-3*t>UtZRE(&_~J1&E?KAD`o6f8dm07Hk-`QAKzm zAn1P$YezRBYar?S{KUzNx@VDxl$X$aOzrHVz`9xr6T?6^f5nRD@h(u!keXz) zErBh|MO}tS{Ws%`)7&uJssXa~{6J?|eXNs*M-+^U%CA@hjb8<95b-TG4Zx>k-d%PQ z870IAnkUQL5u=Dc2Jm>(k3jPb$5CeoV3TGhK!wAHdB(6T>ddHWF$Nie>>Piq*-j9upJcYIZ zabm;TfdmQzWoTs7Jv77(KS#*5I8OflmX?(zaEZ9kSv<0$;nxHc0Blr1_r@t?s5m}G zZ*On#g1Sj=+CHa#FjAi}!y+8@8ZIRM_a57Xm8J<5dq@}G*yJOR2j!@@x2d^#FpM(s zB>>$rAPKAu<`B^Rv3K|GbC^#_c6lQtkZsj^O=x7~LU6K1{{Gy3?g!}J(uB0(lD{`= zH5A$RW)Q~$zOmGM_oVP61l^m$vmQP?2X9g!%JG=%wFT|C&u78EW%FhtRswvA_QSXA zN+6sfr#gd18@>QQvoy-b%j;>HX);(TZ0CMF6YM{YCH<}uyGQsO1u;vhRjaluxFb-L zJ(6fCgGfTMDsjKzGsCrIxw(7<&xSqP#KIy3*xN6JrVHd2jT#sju*!4h0fE4D*RHe3 z2lj*Go>cW+4*+Eyh-kt=4QVc&Dg`S?*!W3lXow?uC!TjC>2(C4J@Kszu~$PfE{WQB z!GZ;4W9EDI5L=2KqR|dP8FmhiY?Masj<5kDzH6X~CuL@G`S|z{+roAvvKL@ua4k0W z8LB+Ehgku69ssu*{v}JQkgvJq3nOe$bMn=-5klvHU1y)9?xv@+Ly8C~nC`Z1Y?nkf zp2bE9OWN0;PZ1?~fO4=h+m7TG6oeqffO9X6diClRf#0DoQTi$d>^0!``2_@sbUyNB zy;G-(S1g0pDbZ?kaF9d{PqcW5SScZ5hl_>+`|gt`A^>O00$rzD+jv8XF;>A&^`tr? zO^~4DEQ~Z)EMQ`4!XCB*Arx`%=P{Bp;wp%lB990T3E|~wMtiUB0Ly!9$7m#x>_2R?tP$m#w2rQq7vf=)oN|Do5nBms@V*3y6cB|MUS zH)27tQMWgrYO%L#EG{ZS+BSXO4bind_fXV|Wl~aSA%i5_egeW*%1TO!h!^gE20!G6 zV!4ivX*34@{<=*V)-yiGkMBZJrC5u|kcy}wDMn>LZ1$QX;8c0=>_f*PGjkbr?bVX-ECs%et{Kc zDy{pXAK+?xd#{Dln?ie6d%Jvz`7NMzkvg@$dzX}&x&X$YRX6hxDns>?&)`nug;6M! zC80JBNol_aBVgK?=h;|WN5IksHZ8r+EzHf?zJLaK?{OfAfW%WR%O9KfegJ%{3Q9}W zHmRwRd=W0WJ*hQY+}c%|kMG{iN8Do&2$HdE@yw7IYe6lPTeC(Vs}EJ9%r9vvAZL?G zN=iD+%dcNw0u5cEm&NdIK$SxH0KmttE1F`NS;t~i7#hEDwEhY-3 zJVls5MMWEsX)Q%JMG(H~0C!&OZ_DDyM%6`hr|=Y#hh(SvBoi^}1Az-<0tPAGZ|+Bq zyjg10CcqaTAAi^4A%X-~*?S@Qz33E{OY<+1j}t zc5%^0HG%jy+Rj;49%-+s6g+)uh7%`x84S?9`}Q@zd$;U~)yN4reg(mN4v8cnbGjOh z3EmBmWQ2nc#OOKbP4wkv=H@@Y?SeEA5TB&ZK_F%CN(H;A%G?1qKL_Z}wI5WizQ+FLZcIV&Ij5RzKbmyX!$UGIifo49}iDLsTUC#z=^S zB5oKYJqcHzu(Duy5?o~uFnV2MV=eZ0FZjfuU`oo%%WKQts;5Ubc~&ki;KtXR zU=u{V!rsCuDk_QsQ4^d4gnJ?oa_#zc?d0ZOf38$D^g`hktmoyWL~!XSII!IcJ()d% zR0iu$=l+|-@^UfAd@*|?#(V)9eucceHeQWN_MU(TUWMRG)La&Ky3F+^M|pm$UyLx$ zh4UbFFe2z_KQ>)h?1J-%e2dwURKZhIzGXYBj`^UO1wIB;r3nd&kbwS9?bx>MDQ-Um zy4}!PNX*VQEy{q^*+Bvl&&lBhEwc(_-xcy)H*enb`t#j5%`9E4_Nie1KC+|kTnKL- zB*jYa%R!%mx`jRVRHDHSuseC&;1ftuz<*>Pc)|Mw8li+tEi5do>+v%F=1pRyj_s(L zunyrjB#8wMo;gL?+1U}D3~)E9VWKng^#$J}tL&sUuPB(YCDi%~j; zp*2N1wMgWll)l*i!lt!r`CvPNWa*ce2qKwmVkY-b#Tvosw$l*Dg!&8CFr}te+SU@f zJP$pI7V4%AIDGy=`lA z#3jLk4S1vD5aIKbyr!mPRNexK{``*r&il(3KyNrg>EMAP>i=&^K>wIImOF}9(wly+ Rm!R-(o8AuHOl|wK{|h@L(T)HB From 5a09c0ac6f6ff675db1b472b6d0184cef53da9b6 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 7 Mar 2025 12:00:59 +0100 Subject: [PATCH 58/82] filter non-self mDNS packets --- toversok/actors/a_mman.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/toversok/actors/a_mman.go b/toversok/actors/a_mman.go index bf87deb..7e55652 100644 --- a/toversok/actors/a_mman.go +++ b/toversok/actors/a_mman.go @@ -219,12 +219,12 @@ func (mm *MDNSManager) handleSystemFrame(frame RecvFrame) { return } - // TODO proper filtering + if !mm.isSelf(frame.src.Addr()) { + L(mm).Log(context.Background(), types.LevelTrace, "dropping mDNS packet due to non-local origin", "from", frame.src) + return + } - //if !frame.src.Addr().IsLoopback() { - // // drop non-loopback, is from LAN - // continue - //} + // TODO proper in-depth filtering L(mm).Debug("spreading local MDNS packet to peers", "len", len(frame.pkt), "from", frame.src.String()) @@ -283,6 +283,10 @@ func (mm *MDNSManager) isLocal(addr netip.Addr) bool { }) != -1 } +func (mm *MDNSManager) isSelf(addr netip.Addr) bool { + return mm.isLocal(addr) || addr == mm.s.control.IPv4().Addr() || addr == mm.s.control.IPv6().Addr() +} + func (mm *MDNSManager) processMDNS(pkt []byte, local bool) []byte { msg := dnsmessage.Message{} if err := msg.Unpack(pkt); err != nil { From afd2216e7b4a59a42acecd61a1721ae3ecd97ffc Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 7 Mar 2025 12:10:03 +0100 Subject: [PATCH 59/82] linter changes --- cmd/mdns_monitor/main.go | 48 ++++----------------------------------- toversok/actors/a_mman.go | 38 ++++++++----------------------- types/msgsess/parsing.go | 1 + usrwg/wgusp.go | 9 ++++---- 4 files changed, 20 insertions(+), 76 deletions(-) diff --git a/cmd/mdns_monitor/main.go b/cmd/mdns_monitor/main.go index f45aa65..b84a4dd 100644 --- a/cmd/mdns_monitor/main.go +++ b/cmd/mdns_monitor/main.go @@ -2,8 +2,6 @@ package main import ( "context" - "crypto/sha256" - "encoding/base64" "fmt" "log" "net" @@ -14,22 +12,6 @@ import ( "golang.org/x/net/dns/dnsmessage" ) -//func Control(network, address string, c syscall.RawConn) (err error) { -// controlErr := c.Control(func(fd uintptr) { -// unix.SetsockoptInet4Addr(int(fd), unix.IPPROTO_IP, unix.IP_ADD_MEMBERSHIP) -// -// err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) -// if err != nil { -// return -// } -// err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) -// }) -// if controlErr != nil { -// err = controlErr -// } -// return -//} - func walkInterfaces() { ift, err := net.Interfaces() if err != nil { @@ -43,36 +25,22 @@ func walkInterfaces() { } } -// loopbackInterface returns an available logical network interface -// for loopback tests. It returns nil if no suitable interface is -// found. -func loopbackInterface() *net.Interface { - ift, err := net.Interfaces() - if err != nil { - return nil - } - for _, ifi := range ift { - if ifi.Flags&net.FlagLoopback != 0 && ifi.Flags&net.FlagUp != 0 { - return &ifi - } - } - return nil -} - func main() { // this code is specific to macos, for now walkInterfaces() IP := "224.0.0.251:5353" - //IP := "[ff02::fb]:5353" + // IP := "[ff02::fb]:5353" ua := net.UDPAddrFromAddrPort(netip.MustParseAddrPort(IP)) iface, err := net.InterfaceByName("lo0") + if err != nil { + log.Fatal(err) + } bind, err := net.ListenMulticastUDP("udp4", iface, ua) - if err != nil { log.Fatal(err) } @@ -99,7 +67,6 @@ func main() { for { n, ap, err := bind.ReadFromUDPAddrPort(buf) - if err != nil { log.Fatal(err) } @@ -115,7 +82,6 @@ func main() { } _, _, _, ok, err := store.Take(context.Background(), msg.GoString()) - if err != nil { log.Fatal(err) } @@ -142,9 +108,3 @@ func main() { } } } - -func dataToB64Hash(b []byte) string { - h := sha256.Sum256(b) - - return base64.StdEncoding.EncodeToString(h[:]) -} diff --git a/toversok/actors/a_mman.go b/toversok/actors/a_mman.go index 7e55652..c956c1e 100644 --- a/toversok/actors/a_mman.go +++ b/toversok/actors/a_mman.go @@ -5,17 +5,18 @@ import ( "crypto/sha256" "encoding/base64" "fmt" + "net" + "net/netip" + "runtime" + "slices" + "time" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/msgactor" "github.com/sethvargo/go-limiter" "github.com/sethvargo/go-limiter/memorystore" "golang.org/x/net/dns/dnsmessage" "golang.org/x/net/ipv4" - "net" - "net/netip" - "runtime" - "slices" - "time" ) type MDNSManager struct { @@ -82,22 +83,6 @@ var ( ip4MDNSBroadcastAddress = netip.AddrPortFrom(netip.MustParseAddr("224.0.0.251"), MDNSPort) ) -// loopbackInterface returns an available logical network interface -// for loopback tests. It returns nil if no suitable interface is -// found. -func loopbackInterface() *net.Interface { - ift, err := net.Interfaces() - if err != nil { - return nil - } - for _, ifi := range ift { - if ifi.Flags&net.FlagLoopback != 0 && ifi.Flags&net.FlagUp != 0 { - return &ifi - } - } - return nil -} - func (mm *MDNSManager) makeMDNSListener() (types.UDPConn, error) { // TODO this only catches ipv4 traffic, which may be a bit "eh", // it may be worth considering firing up one for each stack. @@ -310,7 +295,7 @@ func (mm *MDNSManager) processMDNS(pkt []byte, local bool) []byte { dirty = true } } - } else { + } else if msg.Response { // RFC 6762: // Multicast DNS responses MUST NOT contain any questions in the // Question Section. Any questions in the Question Section of a @@ -322,12 +307,9 @@ func (mm *MDNSManager) processMDNS(pkt []byte, local bool) []byte { // f.e. avahi doesn't properly work if the questions section is filled out, so we need to process that. // // The likes of Apple's mDNSResponder haven't gotten this above message, so we need to check for this. - - if msg.Response { - if len(msg.Questions) != 0 { - msg.Questions = []dnsmessage.Question{} - dirty = true - } + if len(msg.Questions) != 0 { + msg.Questions = []dnsmessage.Question{} + dirty = true } } diff --git a/types/msgsess/parsing.go b/types/msgsess/parsing.go index dc283dd..ebebee0 100644 --- a/types/msgsess/parsing.go +++ b/types/msgsess/parsing.go @@ -3,6 +3,7 @@ package msgsess import ( "errors" "fmt" + "github.com/edup2p/common/types/key" ) diff --git a/usrwg/wgusp.go b/usrwg/wgusp.go index 3392a41..d062e35 100644 --- a/usrwg/wgusp.go +++ b/usrwg/wgusp.go @@ -2,8 +2,6 @@ package usrwg import ( "fmt" - "github.com/google/gopacket/layers" - "golang.zx2c4.com/wireguard/tun" "log/slog" "net" "net/netip" @@ -15,7 +13,9 @@ import ( "github.com/edup2p/common/types/key" "github.com/edup2p/common/usrwg/router" "github.com/google/gopacket" + "github.com/google/gopacket/layers" "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/tun" ) func init() { @@ -139,14 +139,15 @@ func (u *UserSpaceWireGuardController) InjectPacket(from, to netip.AddrPort, pkt DstPort: layers.UDPPort(to.Port()), SrcPort: layers.UDPPort(from.Port()), } - udp.SetNetworkLayerForChecksum(ipv4) + if err := udp.SetNetworkLayerForChecksum(ipv4); err != nil { + return fmt.Errorf("failed to set udp checksum: %w", err) + } err := gopacket.SerializeLayers(buf, opts, ipv4, udp, gopacket.Payload(pkt), ) - if err != nil { return fmt.Errorf("failed to serialize packet: %w", err) } From d438ca1844604da46805618321a50c38a04689b4 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 7 Mar 2025 13:45:21 +0100 Subject: [PATCH 60/82] add runningCtx closes #111 --- cmd/dev_client/main.go | 2 +- test_suite/test_client/main.go | 9 ++++--- toversok/engine.go | 48 +++++++++++++++++++++++++--------- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/cmd/dev_client/main.go b/cmd/dev_client/main.go index 70b08fc..8e2a154 100644 --- a/cmd/dev_client/main.go +++ b/cmd/dev_client/main.go @@ -819,7 +819,7 @@ func enCmd() *ishell.Cmd { c.AddCmd(&ishell.Cmd{Name: "start", Help: "start the engine", Func: func(c *ishell.Context) { if engine != nil { - err := engine.Start() + _, err := engine.Start() if err != nil { c.Err(err) } diff --git a/test_suite/test_client/main.go b/test_suite/test_client/main.go index 344b836..8fa9f22 100644 --- a/test_suite/test_client/main.go +++ b/test_suite/test_client/main.go @@ -180,7 +180,9 @@ func main() { os.Exit(1) } - if err = engine.Start(); err != nil { + var runningCtx context.Context + + if runningCtx, err = engine.Start(); err != nil { slog.Error("could not start engine", "err", err) os.Exit(1) } @@ -201,10 +203,11 @@ func main() { ccc(errors.New("interrupted")) }() - <-engine.Context().Done() + <-runningCtx.Done() + ccc(errors.New("stopping")) if !interrupted { - slog.Warn("engine exited with error", "err", engine.Context().Err()) + slog.Warn("stopped with error", "err", runningCtx.Err()) os.Exit(1) } } diff --git a/toversok/engine.go b/toversok/engine.go index cc86cae..3473bf8 100644 --- a/toversok/engine.go +++ b/toversok/engine.go @@ -11,6 +11,7 @@ import ( "time" "github.com/edup2p/common/types" + "github.com/edup2p/common/types/control" "github.com/edup2p/common/types/key" ) @@ -22,6 +23,9 @@ type Engine struct { ctx context.Context ccc context.CancelCauseFunc + runningCtx context.Context + runningCancel context.CancelFunc + sess *Session extBind *types.UDPConnCloseCatcher @@ -33,9 +37,8 @@ type Engine struct { nodePriv key.NodePrivate - state stateObserver - doAutoRestart bool - dirty bool + state stateObserver + dirty bool deviceKey *string } @@ -48,9 +51,13 @@ type Engine struct { // - Reason for any other startup error. // // After the engine has successfully started once, it will automatically restart on any failure. -func (e *Engine) Start() error { - e.doAutoRestart = true - return e.start(true) +func (e *Engine) Start() (context.Context, error) { + err := e.start(true) + if err != nil { + return nil, err + } + + return e.runningCtx, nil } func (e *Engine) start(allowLogon bool) error { @@ -63,11 +70,17 @@ func (e *Engine) start(allowLogon bool) error { return errors.New("cannot start; already running") } + if e.runningCtx != nil { + e.runningCancel() + } + if e.sess != nil && e.sess.ctx.Err() == nil { // Session is still running, even though that shouldn't be the case, as we checked for NoSession above e.sess.ccc(errors.New("engine state desynced, shutting down")) } + e.runningCtx, e.runningCancel = context.WithCancel(e.ctx) + if err := e.maybeClean(); err != nil { return fmt.Errorf("engine state cleaning failed: %w", err) } @@ -85,6 +98,14 @@ func (e *Engine) Context() context.Context { return e.ctx } +func (e *Engine) RunningContext() context.Context { + if e.runningCtx != nil && e.runningCtx.Err() != nil { + return nil + } + + return e.runningCtx +} + func (e *Engine) maybeClean() error { slog.Debug("maybeClean called", "dirty", e.dirty) @@ -112,6 +133,11 @@ const StalledEngineRestartInterval = time.Second * 2 func (e *Engine) autoRestart() { if e.WillRestart() { if err := e.start(false); err != nil { + if errors.Is(err, control.ErrNeedsLogon) { + // Bail, we can't do anything here + e.runningCancel() + } + slog.Info("autoRestart: will retry in 10 seconds") time.AfterFunc(StalledEngineRestartInterval, e.autoRestart) } @@ -131,11 +157,7 @@ func (e *Engine) Stop() { return } - e.doAutoRestart = false - - if e.sess.ctx.Err() != nil { - e.sess.ccc(errors.New("shutting down")) - } + e.runningCancel() var stillDirty bool @@ -174,7 +196,7 @@ func (e *Engine) installSession(allowLogon bool) error { } var err error - e.sess, err = SetupSession(e.ctx, e.wg, e.fw, e.co, e.getExtConn, e.getNodePriv, logon) + e.sess, err = SetupSession(e.runningCtx, e.wg, e.fw, e.co, e.getExtConn, e.getNodePriv, logon) if err != nil { return fmt.Errorf("failed to setup session: %w", err) } @@ -200,7 +222,7 @@ func (e *Engine) installSession(allowLogon bool) error { // WillRestart says whether the engine strives to be in a running state. func (e *Engine) WillRestart() bool { - return e.doAutoRestart && e.ctx.Err() != nil + return e.runningCtx != nil && e.runningCtx.Err() != nil } func (e *Engine) slog() *slog.Logger { From 60434cc7730fdad92f36736fe8006e448219d482 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 7 Mar 2025 14:27:04 +0100 Subject: [PATCH 61/82] use AfterFunc for actor close Closes #122 --- toversok/actors/a_conn.go | 15 ++++++++------- toversok/actors/a_direct.go | 10 ++++------ toversok/actors/a_eman.go | 5 ++--- toversok/actors/a_mman.go | 10 ++++------ toversok/actors/a_relay.go | 24 ++++++++++-------------- toversok/actors/a_sman.go | 5 ++--- toversok/actors/a_sockrecv.go | 21 ++++++++------------- toversok/actors/a_tman.go | 6 ++---- toversok/actors/common.go | 4 ++++ toversok/actors/stage.go | 12 ++++++++---- toversok/actors/util.go | 6 ++++++ types/ifaces/actor.go | 5 ++++- usrwg/router/router_windows.go | 3 ++- usrwg/router/util.go | 5 +++-- 14 files changed, 67 insertions(+), 64 deletions(-) diff --git a/toversok/actors/a_conn.go b/toversok/actors/a_conn.go index 9f04744..11bf2c4 100644 --- a/toversok/actors/a_conn.go +++ b/toversok/actors/a_conn.go @@ -3,6 +3,7 @@ package actors import ( "context" "errors" + "log/slog" "net" "net/netip" "runtime/debug" @@ -39,7 +40,7 @@ func MakeOutConn(udp types.UDPConn, peer key.NodePublic, homeRelay int64, s *Sta common := MakeCommon(s.Ctx, OutConnInboxChanBuffer) - return &OutConn{ + return assureClose(&OutConn{ ActorCommon: common, sock: MakeSockRecv(common.ctx, udp), @@ -51,7 +52,7 @@ func MakeOutConn(udp types.UDPConn, peer key.NodePublic, homeRelay int64, s *Sta activityTimer: t, isActive: false, - } + }) } func (oc *OutConn) Run() { @@ -73,7 +74,6 @@ func (oc *OutConn) Run() { for { select { case <-oc.ctx.Done(): - oc.Close() return case <-oc.sock.ctx.Done(): oc.Cancel() @@ -201,7 +201,7 @@ func MakeInConn(udp types.UDPConn, peer key.NodePublic, s *Stage) *InConn { t := time.NewTimer(60 * time.Second) t.Stop() - return &InConn{ + return assureClose(&InConn{ ActorCommon: MakeCommon(s.Ctx, -1), s: s, @@ -213,7 +213,7 @@ func MakeInConn(udp types.UDPConn, peer key.NodePublic, s *Stage) *InConn { pktCh: make(chan []byte, InConnFrameChanBuffer), peer: peer, - } + }) } func (ic *InConn) Run() { @@ -221,7 +221,6 @@ func (ic *InConn) Run() { if v := recover(); v != nil { L(ic).Error("panicked", "panic", v, "stack", string(debug.Stack())) ic.Cancel() - ic.Close() bail(ic.ctx, v) } }() @@ -234,7 +233,6 @@ func (ic *InConn) Run() { for { select { case <-ic.ctx.Done(): - ic.Close() return case <-ic.activityTimer.C: ic.UnBump() @@ -265,6 +263,9 @@ func (ic *InConn) Close() { Peer: ic.peer, IsIn: false, } + if err := ic.udp.Close(); err != nil { + slog.Error("failed to close inconn udp", "peer", ic.peer, "err", err) + } } func (ic *InConn) Ctx() context.Context { diff --git a/toversok/actors/a_direct.go b/toversok/actors/a_direct.go index f6057ae..e171777 100644 --- a/toversok/actors/a_direct.go +++ b/toversok/actors/a_direct.go @@ -31,12 +31,12 @@ type DirectManager struct { func (s *Stage) makeDM(udpSocket types.UDPConn) *DirectManager { c := MakeCommon(s.Ctx, -1) - return &DirectManager{ + return assureClose(&DirectManager{ ActorCommon: c, sock: MakeSockRecv(c.ctx, udpSocket), s: s, writeCh: make(chan directWriteRequest, DirectManWriteChLen), - } + }) } func (dm *DirectManager) Run() { @@ -60,7 +60,6 @@ func (dm *DirectManager) Run() { for { select { case <-dm.ctx.Done(): - dm.Close() return case req := <-dm.writeCh: L(dm).Log(context.Background(), types.LevelTrace, "direct: writing") @@ -115,13 +114,13 @@ type DirectRouter struct { } func (s *Stage) makeDR() *DirectRouter { - return &DirectRouter{ + return assureClose(&DirectRouter{ ActorCommon: MakeCommon(s.Ctx, DirectRouterInboxChLen), s: s, aka: make(map[netip.AddrPort]key.NodePublic), stunEndpoints: make(map[netip.AddrPort]bool), frameCh: make(chan ifaces.DirectedPeerFrame, DirectRouterFrameChLen), - } + }) } func (dr *DirectRouter) Push(frame ifaces.DirectedPeerFrame) { @@ -149,7 +148,6 @@ func (dr *DirectRouter) Run() { for { select { case <-dr.ctx.Done(): - dr.Close() return case m := <-dr.inbox: switch m := m.(type) { diff --git a/toversok/actors/a_eman.go b/toversok/actors/a_eman.go index b86a3af..fc99c21 100644 --- a/toversok/actors/a_eman.go +++ b/toversok/actors/a_eman.go @@ -17,14 +17,14 @@ import ( ) func (s *Stage) makeEM() *EndpointManager { - em := &EndpointManager{ + em := assureClose(&EndpointManager{ ActorCommon: MakeCommon(s.Ctx, SessManInboxChLen), s: s, ticker: time.NewTicker(EManTickerInterval), stunTimeout: time.NewTimer(EManStunTimeout), relays: make(map[int64]relay.Information), - } + }) em.stunTimeout.Stop() @@ -82,7 +82,6 @@ func (em *EndpointManager) Run() { for { select { case <-em.ctx.Done(): - em.Close() return case <-em.ticker.C: em.startSTUN() diff --git a/toversok/actors/a_mman.go b/toversok/actors/a_mman.go index c956c1e..5a3f74a 100644 --- a/toversok/actors/a_mman.go +++ b/toversok/actors/a_mman.go @@ -48,11 +48,11 @@ func (s *Stage) makeMM() *MDNSManager { panic(err) } - m := &MDNSManager{ + m := assureClose(&MDNSManager{ ActorCommon: c, s: s, rlStore: store, - } + }) bind, err := m.makeMDNSListener() if err != nil { @@ -189,8 +189,7 @@ func (mm *MDNSManager) Run() { mm.handleSystemFrame(frame) case frame := <-mm.querySock.outCh: mm.handleSystemFrame(frame) - case <-mm.s.Ctx.Done(): - mm.Close() + case <-mm.ctx.Done(): return } } @@ -334,8 +333,7 @@ func (mm *MDNSManager) deadRun() { for { select { case <-mm.inbox: - case <-mm.s.Ctx.Done(): - mm.Close() + case <-mm.ctx.Done(): return } } diff --git a/toversok/actors/a_relay.go b/toversok/actors/a_relay.go index 6ee7370..5b15446 100644 --- a/toversok/actors/a_relay.go +++ b/toversok/actors/a_relay.go @@ -87,11 +87,8 @@ func (c *RestartableRelayConn) Run() { c.connected = false // Possibly the client exited because the relayConn is being closed, check for that first - select { - case <-c.ctx.Done(): + if c.ctx.Err() != nil { return - default: - // fallthrough } if err != nil { c.L().Warn("relay client exited", "error", err) @@ -186,7 +183,7 @@ func (c *RestartableRelayConn) loop() error { } func (c *RestartableRelayConn) Close() { - c.ctxCan() + // TODO nothing much to close? } // Queue queues the pkt for dst in a non-blocking fashion @@ -268,7 +265,7 @@ type RelayManager struct { const HomeRelayChangeInterval = time.Minute * 5 func (s *Stage) makeRM() *RelayManager { - return &RelayManager{ + return assureClose(&RelayManager{ ActorCommon: MakeCommon(s.Ctx, RelayManInboxChLen), s: s, homeRelay: 0, @@ -276,7 +273,7 @@ func (s *Stage) makeRM() *RelayManager { relays: make(map[int64]RelayConnActor), inCh: make(chan ifaces.RelayedPeerFrame, RelayManFrameChLen), writeCh: make(chan relayWriteRequest, RelayManWriteChLen), - } + }) } func (rm *RelayManager) Run() { @@ -298,7 +295,6 @@ func (rm *RelayManager) Run() { for { select { case <-rm.ctx.Done(): - rm.Close() return case m := <-rm.inbox: switch m := m.(type) { @@ -367,7 +363,7 @@ func (rm *RelayManager) Run() { } func (rm *RelayManager) Close() { - rm.ctxCan() + // TODO nothing much to close? } func (rm *RelayManager) selectRelay(latencies map[int64]time.Duration) int64 { @@ -402,9 +398,10 @@ func (rm *RelayManager) getConn(id int64) RelayConnActor { func (rm *RelayManager) update(info relay.Information) { if r, ok := rm.relays[info.ID]; ok { r.Update(info) + return } - r := &RestartableRelayConn{ + r := assureClose(&RestartableRelayConn{ ActorCommon: MakeCommon(rm.ctx, -1), man: rm, config: info, @@ -412,7 +409,7 @@ func (rm *RelayManager) update(info relay.Information) { stay: info.ID == rm.homeRelay, bufferCh: make(chan relay.SendPacket, RelayConnSendBufferSize), pokeCh: make(chan interface{}, 1), - } + }) go r.Run() @@ -441,11 +438,11 @@ type RelayRouter struct { } func (s *Stage) makeRR() *RelayRouter { - return &RelayRouter{ + return assureClose(&RelayRouter{ ActorCommon: MakeCommon(s.Ctx, -1), s: s, frameCh: make(chan ifaces.RelayedPeerFrame, RelayRouterFrameChLen), - } + }) } func (rr *RelayRouter) Push(frame ifaces.RelayedPeerFrame) { @@ -473,7 +470,6 @@ func (rr *RelayRouter) Run() { for { select { case <-rr.ctx.Done(): - rr.Close() return case frame := <-rr.frameCh: if msgsess.LooksLikeSessionWireMessage(frame.Pkt) { diff --git a/toversok/actors/a_sman.go b/toversok/actors/a_sman.go index 927b357..21c90f2 100644 --- a/toversok/actors/a_sman.go +++ b/toversok/actors/a_sman.go @@ -25,11 +25,11 @@ type SessionManager struct { var DebugSManTakeNodeAsSession = false func (s *Stage) makeSM(priv func() *key.SessionPrivate) *SessionManager { - sm := &SessionManager{ + sm := assureClose(&SessionManager{ ActorCommon: MakeCommon(s.Ctx, SessManInboxChLen), s: s, session: priv, - } + }) L(sm).Debug("sman with session key", "sess", priv().Public().Debug()) @@ -53,7 +53,6 @@ func (sm *SessionManager) Run() { for { select { case <-sm.ctx.Done(): - sm.Close() return case inMsg := <-sm.inbox: sm.Handle(inMsg) diff --git a/toversok/actors/a_sockrecv.go b/toversok/actors/a_sockrecv.go index af75e27..30fb1b2 100644 --- a/toversok/actors/a_sockrecv.go +++ b/toversok/actors/a_sockrecv.go @@ -31,12 +31,12 @@ type SockRecv struct { } func MakeSockRecv(ctx context.Context, udp types.UDPConn) *SockRecv { - return &SockRecv{ + return assureClose(&SockRecv{ Conn: udp, outCh: make(chan RecvFrame, SockRecvFrameChanBuffer), ActorCommon: MakeCommon(ctx, -1), - } + }) } func (r *SockRecv) Run() { @@ -95,31 +95,26 @@ func (r *SockRecv) Run() { pkt := slices.Clone(buf[:n]) - if context.Cause(r.ctx) != nil { + if r.ctx.Err() != nil { return } select { - case <-r.ctx.Done(): - r.Close() - return case r.outCh <- RecvFrame{ pkt: pkt, ts: ts, src: ap, }: // fallthrough continue + case <-r.ctx.Done(): + return } } } func (r *SockRecv) Close() { - if r.ctx.Err() == nil { - if err := r.Conn.Close(); err != nil { - slog.Error("failed to close connection for sockrecv", "err", err) - } - close(r.outCh) - r.ctxCan() - return + if err := r.Conn.Close(); err != nil { + slog.Error("failed to close connection for sockrecv", "err", err) } + close(r.outCh) } diff --git a/toversok/actors/a_tman.go b/toversok/actors/a_tman.go index 46ce057..66e558b 100644 --- a/toversok/actors/a_tman.go +++ b/toversok/actors/a_tman.go @@ -35,7 +35,7 @@ type TrafficManager struct { } func (s *Stage) makeTM() *TrafficManager { - return &TrafficManager{ + return assureClose(&TrafficManager{ ActorCommon: MakeCommon(s.Ctx, TrafficManInboxChLen), s: s, @@ -47,7 +47,7 @@ func (s *Stage) makeTM() *TrafficManager { activeOut: make(map[key.NodePublic]bool), activeIn: make(map[key.NodePublic]bool), sessMap: make(map[key.SessionPublic]key.NodePublic), - } + }) } func (tm *TrafficManager) Run() { @@ -55,7 +55,6 @@ func (tm *TrafficManager) Run() { if v := recover(); v != nil { L(tm).Error("panicked", "error", v, "stack", string(debug.Stack())) tm.Cancel() - tm.Close() bail(tm.ctx, v) } }() @@ -69,7 +68,6 @@ func (tm *TrafficManager) Run() { select { case <-tm.ctx.Done(): - tm.Close() return case <-tm.ticker.C: // Run periodic before inbox, as inbox can get backed up, and ping + path management would get delayed. diff --git a/toversok/actors/common.go b/toversok/actors/common.go index 574ec6a..a3fc704 100644 --- a/toversok/actors/common.go +++ b/toversok/actors/common.go @@ -39,6 +39,10 @@ func (ac *ActorCommon) Cancel() { ac.ctxCan() } +func (ac *ActorCommon) Ctx() context.Context { + return ac.ctx +} + func (ac *ActorCommon) logUnknownMessage(am msgactor.ActorMessage) { // TODO make better; somehow get actor name in there slog.Error("got unknown message", "ac", ac, "am", am) diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index 980bbeb..329564a 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -24,15 +24,11 @@ import ( type OutConnActor interface { ifaces.Actor - - Ctx() context.Context } type InConnActor interface { ifaces.Actor - Ctx() context.Context - ForwardPacket(pkt []byte) } @@ -92,6 +88,8 @@ func MakeStage( s.EMan = s.makeEM() s.MMan = s.makeMM() + context.AfterFunc(s.Ctx, s.Close) + return s } @@ -175,6 +173,12 @@ func (s *Stage) Start() { s.started = true } +func (s *Stage) Close() { + if err := s.ext.Close(); err != nil { + slog.Error("error closing ext for stage", "err", err) + } +} + // Watchdog will be run to constantly check for faults on the stage and repair them. func (s *Stage) Watchdog() { ticker := time.NewTicker(time.Second * 5) diff --git a/toversok/actors/util.go b/toversok/actors/util.go index 03e4032..058d605 100644 --- a/toversok/actors/util.go +++ b/toversok/actors/util.go @@ -58,3 +58,9 @@ func sortEndpointSlice(endpoints []netip.AddrPort) { return endpoints[i].Addr().Less(endpoints[j].Addr()) && endpoints[i].Port() < endpoints[j].Port() }) } + +func assureClose[T ifaces.Actor](a T) T { + context.AfterFunc(a.Ctx(), a.Close) + + return a +} diff --git a/types/ifaces/actor.go b/types/ifaces/actor.go index 9fdaf28..1e8b7a5 100644 --- a/types/ifaces/actor.go +++ b/types/ifaces/actor.go @@ -1,6 +1,7 @@ package ifaces import ( + "context" "net/netip" "time" @@ -15,10 +16,12 @@ type Actor interface { Inbox() chan<- msgactor.ActorMessage + Ctx() context.Context + // Cancel this actor's context. Cancel() - // Close is called by the actor's Run loop when cancelled. + // Close is called by AfterFunc to clean up Close() } diff --git a/usrwg/router/router_windows.go b/usrwg/router/router_windows.go index 16e1c0a..09fa845 100644 --- a/usrwg/router/router_windows.go +++ b/usrwg/router/router_windows.go @@ -42,7 +42,8 @@ func init() { func isWindowsService() bool { v, err := svc.IsWindowsService() if err != nil { - log.Fatalf("svc.IsWindowsService failed: %v", err) + // Expect that we can at least poke the local windows service, else we're in trouble. + panic("svc.IsWindowsService failed:", err) } return v } diff --git a/usrwg/router/util.go b/usrwg/router/util.go index 9822638..50ebeff 100644 --- a/usrwg/router/util.go +++ b/usrwg/router/util.go @@ -1,7 +1,7 @@ package router import ( - "log" + "fmt" "net/netip" "os/exec" ) @@ -49,7 +49,8 @@ func inet(p netip.Prefix) string { func cmd(args ...string) *exec.Cmd { if len(args) == 0 { - log.Fatalf("exec.Cmd(%#v) invalid; need argv[0]", args) + // We control this input, and without argv[0] we can't do anything anyways. + panic(fmt.Errorf("exec.Cmd(%#v) invalid; need at least 1 argument", args)) } return exec.Command(args[0], args[1:]...) } From 56551c45e4ec26f904455a5a59308fb52b5272ea Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 7 Mar 2025 14:40:52 +0100 Subject: [PATCH 62/82] Document many panics Closes #114 --- toversok/actors/a_mman.go | 1 + toversok/actors/a_sman.go | 1 + toversok/actors/a_sockrecv.go | 15 ++++++++------- toversok/actors/stage.go | 1 + toversok/actors/util.go | 2 ++ toversok/control_conn.go | 1 + toversok/engine.go | 10 +++++++--- types/control/server.go | 1 + types/dial/tcp.go | 1 + types/misc.go | 1 + types/msgcontrol/msg.go | 3 ++- types/msgsess/ping.go | 1 + types/stun/txid.go | 1 + usrwg/router/router_windows.go | 2 ++ usrwg/tun_windows.go | 1 + 15 files changed, 31 insertions(+), 11 deletions(-) diff --git a/toversok/actors/a_mman.go b/toversok/actors/a_mman.go index 5a3f74a..08f551c 100644 --- a/toversok/actors/a_mman.go +++ b/toversok/actors/a_mman.go @@ -45,6 +45,7 @@ func (s *Stage) makeMM() *MDNSManager { SweepMinTTL: 1 * time.Minute, }) if err != nil { + // memorystore does not return an error, so this is unexpected panic(err) } diff --git a/toversok/actors/a_sman.go b/toversok/actors/a_sman.go index 21c90f2..3307eaf 100644 --- a/toversok/actors/a_sman.go +++ b/toversok/actors/a_sman.go @@ -105,6 +105,7 @@ func (sm *SessionManager) Handle(msg msgactor.ActorMessage) { func (sm *SessionManager) Unpack(frameWithMagic []byte) (*msgsess.ClearMessage, error) { if string(frameWithMagic[:len(msgsess.Magic)]) != msgsess.Magic { + // We check these messages further up, so while this is a safety check, it shouldn't be triggered panic("Somehow received non-session message in unpack") } diff --git a/toversok/actors/a_sockrecv.go b/toversok/actors/a_sockrecv.go index 30fb1b2..b2089cd 100644 --- a/toversok/actors/a_sockrecv.go +++ b/toversok/actors/a_sockrecv.go @@ -3,7 +3,6 @@ package actors import ( "context" "errors" - "fmt" "log/slog" "net" "net/netip" @@ -62,7 +61,9 @@ func (r *SockRecv) Run() { err := r.Conn.SetReadDeadline(time.Now().Add(SockRecvReadTimeout)) if err != nil { - panic(fmt.Sprint("Error when setting read deadline:", err)) + L(r).Error("failed to set read deadline", "err", err) + r.ctxCan() + return } n, ap, err := r.Conn.ReadFromUDPAddrPort(buf) @@ -76,17 +77,17 @@ func (r *SockRecv) Run() { // unsure what to do here, as this might be a permanent error of the socket? // would this result in the closing of the channel? if so, wouldnt the corresponding outconn also die? // if so, then who detects the death of the actor and recreates it like that? - if context.Cause(r.ctx) != nil { + if r.ctx.Err() != nil { // we're closing anyways, just return return } - if errors.Is(err, net.ErrClosed) { - r.Cancel() - return + if !errors.Is(err, net.ErrClosed) { + L(r).Error("failed to read packet", "err", err) } - panic(err) + r.Cancel() + return } if n == 0 { diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index 329564a..8a3ff22 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -373,6 +373,7 @@ func (s *Stage) addConnLocked(peer key.NodePublic, udp types.UDPConn) { pi := s.peerInfo[peer] if pi == nil { + // We run this with the assumption that peerinfo has been given to us panic("expecting to have peer information at this point") } diff --git a/toversok/actors/util.go b/toversok/actors/util.go index 058d605..10907da 100644 --- a/toversok/actors/util.go +++ b/toversok/actors/util.go @@ -41,12 +41,14 @@ func L(a ifaces.Actor) *slog.Logger { func bail(c context.Context, v any) { maybeCcc := c.Value(types.CCC) if maybeCcc == nil { + // We add the CCC early in the engine's lifecycle, so this shouldn't happen. panic(fmt.Errorf("could not bail, cannot find ccc: %s", v)) } probablyCcc, ok := maybeCcc.(context.CancelCauseFunc) if !ok { + // Ditto, if we add it, we make sure its added correctly panic(fmt.Errorf("could not bail, ccc is not CancelCauseFunc: %s", v)) } diff --git a/toversok/control_conn.go b/toversok/control_conn.go index 1516cdb..bc3fd5b 100644 --- a/toversok/control_conn.go +++ b/toversok/control_conn.go @@ -375,6 +375,7 @@ func (rcs *ResumableControlSession) ExpectCallbacks() ifaces.ControlCallbacks { defer rcs.callbackLock.RUnlock() if rcs.callbacks == nil { + // Part of the function contract; if it doesnt exist, it'll blow up panic("expected callbacks to be ready at this stage") } diff --git a/toversok/engine.go b/toversok/engine.go index 3473bf8..3e2026c 100644 --- a/toversok/engine.go +++ b/toversok/engine.go @@ -206,8 +206,9 @@ func (e *Engine) installSession(allowLogon bool) error { }) if !(e.state.change(CreatingSession, Established) || e.state.change(NeedsLogin, Established)) { - e.ccc(errors.New("incorrect state transition")) - panic("incorrect state transition to established") + err = errors.New("incorrect state transition") + e.ccc(err) + return err } context.AfterFunc(e.sess.ctx, func() { @@ -382,7 +383,9 @@ func NewEngine( } else if state == Established { expiry, err := e.Observer().GetEstablishedState() if err != nil { - panic("should never happen") + // We are literally in the established state, we can get the GetEstablishedState + // There is one tiny window where it has flopped back, and so just ignore that if that is the case + return } if expiry != (time.Time{}) { slog.Info("established session with expiry", "expiry", expiry, "in", time.Until(expiry)) @@ -407,6 +410,7 @@ func (e *Engine) getExtConn() types.UDPConn { if e.extBind == nil || e.extBind.Closed { conn, err := e.bindExt() if err != nil { + // We expect the bindext to work, else we more or less just can't do anything panic(fmt.Sprintf("could not bind ext: %s", err)) } diff --git a/types/control/server.go b/types/control/server.go index 6f3129b..0440b6f 100644 --- a/types/control/server.go +++ b/types/control/server.go @@ -282,6 +282,7 @@ func randData() []byte { b := make([]byte, 32) _, err := rand.Read(b) if err != nil { + // We expect the system random to at least be accessible now panic(fmt.Errorf("could not read rand: %w", err)) } return b diff --git a/types/dial/tcp.go b/types/dial/tcp.go index 710c9f3..2c3b56c 100644 --- a/types/dial/tcp.go +++ b/types/dial/tcp.go @@ -34,6 +34,7 @@ func TLS(conn net.Conn, opts Opts) *tls.Conn { case opts.Domain != "": cfg.ServerName = opts.Domain default: + // We assume this is sane, else some upstream provider of the opt isn't proper with what it gives panic("TLS defined, but no domain provided") } diff --git a/types/misc.go b/types/misc.go index 6899a8c..3e569a0 100644 --- a/types/misc.go +++ b/types/misc.go @@ -91,6 +91,7 @@ func RandStringBytesMaskImprSrc(n int) string { b := make([]byte, (n+1)/2) // can be simplified to n/2 if n is always even if _, err := rand.Read(b); err != nil { + // We expect the randomizer to be available here panic(err) } diff --git a/types/msgcontrol/msg.go b/types/msgcontrol/msg.go index 85ce1be..47c8ce9 100644 --- a/types/msgcontrol/msg.go +++ b/types/msgcontrol/msg.go @@ -1,6 +1,7 @@ package msgcontrol import ( + "fmt" "net/netip" "time" @@ -100,7 +101,7 @@ func (r RetryStrategyType) Error() string { case RecreateSession: return "retry by recreating session" default: - panic("unknown retry strategy type") + return fmt.Sprintf("!!!unknown retry strategy type %d!!!", r) } } diff --git a/types/msgsess/ping.go b/types/msgsess/ping.go index 6ecda0a..33f99e8 100644 --- a/types/msgsess/ping.go +++ b/types/msgsess/ping.go @@ -13,6 +13,7 @@ type TxID [12]byte func NewTxID() TxID { var tx TxID if _, err := crand.Read(tx[:]); err != nil { + // We expect the randomiser to be available here panic(err) } return tx diff --git a/types/stun/txid.go b/types/stun/txid.go index 02d0ac3..2bf59ba 100644 --- a/types/stun/txid.go +++ b/types/stun/txid.go @@ -9,6 +9,7 @@ type TxID [12]byte func NewTxID() TxID { var tx TxID if _, err := crand.Read(tx[:]); err != nil { + // We expect the randomizer to be available here panic(err) } return tx diff --git a/usrwg/router/router_windows.go b/usrwg/router/router_windows.go index 09fa845..7b46286 100644 --- a/usrwg/router/router_windows.go +++ b/usrwg/router/router_windows.go @@ -571,6 +571,7 @@ func deltaNets(a, b []netip.Prefix) (add, del []netip.Prefix) { add = append(add, b[j]) j++ default: + // Literally unexpected, since we control the return of the function panic("unexpected compare result") } } @@ -705,6 +706,7 @@ func deltaRouteData(a, b []*routeData) (add, del []*routeData) { add = append(add, b[j]) j++ default: + // Literally unexpected, since we control the return of the function panic("unexpected compare result") } } diff --git a/usrwg/tun_windows.go b/usrwg/tun_windows.go index d1c37ad..1d09998 100644 --- a/usrwg/tun_windows.go +++ b/usrwg/tun_windows.go @@ -13,6 +13,7 @@ func init() { tun.WintunTunnelType = "ToverSok" guid, err := windows.GUIDFromString("{37217669-42da-4657-a55b-13375d328250}") if err != nil { + // We can create a GUID from a static string without error panic(err) } tun.WintunStaticRequestedGUID = &guid From e796d2c09f8ff401a31386ed189843d1cffcef0f Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 7 Mar 2025 14:49:20 +0100 Subject: [PATCH 63/82] Fix relay panic and remove stray WithoutCancel's Closes #128 --- toversok/actors/a_relay.go | 5 +++-- toversok/actors/stage.go | 3 --- toversok/control_conn.go | 8 ++------ types/control/client.go | 3 ++- types/relay/client.go | 3 ++- types/relay/relayhttp/http_client.go | 5 +++-- 6 files changed, 12 insertions(+), 15 deletions(-) diff --git a/toversok/actors/a_relay.go b/toversok/actors/a_relay.go index 5b15446..fa6e748 100644 --- a/toversok/actors/a_relay.go +++ b/toversok/actors/a_relay.go @@ -2,6 +2,7 @@ package actors import ( "context" + "errors" "fmt" "log/slog" "runtime" @@ -158,7 +159,7 @@ func (c *RestartableRelayConn) loop() error { case <-checker.C: if c.shouldIdle() { - c.client.Close() + c.client.Cancel(errors.New("should idle")) return nil } @@ -209,7 +210,7 @@ func (c *RestartableRelayConn) Update(info relay.Information) { // Close the client to trigger a reconnect if c.client != nil { - c.client.Close() + c.client.Cancel(errors.New("relay client exited")) } } diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index 8a3ff22..72fa57a 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -46,9 +46,6 @@ func MakeStage( wgIf *net.Interface, ) ifaces.Stage { - // FIXME ??? why the fuck did we ever decide on this - // ctx := context.WithoutCancel(pCtx) - if dialRelayFunc == nil { dialRelayFunc = relayhttp.Dial } diff --git a/toversok/control_conn.go b/toversok/control_conn.go index bc3fd5b..d04d6ad 100644 --- a/toversok/control_conn.go +++ b/toversok/control_conn.go @@ -75,8 +75,7 @@ type ResumableControlSession struct { func CreateControlSession(ctx context.Context, opts dial.Opts, controlKey key.ControlPublic, getPriv func() *key.NodePrivate, getSess func() *key.SessionPrivate, logon types.LogonCallback) (*ResumableControlSession, error) { rcsCtx, rcsCcc := context.WithCancelCause(ctx) - clientCtx := context.WithoutCancel(rcsCtx) - c, err := controlhttp.Dial(clientCtx, opts, getPriv, getSess, controlKey, nil, logon) + c, err := controlhttp.Dial(rcsCtx, opts, getPriv, getSess, controlKey, nil, logon) if err != nil { rcsCcc(err) return nil, fmt.Errorf("could not create control session: %w", err) @@ -144,7 +143,6 @@ func (rcs *ResumableControlSession) Run() { if types.IsContextDone(rcs.ctx) { slog.Info("control session ended, closing client") - rcs.client.Close() return } @@ -190,10 +188,8 @@ func (rcs *ResumableControlSession) Run() { return } - clientCtx := context.WithoutCancel(rcs.ctx) - client, err = controlhttp.Dial( - clientCtx, rcs.clientOpts, rcs.getPriv, rcs.getSess, rcs.controlKey, session, nil, + rcs.ctx, rcs.clientOpts, rcs.getPriv, rcs.getSess, rcs.controlKey, session, nil, ) r := msgcontrol.NoRetryStrategy diff --git a/types/control/client.go b/types/control/client.go index a174c06..7e29a75 100644 --- a/types/control/client.go +++ b/types/control/client.go @@ -53,6 +53,8 @@ func EstablishClient(parentCtx context.Context, mc types.MetaConn, brw *bufio.Re return nil, err } + context.AfterFunc(c.ctx, c.Close) + return c, nil } @@ -225,5 +227,4 @@ func (c *Client) Close() { func (c *Client) Cancel(err error) { c.ccc(err) - c.Close() } diff --git a/types/relay/client.go b/types/relay/client.go index 3098739..0e83bed 100644 --- a/types/relay/client.go +++ b/types/relay/client.go @@ -39,6 +39,7 @@ type Client interface { Err() error Close() + Cancel(error) } // HTTPClient is a Relay client that lives as long as its conn does @@ -307,7 +308,7 @@ func (c *HTTPClient) RunReceive() { defer func() { if v := recover(); v != nil { - c.ccc(fmt.Errorf("reader panicked: %s", v)) + c.Cancel(fmt.Errorf("reader panicked: %s", v)) } }() diff --git a/types/relay/relayhttp/http_client.go b/types/relay/relayhttp/http_client.go index 1501eaa..4ac11a1 100644 --- a/types/relay/relayhttp/http_client.go +++ b/types/relay/relayhttp/http_client.go @@ -36,9 +36,10 @@ func Dial(ctx context.Context, opts dial.Opts, getPriv func() *key.NodePrivate, } if !expectKey.IsZero() && c.RelayKey() != expectKey { - c.Close() + err = fmt.Errorf("relay key did not match expected key") + c.Cancel(err) - return nil, fmt.Errorf("relay key did not match expected key") + return nil, err } return c, nil From 09db984d36a7c36f0f3c7d0441ac453271f78f4f Mon Sep 17 00:00:00 2001 From: Henk Date: Fri, 7 Mar 2025 18:10:36 +0100 Subject: [PATCH 64/82] Add function that generates a graph to show the variance in measurements between repetitions --- test_suite/visualize_performance_tests.py | 97 ++++++++++++++++++++--- 1 file changed, 87 insertions(+), 10 deletions(-) diff --git a/test_suite/visualize_performance_tests.py b/test_suite/visualize_performance_tests.py index a50cf44..ac0e2b3 100644 --- a/test_suite/visualize_performance_tests.py +++ b/test_suite/visualize_performance_tests.py @@ -48,17 +48,30 @@ def test_iteration(): m = p.match(test_dir) test_var = m.group(1) + # Extract test variable values and corresponding measurements test_var_values, extracted_data = repetition_iteration(test_path, test_var) extracted_data = aggregate_repetitions(extracted_data) + # Create dictionary containing test variable info + test_var_dict = TEST_VARS[test_var] + test_var_dict["values"] = test_var_values + with open(f"{parent_path}/performance_test_data.json", 'w') as file: # Delete transform key from bitrate metric, since it is not JSON serializable del extracted_data["bitrate"]["transform"] - json.dump(extracted_data, file) + # Merge test variable info and measurements into one dictionary + data_dict = { + "test_var": test_var_dict, + "measurements": extracted_data + } + + json.dump(data_dict, file) for metric in extracted_data.keys(): - create_graph(test_var, test_var_values, metric, extracted_data, parent_path) + create_performance_graph(test_var, test_var_values, metric, extracted_data, parent_path) + + create_variance_grid(data_dict, parent_path) if n_tests > 0: plural = "s" if n_tests > 1 else "" @@ -151,7 +164,7 @@ def file_iteration(connection_type: str, connection_path: str, repetition_id: st # Sort data sorted_indices=np.argsort(test_var_values) - test_var_values = np.array(test_var_values)[sorted_indices] + test_var_values = list(np.array(test_var_values)[sorted_indices]) for metric in extracted_data.keys(): sorted_measurements = np.array(extracted_data[metric]["values"][repetition_id][connection_type])[sorted_indices] @@ -192,14 +205,16 @@ def aggregate_repetitions(extracted_data: dict) -> dict: return extracted_data -def create_graph(test_var: str, test_var_values: list[float], metric: str, extracted_data: dict, save_path: str): +# Given a dictionary containing the label and unit of a metric, returns a string to describe the metric on a graph axis +def axis_label(label_unit_dict: dict) -> str: + return f"{label_unit_dict["label"]} ({label_unit_dict["unit"]})" + +# Graph to illustrate the performance of eduP2P, possibly by comparing against WireGuard and/or a direct connection +def create_performance_graph(test_var: str, test_var_values: list[float], metric: str, extracted_data: dict, save_path: str): metric_data = extracted_data[metric] connection_measurements = metric_data["values"]["average"] - test_var_label = TEST_VARS[test_var]["label"] - test_var_unit = TEST_VARS[test_var]["unit"] metric_label = metric_data["label"] - metric_unit = metric_data["unit"] # Different line styles in case they overlap line_styles=["-", "--", ":"] @@ -212,13 +227,75 @@ def create_graph(test_var: str, test_var_values: list[float], metric: str, extra lw=line_widths[i] plt.plot(x, y, linestyle=ls, linewidth=lw, label=connection) - plt.xlabel(f"{test_var_label} ({test_var_unit})") - plt.ylabel(f"{metric_label} ({metric_unit})") + plt.xlabel(axis_label(TEST_VARS[test_var])) + plt.ylabel(axis_label(metric_data)) plt.title(f"{metric_label} for varying {test_var_label}") plt.ticklabel_format(useOffset=False) plt.legend() - + plt.tight_layout() plt.savefig(f"{save_path}/performance_test_{metric}.png") plt.clf() +# Create an * grid of plots showing the variance in measurements across repetitions for each metric and connection type +def create_variance_grid(data_dict: dict, save_path: str): + test_var_info = data_dict["test_var"] + test_var_values = test_var_info["values"] + + measurements = data_dict["measurements"] + metrics = list(measurements.keys()) + n_metrics = len(metrics) + reps_and_avg = measurements[metrics[0]]["values"] + + # This indicates that reps_and_avg = ["repetition1", "average"], so only 1 repetition is performed + if len(reps_and_avg) == 2: + return + + connections = list(reps_and_avg["average"].keys()) + n_connections = len(connections) + fig, ax = plt.subplots(n_metrics, n_connections) + + # Iterate over rows in the grid + for i, metric in enumerate(metrics): + metric_dict = measurements[metric] + create_variance_col(ax[i], i, test_var_values, metric_dict["values"]) + + # Y label is the same for each row, so we only set it on the first column to save space + ax[i][0].set_ylabel(axis_label(metric_dict)) + + # X label is the same for each subplot, so we only set it on the bottom row to save space + for j in range(n_connections): + ax[n_metrics-1][j].set_xlabel(axis_label(test_var_info)) + + # Display legend shared between the subplots and save the figure + handles, labels = ax[0][0].get_legend_handles_labels() + fig.legend(handles, labels, loc='upper center', bbox_to_anchor=(0.5, 1.03), ncol=len(reps_and_avg)//2) + + # Place suptitle higher to free up space for legend + fig.suptitle("Variance of measurements over multiple repetitions", y=1.06) + + subplot_size = 4 + fig.set_figheight(n_metrics * subplot_size) + fig.set_figwidth(n_connections * subplot_size) + fig.tight_layout() + fig.savefig(f"{save_path}/performance_test_variance.png", bbox_inches="tight") # bbox_inches prevents suptitle and legend from being cropped + +# Fill one column of the variance grid with graphs +def create_variance_col(ax: np.ndarray[plt.Axes], i: int, test_var_values: list[float], measurements: dict): + for k, repetition_dict in measurements.items(): + for j, conn in enumerate(repetition_dict.keys()): + conn_measurements = repetition_dict[conn] + + # Make line representing the average stand out from lines representing individual repetitions + if k == "average": + ax[j].plot(test_var_values, conn_measurements, label=k, linestyle="-", linewidth=3, color="black") + else: + ax[j].plot(test_var_values, conn_measurements, label=k, linestyle="--", linewidth=1.5) + + # Each connection type takes up a separate column, so put the connection type above the top subplots + if(i == 0): + ax[j].set_title(conn) + + # On the Y axis, use scientific notation for numbers outside the range [1e-3, 1e4] to prevent them from crossing into other subplots + ax[j].ticklabel_format(axis='y', style='sci', scilimits=(-3,4)) + test_iteration() \ No newline at end of file From b68649766dcbae26fdc939347213735b1a8ba5e6 Mon Sep 17 00:00:00 2001 From: Henk Date: Fri, 7 Mar 2025 18:56:40 +0100 Subject: [PATCH 65/82] Document new performance test variance graph, and show how it improves performance test reliability --- test_suite/README.md | 40 ++++++++++++++++-- .../performance_test_variance.png | Bin 0 -> 260438 bytes 2 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 test_suite/images/performance_tests/performance_test_variance.png diff --git a/test_suite/README.md b/test_suite/README.md index c6ab18e..da90c88 100644 --- a/test_suite/README.md +++ b/test_suite/README.md @@ -518,9 +518,11 @@ during the test. Using the Python script [visualize_performance_tests.py](visualize_performance_tests.py), these performance metrics are extracted from the json files, and graphs are automatically created that plot the independent variable on the X axis -against each performance metric on the Y axis. Some of these graphs are -shown in the [performance test results -section](#performance-test-results) +against each performance metric on the Y axis. Furthermore, if `-r` has +a value greater than one, another graph is created to show the variance +of the measurements across the different repetitions of the test. Some +of these graphs are shown in the [performance test results +section](#performance-test-results). ## Integration Tests @@ -1232,7 +1234,7 @@ further, however: Command used: - run_system_test -k delay -v 0,1,2,3 -d 3 -b both TS_PASS_DIRECT router1-router2 : : + run_system_test -k delay -v 0,1,2,3 -d 3 -b both -r 3 TS_PASS_DIRECT router1-router2 : : ![](./images/performance_tests/x_ow_delay_y_http_latency.png) @@ -1254,6 +1256,36 @@ eduP2P into account, we can conclude that increasing the one-way delay does not increase eduP2P’s HTTP latency more than the HTTP latency of WireGuard or the direct connection. +### Consistency of results + +The results of the performance tests may be affected by external +factors, such as other processes running on the same machine. Therefore, +the performance test results may be inconsistent: two runs of the same +performance tests may have different results. + +To improve the reliability of the performance tests, the `-r` option has +been introduced to repeat the tests and aggregate their results. The +performance tests also generate a graph illustrating the variance over +multiple repetitions. Below, this graph is shown for the performance +test described in [bitrate performance test with external +WireGuard](./README.md#results-with-varying-bitrate-and-peers-using-external-wireguard). + +![](./images/performance_tests/performance_test_variance.png) + +This graph shows that there is quite a lot of variance between certain +measurements. For example, for the eduP2P bitrate measurement (top-left) +and WireGuard packet loss measurement (bottom-center), the absolute +difference between the minimum and maximum measurements grows quite +large as the target bitrate increases. + +Some of the measurements also contain outliers, such as the WireGuard +jitter measurement (center) with large spikes in the third and fifth +repetition. + +As seen from the black lines in the graphs, calculating the average over +the repetitions improves the reliability of the results where the +variance is large or outliers are present. + ## Integration Test Results Currently, the integration tests focus on the lowest level components diff --git a/test_suite/images/performance_tests/performance_test_variance.png b/test_suite/images/performance_tests/performance_test_variance.png new file mode 100644 index 0000000000000000000000000000000000000000..92c582bbaef77abc026edb2e7ae24aa4495fff5b GIT binary patch literal 260438 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 z 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

W_K^H8p}fA`=#Tml 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 Date: Fri, 7 Mar 2025 19:08:25 +0100 Subject: [PATCH 66/82] Add update below funding attribution about finished repeated performance test feature --- test_suite/README.md | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/test_suite/README.md b/test_suite/README.md index da90c88..17614b2 100644 --- a/test_suite/README.md +++ b/test_suite/README.md @@ -1429,17 +1429,32 @@ Conservancy](https://commonsconservancy.org/). The test suite features that have been made possible thanks to this funding are described below. -### Simulating network delay (finished 04-03-2025) +### Simulating network delay (finished March 4, 2025) This feature makes it possible to add artificial network delay in the system and performance tests. The feature can be used with the system tests by calling -`system_tests.sh` with the option `-d `. +`system_tests.sh` with the option `-d `. In the performance +tests, this artificial delay can be configured as the independent test +variable. More details are given in the [performance test +documentation](./README.md#performance-tests). -In the performance tests, this artificial delay can be configured as the -independent test variable. More details are given in the [performance -test documentation](./README.md#performance-tests). Furthermore, the -effect of the artificial delay on the eduP2P network performance is -reported in the [performance test +Furthermore, the effect of the artificial delay on the eduP2P network +performance is reported in the [performance test results](./README.md#results-with-varying-one-way-delay). + +### Repeated performance tests (finished March 7, 2025) + +This feature adds the option to repeat the same performance test +multiple times and aggregate the results of each repetition by taking +their average. + +The option is configured with the `-r` flag of the performance tests. +See the [performance test documentation](./README.md#performance-tests) +for details on how to configure and run a performance test. + +In the [performance test results](./README.md#consistency-of-results), +the variance between different repetitions of the same performance test +is analysed. This analysis shows that aggregating the results can +improve their reliability. From f1a24761507e0d2f49d0cd79257bb1f11af59cc6 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 10 Mar 2025 09:16:05 +0100 Subject: [PATCH 67/82] Remove go-test.yml CI, as IntegrationTest already exists. Closes #134 --- .github/workflows/go-test.yml | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 .github/workflows/go-test.yml diff --git a/.github/workflows/go-test.yml b/.github/workflows/go-test.yml deleted file mode 100644 index cd3f070..0000000 --- a/.github/workflows/go-test.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: go test -on: [push] - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - name: Install dependencies - run: go get ./... - - name: Build - run: go build -v ./... - - name: Test with the Go CLI - run: go test -v ./... \ No newline at end of file From 49d9b446d933ed4940328308bf4887bcb5c4d231 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 10 Mar 2025 09:27:01 +0100 Subject: [PATCH 68/82] actors: Remove extra cancels, put cancel on defer, rearrange CheckOrMark to stop extra cancels --- toversok/actors/a_conn.go | 30 ++++++++++++++---------------- toversok/actors/a_direct.go | 24 ++++++++++++------------ toversok/actors/a_eman.go | 8 ++------ toversok/actors/a_mman.go | 14 ++++++++++++++ toversok/actors/a_relay.go | 26 ++++++++++++++------------ toversok/actors/a_sman.go | 12 ++++++------ toversok/actors/a_sockrecv.go | 14 ++++++-------- toversok/actors/a_tman.go | 12 ++++++------ 8 files changed, 74 insertions(+), 66 deletions(-) diff --git a/toversok/actors/a_conn.go b/toversok/actors/a_conn.go index 11bf2c4..e92cb2e 100644 --- a/toversok/actors/a_conn.go +++ b/toversok/actors/a_conn.go @@ -56,19 +56,19 @@ func MakeOutConn(udp types.UDPConn, peer key.NodePublic, homeRelay int64, s *Sta } func (oc *OutConn) Run() { + if !oc.running.CheckOrMark() { + L(oc).Warn("tried to run agent, while already running") + return + } + + defer oc.Cancel() defer func() { if v := recover(); v != nil { L(oc).Error("panicked", "panic", v, "stack", string(debug.Stack())) - oc.Cancel() bail(oc.ctx, v) } }() - if !oc.running.CheckOrMark() { - L(oc).Warn("tried to run agent, while already running") - return - } - go oc.sock.Run() for { @@ -76,7 +76,7 @@ func (oc *OutConn) Run() { case <-oc.ctx.Done(): return case <-oc.sock.ctx.Done(): - oc.Cancel() + return case <-oc.activityTimer.C: oc.UnBump() case msg := <-oc.inbox: @@ -98,8 +98,7 @@ func (oc *OutConn) Run() { // sock closed, the peer is dead // TODO: // trigger some kind of healing logic elsewhere? - oc.Cancel() - continue + return } if oc.useRelay { @@ -217,19 +216,19 @@ func MakeInConn(udp types.UDPConn, peer key.NodePublic, s *Stage) *InConn { } func (ic *InConn) Run() { + if !ic.running.CheckOrMark() { + L(ic).Warn("tried to run agent, while already running") + return + } + + defer ic.Cancel() defer func() { if v := recover(); v != nil { L(ic).Error("panicked", "panic", v, "stack", string(debug.Stack())) - ic.Cancel() bail(ic.ctx, v) } }() - if !ic.running.CheckOrMark() { - L(ic).Warn("tried to run agent, while already running") - return - } - for { select { case <-ic.ctx.Done(): @@ -240,7 +239,6 @@ func (ic *InConn) Run() { n, err := ic.udp.Write(frame) if err != nil { if errors.Is(err, net.ErrClosed) { - ic.Cancel() return } // TODO failsafe logic diff --git a/toversok/actors/a_direct.go b/toversok/actors/a_direct.go index e171777..d7b02ad 100644 --- a/toversok/actors/a_direct.go +++ b/toversok/actors/a_direct.go @@ -40,19 +40,19 @@ func (s *Stage) makeDM(udpSocket types.UDPConn) *DirectManager { } func (dm *DirectManager) Run() { + if !dm.running.CheckOrMark() { + L(dm).Warn("tried to run agent, while already running") + return + } + + defer dm.Cancel() defer func() { if v := recover(); v != nil { L(dm).Error("panicked", "panic", v, "stack", string(debug.Stack())) - dm.Cancel() bail(dm.ctx, v) } }() - if !dm.running.CheckOrMark() { - L(dm).Warn("tried to run agent, while already running") - return - } - go dm.sock.Run() runtime.LockOSThread() @@ -130,19 +130,19 @@ func (dr *DirectRouter) Push(frame ifaces.DirectedPeerFrame) { } func (dr *DirectRouter) Run() { + if !dr.running.CheckOrMark() { + L(dr).Warn("tried to run agent, while already running") + return + } + + defer dr.Cancel() defer func() { if v := recover(); v != nil { L(dr).Error("panicked", "panic", v, "stack", string(debug.Stack())) - dr.Cancel() bail(dr.ctx, v) } }() - if !dr.running.CheckOrMark() { - L(dr).Warn("tried to run agent, while already running") - return - } - runtime.LockOSThread() for { diff --git a/toversok/actors/a_eman.go b/toversok/actors/a_eman.go index fc99c21..5b150b7 100644 --- a/toversok/actors/a_eman.go +++ b/toversok/actors/a_eman.go @@ -66,19 +66,15 @@ type stunResponse struct { // - UPnP? Other stuff? func (em *EndpointManager) Run() { + + defer em.Cancel() defer func() { if v := recover(); v != nil { L(em).Error("panicked", "panic", v, "stack", string(debug.Stack())) - em.Cancel() bail(em.ctx, v) } }() - if !em.running.CheckOrMark() { - L(em).Warn("tried to run agent, while already running") - return - } - for { select { case <-em.ctx.Done(): diff --git a/toversok/actors/a_mman.go b/toversok/actors/a_mman.go index 08f551c..4e139ab 100644 --- a/toversok/actors/a_mman.go +++ b/toversok/actors/a_mman.go @@ -8,6 +8,7 @@ import ( "net" "net/netip" "runtime" + "runtime/debug" "slices" "time" @@ -140,6 +141,19 @@ func dataToB64Hash(b []byte) string { } func (mm *MDNSManager) Run() { + if !mm.running.CheckOrMark() { + L(mm).Warn("tried to run agent, while already running") + return + } + + defer mm.Cancel() + defer func() { + if v := recover(); v != nil { + L(mm).Error("panicked", "panic", v, "stack", string(debug.Stack())) + bail(mm.ctx, v) + } + }() + if !mm.working { mm.deadRun() return diff --git a/toversok/actors/a_relay.go b/toversok/actors/a_relay.go index fa6e748..dc9b8bf 100644 --- a/toversok/actors/a_relay.go +++ b/toversok/actors/a_relay.go @@ -54,6 +54,8 @@ func (c *RestartableRelayConn) Poke() { } func (c *RestartableRelayConn) Run() { + defer c.Cancel() + for { if c.shouldIdle() { select { @@ -278,19 +280,19 @@ func (s *Stage) makeRM() *RelayManager { } func (rm *RelayManager) Run() { + if !rm.running.CheckOrMark() { + L(rm).Warn("tried to run agent, while already running") + return + } + + defer rm.Cancel() defer func() { if v := recover(); v != nil { L(rm).Error("panicked", "panic", v, "stack", string(debug.Stack())) - rm.Cancel() bail(rm.ctx, v) } }() - if !rm.running.CheckOrMark() { - L(rm).Warn("tried to run agent, while already running") - return - } - runtime.LockOSThread() for { @@ -453,19 +455,19 @@ func (rr *RelayRouter) Push(frame ifaces.RelayedPeerFrame) { } func (rr *RelayRouter) Run() { + if !rr.running.CheckOrMark() { + L(rr).Warn("tried to run agent, while already running") + return + } + + defer rr.Cancel() defer func() { if v := recover(); v != nil { L(rr).Warn("panicked", "error", v, "stack", string(debug.Stack())) - rr.Cancel() bail(rr.ctx, v) } }() - if !rr.running.CheckOrMark() { - L(rr).Warn("tried to run agent, while already running") - return - } - runtime.LockOSThread() for { diff --git a/toversok/actors/a_sman.go b/toversok/actors/a_sman.go index 3307eaf..c8acdaf 100644 --- a/toversok/actors/a_sman.go +++ b/toversok/actors/a_sman.go @@ -37,19 +37,19 @@ func (s *Stage) makeSM(priv func() *key.SessionPrivate) *SessionManager { } func (sm *SessionManager) Run() { + if !sm.running.CheckOrMark() { + L(sm).Warn("tried to run agent, while already running") + return + } + + defer sm.Cancel() defer func() { if v := recover(); v != nil { L(sm).Error("panicked", "panic", v, "stack", string(debug.Stack())) - sm.Cancel() bail(sm.ctx, v) } }() - if !sm.running.CheckOrMark() { - L(sm).Warn("tried to run agent, while already running") - return - } - for { select { case <-sm.ctx.Done(): diff --git a/toversok/actors/a_sockrecv.go b/toversok/actors/a_sockrecv.go index b2089cd..374da4a 100644 --- a/toversok/actors/a_sockrecv.go +++ b/toversok/actors/a_sockrecv.go @@ -39,19 +39,19 @@ func MakeSockRecv(ctx context.Context, udp types.UDPConn) *SockRecv { } func (r *SockRecv) Run() { + if !r.running.CheckOrMark() { + L(r).Warn("tried to run agent, while already running") + return + } + + defer r.Cancel() defer func() { if v := recover(); v != nil { L(r).Error("panicked", "err", v, "stack", string(debug.Stack())) - r.Cancel() bail(r.ctx, v) } }() - if !r.running.CheckOrMark() { - L(r).Warn("tried to run agent, while already running") - return - } - buf := make([]byte, 1<<16) for { @@ -62,7 +62,6 @@ func (r *SockRecv) Run() { err := r.Conn.SetReadDeadline(time.Now().Add(SockRecvReadTimeout)) if err != nil { L(r).Error("failed to set read deadline", "err", err) - r.ctxCan() return } @@ -86,7 +85,6 @@ func (r *SockRecv) Run() { L(r).Error("failed to read packet", "err", err) } - r.Cancel() return } diff --git a/toversok/actors/a_tman.go b/toversok/actors/a_tman.go index 66e558b..29c4f2d 100644 --- a/toversok/actors/a_tman.go +++ b/toversok/actors/a_tman.go @@ -51,19 +51,19 @@ func (s *Stage) makeTM() *TrafficManager { } func (tm *TrafficManager) Run() { + if !tm.running.CheckOrMark() { + L(tm).Warn("tried to run agent, while already running") + return + } + + defer tm.Cancel() defer func() { if v := recover(); v != nil { L(tm).Error("panicked", "error", v, "stack", string(debug.Stack())) - tm.Cancel() bail(tm.ctx, v) } }() - if !tm.running.CheckOrMark() { - L(tm).Warn("tried to run agent, while already running") - return - } - for { select { From 6b0caeaac5e76cf815a9705424a094bf4cd8111d Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 10 Mar 2025 11:18:21 +0100 Subject: [PATCH 69/82] praise the linter --- toversok/actors/a_eman.go | 1 - 1 file changed, 1 deletion(-) diff --git a/toversok/actors/a_eman.go b/toversok/actors/a_eman.go index 5b150b7..a6d0963 100644 --- a/toversok/actors/a_eman.go +++ b/toversok/actors/a_eman.go @@ -66,7 +66,6 @@ type stunResponse struct { // - UPnP? Other stuff? func (em *EndpointManager) Run() { - defer em.Cancel() defer func() { if v := recover(); v != nil { From be8eac0f6a0ef861104de14bc6d0802ab340e0e2 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 13 Mar 2025 14:11:35 +0100 Subject: [PATCH 70/82] Add bit of documentation regarding mDNS, add a TODO which was realised while reading RFC. --- docs/mdns.md | 29 +++++++++++++++++++++++++++++ toversok/actors/a_mman.go | 15 +++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 docs/mdns.md diff --git a/docs/mdns.md b/docs/mdns.md new file mode 100644 index 0000000..3c14362 --- /dev/null +++ b/docs/mdns.md @@ -0,0 +1,29 @@ +# mDNS Notes + +[mDNS](https://en.wikipedia.org/wiki/Multicast_DNS) (multicast DNS) is defined in +[RFC 6763](https://datatracker.ietf.org/doc/html/rfc6762) as, essentially, UDP DNS packets sent to broadcast addresses +`224.0.0.251` and `FF02::FB` on port `5353`. + +On top of that, a new `UNICAST-RESPONSE` (`"QU"`) bit is added to the query section, which can be parsed as `QCLASS` +`2^15`, and a `CACHE-FLUSH` bit on every resource (answer/additional/authority) record, which can be parsed as `RRCLASS` +`2^15`. + +Exact implementation differs per operating system, but as a rule of thumb; + +- mDNS (like most broadcast packets) aren't sent over `PPP` (point to point) classes of networks, which Wireguard is. +- Linux needs an additional system component to enable mDNS, such as "Avahi". +- MacOS, Linux, and Windows have differently covering implementations; + f.e. Windows doesn't allow unicast queries, while macOS does. +- While mDNS works via loopback, some operating systems have quirks with how they work, and only fully work mDNS via " + regular" LAN interfaces. + +## Intercepting mDNS + +Because of the above limitations (no mDNS on PPP, etc.), we cannot intercept mDNS packets via TUN (level 3, IP), +and would have to listen to them on the regular interfaces, by listening on port `5353` (and fight with the system +listener to have it share its port), +grab mDNS packets, filter them (to prevent noise from the local LAN), transform them (to point to the right IP address +over the Wireguard interface), send them over to interested parties, and then inject them. + +This essentially makes mDNS packets get wiretapped, and "appear out of thin air" at the recipient, +which should tie it together. \ No newline at end of file diff --git a/toversok/actors/a_mman.go b/toversok/actors/a_mman.go index 4e139ab..bcdae33 100644 --- a/toversok/actors/a_mman.go +++ b/toversok/actors/a_mman.go @@ -321,6 +321,21 @@ func (mm *MDNSManager) processMDNS(pkt []byte, local bool) []byte { // f.e. avahi doesn't properly work if the questions section is filled out, so we need to process that. // // The likes of Apple's mDNSResponder haven't gotten this above message, so we need to check for this. + // + // TODO this may be because we're regarding it via unicast DNS, and then this is the fallback described in + // section 6.7? + // If the source UDP port in a received Multicast DNS query is not port + // 5353, this indicates that the querier originating the query is a + // simple resolver such as described in Section 5.1, "One-Shot Multicast + // DNS Queries", which does not fully implement all of Multicast DNS. + // In this case, the Multicast DNS responder MUST send a UDP response + // directly back to the querier, via unicast, to the query packet's + // source IP address and port. This unicast response MUST be a + // conventional unicast response as would be generated by a conventional + // Unicast DNS server; for example, it MUST repeat the query ID and the + // question given in the query message. In addition, the cache-flush + // bit described in Section 10.2, "Announcements to Flush Outdated Cache + // Entries", MUST NOT be set in legacy unicast responses. if len(msg.Questions) != 0 { msg.Questions = []dnsmessage.Question{} dirty = true From 0acee9d9634211703b09acd85fa0123bc7d60547 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Fri, 14 Mar 2025 09:52:18 +0100 Subject: [PATCH 71/82] Explicitly add keepalive of 10 seconds to TCP connections (relay and control) Fixes #139 --- types/dial/tcp.go | 1 + 1 file changed, 1 insertion(+) diff --git a/types/dial/tcp.go b/types/dial/tcp.go index 2c3b56c..e0efaed 100644 --- a/types/dial/tcp.go +++ b/types/dial/tcp.go @@ -120,6 +120,7 @@ func dialOneTCP(ctx context.Context, ap netip.AddrPort) (net.Conn, error) { var d net.Dialer d.LocalAddr = nil + d.KeepAlive = time.Second * 10 return d.DialContext(ctx, "tcp", ap.String()) } From e294d60ae979b91d18772e4cff86df609a8fed28 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Mon, 17 Mar 2025 09:39:07 +0100 Subject: [PATCH 72/82] Add keepalive to Relay and Control server --- types/dial/http_server.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/types/dial/http_server.go b/types/dial/http_server.go index 0994044..cba1edf 100644 --- a/types/dial/http_server.go +++ b/types/dial/http_server.go @@ -5,9 +5,11 @@ import ( "context" "fmt" "log/slog" + "net" "net/http" "net/netip" "strings" + "time" "github.com/edup2p/common/types" ) @@ -42,6 +44,18 @@ func HTTPHandler(s ProtocolServer, proto string) http.Handler { return } + if tcpConn, ok := netConn.(*net.TCPConn); ok { + if err := tcpConn.SetKeepAlive(true); err != nil { + s.Logger().Warn("set keep alive failed", "error", err, "peer", r.RemoteAddr) + } + + if err := tcpConn.SetKeepAlivePeriod(11 * time.Second); err != nil { + s.Logger().Warn("set keep alive period failed", "error", err, "peer", r.RemoteAddr) + } + } else { + s.Logger().Warn("could not get *net.TCPConn, to set keepalive", "peer", r.RemoteAddr) + } + defer func() { if err := netConn.Close(); err != nil { slog.Error("error when closing netconn", "err", err) From c1afd66e69f1bd2df5a8a42cd54549a2b34f61e1 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 20 Mar 2025 12:39:19 +0100 Subject: [PATCH 73/82] Fix ChannelConn close panic --- usrwg/channel_conn.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/usrwg/channel_conn.go b/usrwg/channel_conn.go index ece9500..89581df 100644 --- a/usrwg/channel_conn.go +++ b/usrwg/channel_conn.go @@ -4,6 +4,7 @@ import ( "context" "net" "net/netip" + "sync" "time" ) @@ -17,6 +18,8 @@ type ChannelConn struct { // Packets written by the frontend outgoing chan []byte + doClose sync.Once + currentReadDeadline time.Time } @@ -24,9 +27,8 @@ const ChannelConnBufferSize = 16 func makeChannelConn() *ChannelConn { return &ChannelConn{ - incoming: make(chan []byte, ChannelConnBufferSize), - outgoing: make(chan []byte, ChannelConnBufferSize), - currentReadDeadline: time.Time{}, + incoming: make(chan []byte, ChannelConnBufferSize), + outgoing: make(chan []byte, ChannelConnBufferSize), } } @@ -69,10 +71,10 @@ func (cc *ChannelConn) WriteToUDPAddrPort(_ []byte, _ netip.AddrPort) (int, erro } func (cc *ChannelConn) Close() error { - // TODO boolean to check if is already closed? - - close(cc.outgoing) - close(cc.incoming) + cc.doClose.Do(func() { + close(cc.outgoing) + close(cc.incoming) + }) return nil } From f23f27ffa468e6788ba3dff72c385fca489d2422 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 20 Mar 2025 12:39:27 +0100 Subject: [PATCH 74/82] Fix windows compile issue --- usrwg/router/router_windows.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usrwg/router/router_windows.go b/usrwg/router/router_windows.go index 7b46286..cd411e8 100644 --- a/usrwg/router/router_windows.go +++ b/usrwg/router/router_windows.go @@ -43,7 +43,7 @@ func isWindowsService() bool { v, err := svc.IsWindowsService() if err != nil { // Expect that we can at least poke the local windows service, else we're in trouble. - panic("svc.IsWindowsService failed:", err) + panic(fmt.Errorf("svc.IsWindowsService failed: %w", err)) } return v } From 4df89ea15bcb8c34044e5cc12df2cfbc8ea95582 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 20 Mar 2025 12:39:44 +0100 Subject: [PATCH 75/82] Add mdns_test program --- cmd/mdns_test/main.go | 427 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) create mode 100644 cmd/mdns_test/main.go diff --git a/cmd/mdns_test/main.go b/cmd/mdns_test/main.go new file mode 100644 index 0000000..19a8a4b --- /dev/null +++ b/cmd/mdns_test/main.go @@ -0,0 +1,427 @@ +package main + +import ( + "errors" + "fmt" + "log" + "log/slog" + "net" + "net/netip" + "os" + "sync" + "syscall" + "time" + + "github.com/edup2p/common/types" + "golang.org/x/net/dns/dnsmessage" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +var multicastIface string + +func init() { + ifaces, err := net.Interfaces() + if err != nil { + panic(fmt.Errorf("could not list network interfaces: %w", err)) + } + + for _, iface := range ifaces { + if iface.Flags&net.FlagUp != 0 && iface.Flags&net.FlagLoopback != 0 { + multicastIface = iface.Name + } + } +} + +//nolint:unused +var ( + MDNSPort uint16 = 5353 + ip4MDNSBroadcastBare = netip.MustParseAddr("224.0.0.251") + ip6MDNSBroadcastBare = netip.MustParseAddr("ff02::fb") + + ip4MDNSUnspecifiedAP = netip.AddrPortFrom(netip.IPv4Unspecified(), MDNSPort) + ip6MDNSUnspecifiedAP = netip.AddrPortFrom(netip.IPv6Unspecified(), MDNSPort) + + ip4MDNSBroadcastAP = netip.AddrPortFrom(ip4MDNSBroadcastBare, MDNSPort) + ip6MDNSBroadcastAP = netip.AddrPortFrom(ip6MDNSBroadcastBare, MDNSPort) + + ip4MDNSLoopBackAP = netip.AddrPortFrom(netip.MustParseAddr("127.0.0.1"), MDNSPort) + ip4MDNSLoopBackAPAlt = netip.AddrPortFrom(netip.MustParseAddr("127.0.0.2"), MDNSPort) + ip6MDNSLoopBackAP = netip.AddrPortFrom(netip.IPv6Loopback(), MDNSPort) + ip6MDNSLoopBackAPAlt = netip.AddrPortFrom(netip.MustParseAddr("::2"), MDNSPort) +) + +const bit15 = 2 << 14 + +func main() { + if len(os.Args) < 2 { + println("Usage: mdns_test <.local name>") + os.Exit(1) + } + + name := os.Args[1] + ".local." + + dnsName, err := dnsmessage.NewName(name) + if err != nil { + panic(fmt.Errorf("failed to make DNS name: %w", err)) + } + allServicesName := dnsmessage.MustNewName("_services._dns-sd._udp.local.") + + nameQM := dnsmessage.Question{ + Name: dnsName, + Type: dnsmessage.TypeA, + Class: dnsmessage.ClassINET, + } + + servicesQM := dnsmessage.Question{ + Name: allServicesName, + Type: dnsmessage.TypePTR, + Class: dnsmessage.ClassINET, + } + + ml4, p4, err := makeIPv4MDNSListener() + if err != nil { + panic(fmt.Errorf("failed to make ipv4 mdns listener: %w", err)) + } + defer ml4.Close() + + ml6, p6, err := makeIPv6MDNSListener() + if err != nil { + panic(fmt.Errorf("failed to make ipv6 mdns listener: %w", err)) + } + defer ml6.Close() + + u4, err := net.DialUDP("udp4", nil, net.UDPAddrFromAddrPort(ip4MDNSLoopBackAP)) + if err != nil { + panic(fmt.Errorf("failed to make ipv4 unicast listener: %w", err)) + } + defer u4.Close() + + u6, err := net.DialUDP("udp6", nil, net.UDPAddrFromAddrPort(ip6MDNSLoopBackAP)) + if err != nil { + panic(fmt.Errorf("failed to make ipv6 unicast listener: %w", err)) + } + defer u6.Close() + + var respMu sync.Mutex + var responses []*dnsmessage.Message + + appendResponse := func(msg *dnsmessage.Message) { + respMu.Lock() + defer respMu.Unlock() + responses = append(responses, msg) + } + + go func() { + buf := make([]byte, 1<<16) + + for { + n, cm, ap, err := p6.ReadFrom(buf) + if err != nil { + panic(fmt.Errorf("failed to read from ipv6 mdns listener: %w", err)) + } + + var dst net.IP + + if cm != nil && cm.Dst != nil { + dst = cm.Dst + } + + slog.Info("received ipv6 packet", "from", ap.String(), "len", n, "dst", dst) + + msg := new(dnsmessage.Message) + + if err := msg.Unpack(buf[:n]); err != nil { + slog.Error("could not unpack ipv6 packet into mdns", "error", err) + continue + } + + appendResponse(msg) + } + }() + + go func() { + buf := make([]byte, 1<<16) + + for { + n, cm, ap, err := p4.ReadFrom(buf) + if err != nil { + panic(fmt.Errorf("failed to read from ipv4 mdns listener: %w", err)) + } + + var dst net.IP + + if cm != nil && cm.Dst != nil { + dst = cm.Dst + } + + slog.Info("received ipv4 packet", "from", ap.String(), "len", n, "dst", dst) + + msg := new(dnsmessage.Message) + + if err := msg.Unpack(buf[:n]); err != nil { + slog.Error("could not unpack ipv4 packet into mdns", "error", err) + continue + } + + appendResponse(msg) + } + }() + + listener := func(name string, conn *net.UDPConn) { + buf := make([]byte, 1<<16) + + for { + n, ap, err := conn.ReadFromUDPAddrPort(buf) + if err != nil { + panic(fmt.Errorf(name+": failed to read: %w", err)) + } + + slog.Info(name+": received packet", "from", ap.String(), "len", n) + + msg := new(dnsmessage.Message) + + if err := msg.Unpack(buf[:n]); err != nil { + slog.Error(name+": could not unpack packet into mdns", "error", err) + continue + } + + appendResponse(msg) + } + } + + go listener("uni4", u4) + go listener("uni6", u6) + + nameQU := nameQM + // unicast-response + nameQU.Class |= bit15 + + servicesQU := servicesQM + servicesQU.Class |= bit15 + + questions := []dnsmessage.Question{ + // nameQM, + // nameQU, + servicesQM, + servicesQU, + } + + var queries []*dnsmessage.Message + + for _, q := range questions { + queries = append(queries, makeQuery(q)) + } + + type writeTo struct { + conn types.UDPConn + name string + to *netip.AddrPort + } + + toWrite := []writeTo{ + {conn: ml6, to: &ip6MDNSBroadcastAP, name: "ml6bc"}, + {conn: ml4, to: &ip4MDNSBroadcastAP, name: "ml4bc"}, + {conn: ml6, to: &ip6MDNSLoopBackAP, name: "ml6lo"}, + {conn: ml4, to: &ip4MDNSLoopBackAP, name: "ml4lo"}, + {conn: u4, name: "u4"}, + {conn: u6, name: "u6"}, + } + + qna := make(map[*dnsmessage.Message]map[string][]*dnsmessage.Message) + + for _, q := range queries { + current := make(map[string][]*dnsmessage.Message) + qna[q] = current + + for _, w := range toWrite { + doWrite(w.conn, w.name, w.to, q) + + time.Sleep(1 * time.Second) + + respMu.Lock() + current[w.name] = responses + responses = nil + respMu.Unlock() + } + } + + processQNA(qna) +} + +func processQNA(m map[*dnsmessage.Message]map[string][]*dnsmessage.Message) { + println("\n\n\n") + slog.Info("Printing QNA result") + + for query, result := range m { + println("\n") + slog.Info("Printing for query") + debugMDNS(query) + println() + + for name, responses := range result { + slog.Info("Printing responses for name", "name", name) + + for _, msg := range responses { + debugMDNS(msg) + } + + println() + } + } +} + +func doWrite(c types.UDPConn, name string, to *netip.AddrPort, msg *dnsmessage.Message) { + q, err := msg.Pack() + if err != nil { + log.Fatal(fmt.Errorf("%s: could not pack message: %w", name, err)) + } + + if to == nil { + if _, err := c.Write(q); err != nil { + log.Println(fmt.Errorf("%s: failed to write query: %w", name, err)) + } + } else { + if _, err := c.WriteToUDPAddrPort(q, *to); err != nil { + log.Println(fmt.Errorf("%s: failed to write query to addrport: %w", name, err)) + } + } +} + +func makeQuery(q dnsmessage.Question) *dnsmessage.Message { + return &dnsmessage.Message{ + Header: dnsmessage.Header{}, + Questions: []dnsmessage.Question{q}, + } +} + +func makeIPv4MDNSListener() (types.UDPConn, *ipv4.PacketConn, error) { + ua := net.UDPAddrFromAddrPort(ip4MDNSBroadcastAP) + + conn, err := net.ListenUDP("udp4", ua) + if err != nil { + return nil, nil, fmt.Errorf("ListenUDP error: %w", err) + } + + p4 := ipv4.NewPacketConn(conn) + + ift, err := net.Interfaces() + if err != nil { + return nil, nil, fmt.Errorf("cannot get interfaces: %w", err) + } + for _, ifi := range ift { + if ifi.Flags&net.FlagUp != 0 && ifi.Flags&net.FlagPointToPoint == 0 { + if err := p4.JoinGroup(&ifi, &net.UDPAddr{IP: ip4MDNSBroadcastBare.AsSlice()}); err != nil && !errors.Is(err, syscall.EAFNOSUPPORT) { + slog.Warn("p4 multicast JoinGroup failed", "err", err, "iface", ifi.Name) + } + } + } + + if loop, err := p4.MulticastLoopback(); err == nil { + if !loop { + if err := p4.SetMulticastLoopback(true); err != nil { + return nil, nil, fmt.Errorf("cannot set multicast loopback: %w", err) + } + slog.Info("Multicast Loopback enabled") + } else { + slog.Info("Multicast Loopback was enabled") + } + } else { + return nil, nil, fmt.Errorf("cannot get MulticastLoopback: %w", err) + } + + ifi, err := net.InterfaceByName(multicastIface) + if err != nil { + panic(err) + } + + if err := p4.SetMulticastInterface(ifi); err != nil { + return nil, nil, fmt.Errorf("cannot set multicast interface: %w", err) + } + + if err := p4.SetTTL(255); err != nil { + return nil, nil, fmt.Errorf("cannot set TTL: %w", err) + } + if err := p4.SetMulticastTTL(255); err != nil { + return nil, nil, fmt.Errorf("cannot set Multicast TTL: %w", err) + } + + if err = p4.SetControlMessage(ipv4.FlagDst, true); err != nil { + slog.Warn("cannot set control message dstflag", "err", err) + } + + return conn, p4, nil +} + +func makeIPv6MDNSListener() (types.UDPConn, *ipv6.PacketConn, error) { + ua := net.UDPAddrFromAddrPort(ip6MDNSBroadcastAP) + + conn, err := net.ListenUDP("udp6", ua) + if err != nil { + return nil, nil, fmt.Errorf("ListenUDP error: %w", err) + } + + p6 := ipv6.NewPacketConn(conn) + + ift, err := net.Interfaces() + if err != nil { + return nil, nil, fmt.Errorf("cannot get interfaces: %w", err) + } + for _, ifi := range ift { + if ifi.Flags&net.FlagUp != 0 && ifi.Flags&net.FlagPointToPoint == 0 { + if err := p6.JoinGroup(&ifi, &net.UDPAddr{IP: ip6MDNSBroadcastBare.AsSlice()}); err != nil && !errors.Is(err, syscall.EAFNOSUPPORT) { + slog.Warn("p6 multicast JoinGroup failed", "err", err, "iface", ifi.Name) + } + } + } + + if loop, err := p6.MulticastLoopback(); err == nil { + if !loop { + if err := p6.SetMulticastLoopback(true); err != nil { + return nil, nil, fmt.Errorf("cannot set multicast loopback: %w", err) + } + slog.Info("Multicast Loopback enabled") + } else { + slog.Info("Multicast Loopback was enabled") + } + } else { + return nil, nil, fmt.Errorf("cannot get MulticastLoopback: %w", err) + } + + ifi, err := net.InterfaceByName(multicastIface) + if err != nil { + panic(err) + } + + if err := p6.SetMulticastInterface(ifi); err != nil { + return nil, nil, fmt.Errorf("cannot set multicast interface: %w", err) + } + + if err = p6.SetControlMessage(ipv6.FlagDst, true); err != nil { + slog.Warn("cannot set control message dstflag", "err", err) + } + + return conn, p6, nil +} + +func debugMDNS(msg *dnsmessage.Message) { + slog.Info("debugMDNS: TXID", "txid", msg.ID) + + for _, q := range msg.Questions { + slog.Info( + "debugMDNS: Q", + "txid", msg.ID, + "name", q.Name, + "type", q.Type.GoString(), + "class", q.Class.GoString(), + ) + } + for _, a := range msg.Answers { + slog.Info( + "debugMDNS: A", + "txid", msg.ID, + "header", a.Header.GoString(), + "body", a.Body.GoString(), + ) + } +} From ed0174c3e4408b744a14434b9904666bf5ee1011 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 20 Mar 2025 12:39:58 +0100 Subject: [PATCH 76/82] Various improvements to MMan --- toversok/actors/a_mman.go | 63 +++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/toversok/actors/a_mman.go b/toversok/actors/a_mman.go index bcdae33..1c21c2a 100644 --- a/toversok/actors/a_mman.go +++ b/toversok/actors/a_mman.go @@ -27,7 +27,7 @@ type MDNSManager struct { rlStore limiter.Store broadSock *SockRecv - querySock *SockRecv + uniSock *SockRecv working bool } @@ -64,14 +64,14 @@ func (s *Stage) makeMM() *MDNSManager { } m.broadSock = MakeSockRecv(c.ctx, bind) - queryBind, err := m.makeQueryListener() + uniBind, err := m.makeUnicastListener() if err != nil { L(m).Error("could not start MDNS Manager; MDNS sender creation failed", "err", err) return m } - m.querySock = MakeSockRecv(c.ctx, queryBind) + m.uniSock = MakeSockRecv(c.ctx, uniBind) m.working = true @@ -79,16 +79,31 @@ func (s *Stage) makeMM() *MDNSManager { } var ( - MDNSPort uint16 = 5353 - ip4uaMDNSBare = net.UDPAddr{IP: net.IPv4(224, 0, 0, 251)} - ip4MDNSLoopBackAP = netip.AddrPortFrom(netip.MustParseAddr("127.0.0.1"), MDNSPort) - ip4MDNSBroadcastAddress = netip.AddrPortFrom(netip.MustParseAddr("224.0.0.251"), MDNSPort) + MDNSPort uint16 = 5353 + ip4MDNSBroadcastBare = netip.MustParseAddr("224.0.0.251") + ip4MDNSBroadcastAP = netip.AddrPortFrom(ip4MDNSBroadcastBare, MDNSPort) + ip4MDNSLoopBackAP = netip.AddrPortFrom(netip.MustParseAddr("127.0.0.1"), MDNSPort) ) +func getLoopBackInterface() (*net.Interface, error) { + ifaces, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("could not list network interfaces: %w", err) + } + + for _, iface := range ifaces { + if iface.Flags&net.FlagUp != 0 && iface.Flags&net.FlagLoopback != 0 { + return &iface, nil + } + } + + return nil, fmt.Errorf("no loopback interface found") +} + func (mm *MDNSManager) makeMDNSListener() (types.UDPConn, error) { // TODO this only catches ipv4 traffic, which may be a bit "eh", // it may be worth considering firing up one for each stack. - ua := net.UDPAddrFromAddrPort(ip4MDNSBroadcastAddress) + ua := net.UDPAddrFromAddrPort(ip4MDNSBroadcastAP) conn, err := net.ListenUDP("udp4", ua) if err != nil { @@ -103,7 +118,7 @@ func (mm *MDNSManager) makeMDNSListener() (types.UDPConn, error) { } for _, ifi := range ift { if ifi.Flags&net.FlagUp != 0 && ifi.Flags&net.FlagPointToPoint == 0 { - if err := pc.JoinGroup(&ifi, &ip4uaMDNSBare); err != nil { + if err := pc.JoinGroup(&ifi, &net.UDPAddr{IP: ip4MDNSBroadcastBare.AsSlice()}); err != nil { L(mm).Warn("Multicast JoinGroup failed", "err", err, "iface", ifi.Name) } } @@ -117,10 +132,26 @@ func (mm *MDNSManager) makeMDNSListener() (types.UDPConn, error) { } } + lo, err := getLoopBackInterface() + if err != nil { + return nil, fmt.Errorf("cannot get loopback interface: %w", err) + } + + if err := pc.SetMulticastInterface(lo); err != nil { + return nil, fmt.Errorf("cannot set multicast interface: %w", err) + } + + if err := pc.SetTTL(255); err != nil { + return nil, fmt.Errorf("cannot set TTL: %w", err) + } + if err := pc.SetMulticastTTL(255); err != nil { + return nil, fmt.Errorf("cannot set Multicast TTL: %w", err) + } + return conn, nil } -func (mm *MDNSManager) makeQueryListener() (types.UDPConn, error) { +func (mm *MDNSManager) makeUnicastListener() (types.UDPConn, error) { var laddr *net.UDPAddr addr := ip4MDNSLoopBackAP @@ -128,7 +159,7 @@ func (mm *MDNSManager) makeQueryListener() (types.UDPConn, error) { laddr = net.UDPAddrFromAddrPort( netip.AddrPortFrom(mm.s.control.IPv4().Addr(), 0), ) - addr = ip4MDNSBroadcastAddress + addr = ip4MDNSBroadcastAP } return net.DialUDP("udp4", laddr, net.UDPAddrFromAddrPort(addr)) @@ -160,7 +191,7 @@ func (mm *MDNSManager) Run() { } go mm.broadSock.Run() - go mm.querySock.Run() + go mm.uniSock.Run() for { select { @@ -187,12 +218,12 @@ func (mm *MDNSManager) Run() { // TODO process external mDNS packet - if runtime.GOOS == "darwin" || runtime.GOOS == "windows" { + if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { // On macOS, we can't use the broadsock's WriteTo, since it just doesn't generate a packet. // However, we can use our specialised query sock to poke responses in unicast, even if they're QM. - _, err = mm.querySock.Conn.Write(pkt) + _, err = mm.uniSock.Conn.Write(pkt) } else { - _, err = mm.broadSock.Conn.WriteToUDPAddrPort(pkt, ip4MDNSBroadcastAddress) + _, err = mm.broadSock.Conn.WriteToUDPAddrPort(pkt, ip4MDNSBroadcastAP) } if err != nil { L(mm).Warn("failed to process external MDNS packet", "err", err) @@ -202,7 +233,7 @@ func (mm *MDNSManager) Run() { } case frame := <-mm.broadSock.outCh: mm.handleSystemFrame(frame) - case frame := <-mm.querySock.outCh: + case frame := <-mm.uniSock.outCh: mm.handleSystemFrame(frame) case <-mm.ctx.Done(): return From 1a9474d8f31b327db04848d63b3649a02ba72072 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 20 Mar 2025 12:50:15 +0100 Subject: [PATCH 77/82] Have stage actor crash cancel entire stage, and have that cancel the session. Addresses #135 --- toversok/actors/stage.go | 31 +++++++++++++++++++++++++++++-- toversok/session.go | 3 +++ types/ifaces/stage.go | 3 +++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/toversok/actors/stage.go b/toversok/actors/stage.go index 72fa57a..8f1a79a 100644 --- a/toversok/actors/stage.go +++ b/toversok/actors/stage.go @@ -50,8 +50,11 @@ func MakeStage( dialRelayFunc = relayhttp.Dial } + ctx, cancel := context.WithCancel(pCtx) + s := &Stage{ - Ctx: pCtx, + Ctx: ctx, + cancel: cancel, connMutex: sync.RWMutex{}, inConn: make(map[key.NodePublic]InConnActor), @@ -85,16 +88,36 @@ func MakeStage( s.EMan = s.makeEM() s.MMan = s.makeMM() - context.AfterFunc(s.Ctx, s.Close) + s.installAfterFunc() return s } +func (s *Stage) installAfterFunc() { + context.AfterFunc(s.Ctx, s.Close) + + // TODO: self-heal. Currently these just cancel the stage, which then propagates back upwards, but we should figure + // out if its possible to heal components. + + context.AfterFunc(s.DMan.Ctx(), s.cancel) + context.AfterFunc(s.DRouter.Ctx(), s.cancel) + + context.AfterFunc(s.RMan.Ctx(), s.cancel) + context.AfterFunc(s.RRouter.Ctx(), s.cancel) + + context.AfterFunc(s.TMan.Ctx(), s.cancel) + context.AfterFunc(s.SMan.Ctx(), s.cancel) + context.AfterFunc(s.EMan.Ctx(), s.cancel) + context.AfterFunc(s.MMan.Ctx(), s.cancel) +} + // Stage for the Actors type Stage struct { // The parent context of the stage that all actors must parent Ctx context.Context + cancel context.CancelFunc + // The DirectManager DMan ifaces.DirectManagerActor // The DirectRouter @@ -614,3 +637,7 @@ func (s *Stage) ControlSTUN() []netip.AddrPort { // TODO return []netip.AddrPort{} } + +func (s *Stage) Context() context.Context { + return s.Ctx +} diff --git a/toversok/session.go b/toversok/session.go index ecb6422..f3d7dad 100644 --- a/toversok/session.go +++ b/toversok/session.go @@ -91,6 +91,9 @@ func SetupSession( context.AfterFunc(sess.cs.Context(), func() { sess.ccc(errors.New("resumable control session exited")) }) + context.AfterFunc(sess.stage.Context(), func() { + sess.ccc(errors.New("stage exited")) + }) return sess, nil } diff --git a/types/ifaces/stage.go b/types/ifaces/stage.go index 1f0268f..cccf593 100644 --- a/types/ifaces/stage.go +++ b/types/ifaces/stage.go @@ -1,6 +1,7 @@ package ifaces import ( + "context" "net/netip" "github.com/edup2p/common/types/key" @@ -19,4 +20,6 @@ type Stage interface { GetPeerInfo(peer key.NodePublic) *stage.PeerInfo GetEndpoints() []netip.AddrPort + + Context() context.Context } From 75ee7a520f87298e3cb2e00ef246dd22f1234116 Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 20 Mar 2025 13:23:25 +0100 Subject: [PATCH 78/82] MMan: support ipv6 Closes #132 --- toversok/actors/a_mman.go | 178 +++++++++++++++++++++++++++------- toversok/actors/a_tman.go | 33 +++++-- types/msgactor/msg.go | 3 + types/msgsess/sidebanddata.go | 3 +- 4 files changed, 173 insertions(+), 44 deletions(-) diff --git a/toversok/actors/a_mman.go b/toversok/actors/a_mman.go index 1c21c2a..e336796 100644 --- a/toversok/actors/a_mman.go +++ b/toversok/actors/a_mman.go @@ -4,12 +4,14 @@ import ( "context" "crypto/sha256" "encoding/base64" + "errors" "fmt" "net" "net/netip" "runtime" "runtime/debug" "slices" + "syscall" "time" "github.com/edup2p/common/types" @@ -18,6 +20,7 @@ import ( "github.com/sethvargo/go-limiter/memorystore" "golang.org/x/net/dns/dnsmessage" "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" ) type MDNSManager struct { @@ -26,8 +29,11 @@ type MDNSManager struct { rlStore limiter.Store - broadSock *SockRecv - uniSock *SockRecv + b4Sock *SockRecv + b6Sock *SockRecv + + u4Sock *SockRecv + u6Sock *SockRecv working bool } @@ -56,22 +62,45 @@ func (s *Stage) makeMM() *MDNSManager { rlStore: store, }) - bind, err := m.makeMDNSListener() + b4bind, err := m.makeMDNSv4Listener() + if err != nil { + L(m).Warn("MDNS ipv4 listener creation failed", "err", err) + } else { + m.b4Sock = MakeSockRecv(c.ctx, b4bind) + } + + b6bind, err := m.makeMDNSv6Listener() if err != nil { - L(m).Error("could not start MDNS Manager; MDNS listener creation failed", "err", err) + L(m).Warn("MDNS ipv6 listener creation failed", "err", err) + } else { + m.b6Sock = MakeSockRecv(c.ctx, b6bind) + } + + if m.b4Sock == nil && m.b6Sock == nil { + L(m).Error("could not start MDNS Manager; creating both MDNS broadcast sockets failed") return m } - m.broadSock = MakeSockRecv(c.ctx, bind) - uniBind, err := m.makeUnicastListener() + u4bind, err := m.makeIPv4UnicastListener() if err != nil { - L(m).Error("could not start MDNS Manager; MDNS sender creation failed", "err", err) + L(m).Warn("MDNS ipv4 sender creation failed", "err", err) + } else { + m.u4Sock = MakeSockRecv(c.ctx, u4bind) + } - return m + u6bind, err := m.makeIPv6UnicastListener() + if err != nil { + L(m).Warn("MDNS ipv4 sender creation failed", "err", err) + } else { + m.u6Sock = MakeSockRecv(c.ctx, u6bind) } - m.uniSock = MakeSockRecv(c.ctx, uniBind) + if m.u4Sock == nil && m.u6Sock == nil { + L(m).Error("could not start MDNS Manager; creating both MDNS unicast sockets failed") + + return m + } m.working = true @@ -81,8 +110,13 @@ func (s *Stage) makeMM() *MDNSManager { var ( MDNSPort uint16 = 5353 ip4MDNSBroadcastBare = netip.MustParseAddr("224.0.0.251") - ip4MDNSBroadcastAP = netip.AddrPortFrom(ip4MDNSBroadcastBare, MDNSPort) - ip4MDNSLoopBackAP = netip.AddrPortFrom(netip.MustParseAddr("127.0.0.1"), MDNSPort) + ip6MDNSBroadcastBare = netip.MustParseAddr("ff02::fb") + + ip4MDNSBroadcastAP = netip.AddrPortFrom(ip4MDNSBroadcastBare, MDNSPort) + ip6MDNSBroadcastAP = netip.AddrPortFrom(ip6MDNSBroadcastBare, MDNSPort) + + ip4MDNSLoopBackAP = netip.AddrPortFrom(netip.MustParseAddr("127.0.0.1"), MDNSPort) + ip6MDNSLoopBackAP = netip.AddrPortFrom(netip.IPv6Loopback(), MDNSPort) ) func getLoopBackInterface() (*net.Interface, error) { @@ -100,9 +134,7 @@ func getLoopBackInterface() (*net.Interface, error) { return nil, fmt.Errorf("no loopback interface found") } -func (mm *MDNSManager) makeMDNSListener() (types.UDPConn, error) { - // TODO this only catches ipv4 traffic, which may be a bit "eh", - // it may be worth considering firing up one for each stack. +func (mm *MDNSManager) makeMDNSv4Listener() (types.UDPConn, error) { ua := net.UDPAddrFromAddrPort(ip4MDNSBroadcastAP) conn, err := net.ListenUDP("udp4", ua) @@ -110,7 +142,7 @@ func (mm *MDNSManager) makeMDNSListener() (types.UDPConn, error) { return nil, fmt.Errorf("ListenUDP error: %w", err) } - pc := ipv4.NewPacketConn(conn) + pc4 := ipv4.NewPacketConn(conn) ift, err := net.Interfaces() if err != nil { @@ -118,15 +150,15 @@ func (mm *MDNSManager) makeMDNSListener() (types.UDPConn, error) { } for _, ifi := range ift { if ifi.Flags&net.FlagUp != 0 && ifi.Flags&net.FlagPointToPoint == 0 { - if err := pc.JoinGroup(&ifi, &net.UDPAddr{IP: ip4MDNSBroadcastBare.AsSlice()}); err != nil { - L(mm).Warn("Multicast JoinGroup failed", "err", err, "iface", ifi.Name) + if err := pc4.JoinGroup(&ifi, &net.UDPAddr{IP: ip4MDNSBroadcastBare.AsSlice()}); err != nil { + L(mm).Warn("pc4 Multicast JoinGroup failed", "err", err, "iface", ifi.Name) } } } - if loop, err := pc.MulticastLoopback(); err == nil { + if loop, err := pc4.MulticastLoopback(); err == nil { if !loop { - if err := pc.SetMulticastLoopback(true); err != nil { + if err := pc4.SetMulticastLoopback(true); err != nil { return nil, fmt.Errorf("cannot set multicast loopback: %w", err) } } @@ -137,21 +169,63 @@ func (mm *MDNSManager) makeMDNSListener() (types.UDPConn, error) { return nil, fmt.Errorf("cannot get loopback interface: %w", err) } - if err := pc.SetMulticastInterface(lo); err != nil { + if err := pc4.SetMulticastInterface(lo); err != nil { return nil, fmt.Errorf("cannot set multicast interface: %w", err) } - if err := pc.SetTTL(255); err != nil { + if err := pc4.SetTTL(255); err != nil { return nil, fmt.Errorf("cannot set TTL: %w", err) } - if err := pc.SetMulticastTTL(255); err != nil { + if err := pc4.SetMulticastTTL(255); err != nil { return nil, fmt.Errorf("cannot set Multicast TTL: %w", err) } return conn, nil } -func (mm *MDNSManager) makeUnicastListener() (types.UDPConn, error) { +func (mm *MDNSManager) makeMDNSv6Listener() (types.UDPConn, error) { + ua := net.UDPAddrFromAddrPort(ip6MDNSBroadcastAP) + + conn, err := net.ListenUDP("udp6", ua) + if err != nil { + return nil, fmt.Errorf("ListenUDP error: %w", err) + } + + pc6 := ipv6.NewPacketConn(conn) + + ift, err := net.Interfaces() + if err != nil { + return nil, fmt.Errorf("cannot get interfaces: %w", err) + } + for _, ifi := range ift { + if ifi.Flags&net.FlagUp != 0 && ifi.Flags&net.FlagPointToPoint == 0 { + if err := pc6.JoinGroup(&ifi, &net.UDPAddr{IP: ip6MDNSBroadcastBare.AsSlice()}); err != nil && !errors.Is(err, syscall.EAFNOSUPPORT) { + L(mm).Warn("pc6 Multicast JoinGroup failed", "err", err, "iface", ifi.Name) + } + } + } + + if loop, err := pc6.MulticastLoopback(); err == nil { + if !loop { + if err := pc6.SetMulticastLoopback(true); err != nil { + return nil, fmt.Errorf("cannot set multicast loopback: %w", err) + } + } + } + + lo, err := getLoopBackInterface() + if err != nil { + return nil, fmt.Errorf("cannot get loopback interface: %w", err) + } + + if err := pc6.SetMulticastInterface(lo); err != nil { + return nil, fmt.Errorf("cannot set multicast interface: %w", err) + } + + return conn, nil +} + +func (mm *MDNSManager) makeIPv4UnicastListener() (types.UDPConn, error) { var laddr *net.UDPAddr addr := ip4MDNSLoopBackAP @@ -165,6 +239,20 @@ func (mm *MDNSManager) makeUnicastListener() (types.UDPConn, error) { return net.DialUDP("udp4", laddr, net.UDPAddrFromAddrPort(addr)) } +func (mm *MDNSManager) makeIPv6UnicastListener() (types.UDPConn, error) { + var laddr *net.UDPAddr + addr := ip6MDNSLoopBackAP + + if runtime.GOOS == "windows" { + laddr = net.UDPAddrFromAddrPort( + netip.AddrPortFrom(mm.s.control.IPv6().Addr(), 0), + ) + addr = ip6MDNSBroadcastAP + } + + return net.DialUDP("udp6", laddr, net.UDPAddrFromAddrPort(addr)) +} + func dataToB64Hash(b []byte) string { h := sha256.Sum256(b) @@ -190,8 +278,8 @@ func (mm *MDNSManager) Run() { return } - go mm.broadSock.Run() - go mm.uniSock.Run() + go mm.b4Sock.Run() + go mm.u4Sock.Run() for { select { @@ -205,7 +293,12 @@ func (mm *MDNSManager) Run() { continue } - if _, _, _, ok, _ := mm.rlStore.Take(context.Background(), dataToB64Hash(msg.Data)); !ok { + extra := "ip4" + if msg.IP6 { + extra = "ip6" + } + + if _, _, _, ok, _ := mm.rlStore.Take(context.Background(), dataToB64Hash(msg.Data)+extra); !ok { // some rudimentary filtering to prevent true loop storms continue } @@ -221,9 +314,17 @@ func (mm *MDNSManager) Run() { if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { // On macOS, we can't use the broadsock's WriteTo, since it just doesn't generate a packet. // However, we can use our specialised query sock to poke responses in unicast, even if they're QM. - _, err = mm.uniSock.Conn.Write(pkt) + if msg.IP6 { + _, err = mm.u6Sock.Conn.Write(pkt) + } else { + _, err = mm.u4Sock.Conn.Write(pkt) + } } else { - _, err = mm.broadSock.Conn.WriteToUDPAddrPort(pkt, ip4MDNSBroadcastAP) + if msg.IP6 { + _, err = mm.b6Sock.Conn.WriteToUDPAddrPort(pkt, ip6MDNSBroadcastAP) + } else { + _, err = mm.b4Sock.Conn.WriteToUDPAddrPort(pkt, ip4MDNSBroadcastAP) + } } if err != nil { L(mm).Warn("failed to process external MDNS packet", "err", err) @@ -231,9 +332,13 @@ func (mm *MDNSManager) Run() { default: mm.logUnknownMessage(msg) } - case frame := <-mm.broadSock.outCh: + case frame := <-mm.b4Sock.outCh: + mm.handleSystemFrame(frame) + case frame := <-mm.b6Sock.outCh: + mm.handleSystemFrame(frame) + case frame := <-mm.u4Sock.outCh: mm.handleSystemFrame(frame) - case frame := <-mm.uniSock.outCh: + case frame := <-mm.u6Sock.outCh: mm.handleSystemFrame(frame) case <-mm.ctx.Done(): return @@ -244,12 +349,19 @@ func (mm *MDNSManager) Run() { func (mm *MDNSManager) handleSystemFrame(frame RecvFrame) { // got MDNS message from system; forward - if _, _, _, ok, _ := mm.rlStore.Take(context.Background(), dataToB64Hash(frame.pkt)); !ok { + nap := types.NormaliseAddr(frame.src.Addr()) + + extra := "ip4" + if nap.Is6() { + extra = "ip6" + } + + if _, _, _, ok, _ := mm.rlStore.Take(context.Background(), dataToB64Hash(frame.pkt)+extra); !ok { // some rudimentary filtering to prevent true loop storms return } - if !mm.isSelf(frame.src.Addr()) { + if !mm.isSelf(nap) { L(mm).Log(context.Background(), types.LevelTrace, "dropping mDNS packet due to non-local origin", "from", frame.src) return } @@ -260,7 +372,7 @@ func (mm *MDNSManager) handleSystemFrame(frame RecvFrame) { pkt := mm.processMDNS(frame.pkt, true) - SendMessage(mm.s.TMan.Inbox(), &msgactor.TManSpreadMDNSPacket{Pkt: pkt}) + SendMessage(mm.s.TMan.Inbox(), &msgactor.TManSpreadMDNSPacket{Pkt: pkt, IP6: nap.Is6()}) } func (mm *MDNSManager) debugMDNS(msg *dnsmessage.Message) { diff --git a/toversok/actors/a_tman.go b/toversok/actors/a_tman.go index 29c4f2d..a749f8a 100644 --- a/toversok/actors/a_tman.go +++ b/toversok/actors/a_tman.go @@ -117,12 +117,12 @@ func (tm *TrafficManager) Handle(m msgactor.ActorMessage) { node := *n - if tm.isMDNS(m.Msg) { + if ok, ip6 := tm.isMDNS(m.Msg); ok { if !tm.mdnsAllowed(node) { L(tm).Warn("got direct MDNS packet from peer where it is not allowed", "peer", node.Debug()) return } - tm.sendMDNS(node, m.Msg) + tm.sendMDNS(node, m.Msg, ip6) return } @@ -136,12 +136,12 @@ func (tm *TrafficManager) Handle(m msgactor.ActorMessage) { return } - if tm.isMDNS(m.Msg) { + if ok, ip6 := tm.isMDNS(m.Msg); ok { if !tm.mdnsAllowed(m.Peer) { L(tm).Warn("got relay MDNS packet from peer where it is not allowed", "peer", m.Peer.Debug()) return } - tm.sendMDNS(m.Peer, m.Msg) + tm.sendMDNS(m.Peer, m.Msg, ip6) return } @@ -167,16 +167,20 @@ func (tm *TrafficManager) Handle(m msgactor.ActorMessage) { }) } case *msgactor.TManSpreadMDNSPacket: - tm.spreadMDNS(m.Pkt) + tm.spreadMDNS(m.Pkt, m.IP6) default: tm.logUnknownMessage(m) } } -func (tm *TrafficManager) isMDNS(msg *msgsess.ClearMessage) bool { +func (tm *TrafficManager) isMDNS(msg *msgsess.ClearMessage) (isMDNS bool, ip6 bool) { sbd, ok := msg.Message.(*msgsess.SideBandData) - return ok && sbd.Type == msgsess.MDNSType + if ok { + return sbd.Type == msgsess.MDNSv4Type || sbd.Type == msgsess.MDNSv6Type, sbd.Type == msgsess.MDNSv6Type + } + + return false, false } func (tm *TrafficManager) mdnsAllowed(node key.NodePublic) bool { @@ -189,16 +193,17 @@ func (tm *TrafficManager) mdnsAllowed(node key.NodePublic) bool { return pi.MDNS } -func (tm *TrafficManager) sendMDNS(peer key.NodePublic, msg *msgsess.ClearMessage) { +func (tm *TrafficManager) sendMDNS(peer key.NodePublic, msg *msgsess.ClearMessage, ip6 bool) { sbd := msg.Message.(*msgsess.SideBandData) go SendMessage(tm.s.MMan.Inbox(), &msgactor.MManReceivedPacket{ From: peer, Data: sbd.Data, + IP6: ip6, }) } -func (tm *TrafficManager) spreadMDNS(pkt []byte) { +func (tm *TrafficManager) spreadMDNS(pkt []byte, ip6 bool) { peers := tm.s.GetPeersWhere(func(_ key.NodePublic, info *stage.PeerInfo) bool { return info.MDNS }) @@ -207,9 +212,17 @@ func (tm *TrafficManager) spreadMDNS(pkt []byte) { return t.Debug() }) L(tm).Log(context.Background(), types.LevelTrace, "sending mdns packet to peers", "peers", peersDebug) + + var t msgsess.SideBandDataType + if ip6 { + t = msgsess.MDNSv6Type + } else { + t = msgsess.MDNSv4Type + } + for _, peer := range peers { tm.opportunisticSendTo(peer, &msgsess.SideBandData{ - Type: msgsess.MDNSType, + Type: t, Data: pkt, }) } diff --git a/types/msgactor/msg.go b/types/msgactor/msg.go index 1be49b4..de0b3c4 100644 --- a/types/msgactor/msg.go +++ b/types/msgactor/msg.go @@ -47,6 +47,7 @@ type TManSessionMessageFromDirect struct { type TManSpreadMDNSPacket struct { Pkt []byte + IP6 bool } // ====================================================================================================== @@ -118,6 +119,8 @@ type MManReceivedPacket struct { From key.NodePublic Data []byte + + IP6 bool } // ====================================================================================================== diff --git a/types/msgsess/sidebanddata.go b/types/msgsess/sidebanddata.go index e3666da..8adc063 100644 --- a/types/msgsess/sidebanddata.go +++ b/types/msgsess/sidebanddata.go @@ -8,7 +8,8 @@ import ( type SideBandDataType byte const ( - MDNSType SideBandDataType = iota + MDNSv4Type SideBandDataType = iota + MDNSv6Type SideBandDataType = iota ) type SideBandData struct { From b95cf346dea23c1ee93096986d37aaa64ce0a05c Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Thu, 20 Mar 2025 14:05:38 +0100 Subject: [PATCH 79/82] Do last of cleanup of MMan Closes #130 --- toversok/actors/a_mman.go | 88 ++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/toversok/actors/a_mman.go b/toversok/actors/a_mman.go index e336796..d0b74f6 100644 --- a/toversok/actors/a_mman.go +++ b/toversok/actors/a_mman.go @@ -287,48 +287,7 @@ func (mm *MDNSManager) Run() { // got MDNS message from external; inject switch msg := msg.(type) { case *msgactor.MManReceivedPacket: - pi := mm.s.GetPeerInfo(msg.From) - if pi == nil { - L(mm).Warn("ignoring MDNS packet due to nonexistent peerinfo", "from", msg.From.Debug()) - continue - } - - extra := "ip4" - if msg.IP6 { - extra = "ip6" - } - - if _, _, _, ok, _ := mm.rlStore.Take(context.Background(), dataToB64Hash(msg.Data)+extra); !ok { - // some rudimentary filtering to prevent true loop storms - continue - } - - L(mm).Debug("processing external MDNS packet", "len", len(msg.Data), "from", msg.From.Debug()) - - pkt := mm.processMDNS(msg.Data, false) - - var err error - - // TODO process external mDNS packet - - if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { - // On macOS, we can't use the broadsock's WriteTo, since it just doesn't generate a packet. - // However, we can use our specialised query sock to poke responses in unicast, even if they're QM. - if msg.IP6 { - _, err = mm.u6Sock.Conn.Write(pkt) - } else { - _, err = mm.u4Sock.Conn.Write(pkt) - } - } else { - if msg.IP6 { - _, err = mm.b6Sock.Conn.WriteToUDPAddrPort(pkt, ip6MDNSBroadcastAP) - } else { - _, err = mm.b4Sock.Conn.WriteToUDPAddrPort(pkt, ip4MDNSBroadcastAP) - } - } - if err != nil { - L(mm).Warn("failed to process external MDNS packet", "err", err) - } + mm.handleReceivedPacket(msg) default: mm.logUnknownMessage(msg) } @@ -346,6 +305,51 @@ func (mm *MDNSManager) Run() { } } +func (mm *MDNSManager) handleReceivedPacket(msg *msgactor.MManReceivedPacket) { + pi := mm.s.GetPeerInfo(msg.From) + if pi == nil { + L(mm).Warn("ignoring MDNS packet due to nonexistent peerinfo", "from", msg.From.Debug()) + return + } + + extra := "ip4" + if msg.IP6 { + extra = "ip6" + } + + if _, _, _, ok, _ := mm.rlStore.Take(context.Background(), dataToB64Hash(msg.Data)+extra); !ok { + // some rudimentary filtering to prevent true loop storms + return + } + + L(mm).Debug("processing external MDNS packet", "len", len(msg.Data), "from", msg.From.Debug()) + + pkt := mm.processMDNS(msg.Data, false) + + var err error + + // TODO process external mDNS packet + + if runtime.GOOS == "windows" || runtime.GOOS == "darwin" { + // On macOS, we can't use the broadsock's WriteTo, since it just doesn't generate a packet. + // However, we can use our specialised query sock to poke responses in unicast, even if they're QM. + if msg.IP6 { + _, err = mm.u6Sock.Conn.Write(pkt) + } else { + _, err = mm.u4Sock.Conn.Write(pkt) + } + } else { + if msg.IP6 { + _, err = mm.b6Sock.Conn.WriteToUDPAddrPort(pkt, ip6MDNSBroadcastAP) + } else { + _, err = mm.b4Sock.Conn.WriteToUDPAddrPort(pkt, ip4MDNSBroadcastAP) + } + } + if err != nil { + L(mm).Warn("failed to process external MDNS packet", "err", err) + } +} + func (mm *MDNSManager) handleSystemFrame(frame RecvFrame) { // got MDNS message from system; forward From fe659f0ab5b3b925dd16bc538f1a2a53cd26623a Mon Sep 17 00:00:00 2001 From: Henk Berendsen <61596108+hb140502@users.noreply.github.com> Date: Thu, 27 Mar 2025 12:53:31 +0100 Subject: [PATCH 80/82] Compare CI performance (#140) * Add artifact for performance test data (with commit hash as unique ID), and allow multiple performance tests per CI run by putting test number in filenames * Try to download artifact from other commit in workflow * Refine downloading artifact from other commit * Try to fix workflow failure * Debug other failure * .. * Fix failure (typo in zip name) * Revert adding index to performance test data (was not necessary due to files being in subfolders), small changes when saving performance test data * Forgot to remove index at one point in code * Add script to compare performance between 2 performance tests, and deploy it in CI * Debug workflow failure * Attempt fix * .. * Ignore comparison artifact's workflow conclusion * Account for edge case of baseline performance value being 0, which would make comparison result in worse performance for any non-zero value, no matter how small * Add performance result in workflow job summary * Remove debug job steps * Test worse performance by simulating extra packet loss * Fix logic bug in edge case handling * Retry testing worse performance after fixing bug * Update path regex to solve problem of no files being iterated over in comparison, add more detailed logging * Add debug step * Fix wrongly specified path to new performance test data * Attempt to fix syntax error, increase packet loss * Replace case statement by if elif else * Fix syntax error * Debug exit_code variable * Continue debug * New approach: pipe comparison output directly to job summary, also add edge case for better performance * Format comparison script output as markdown * Fix wrong redirect variable * Get rid of simulated packet loss, fix bug with booleans in comparison script * Uncomment other workflows * Make performance change notifications more readable * Exit successfully with warning if downloading artifact of previous performance test fails * Prevent stops dependent on successful artifact download from running when this step fails * Prevent false positives/negatives in comparison by making bounds more lenient and repeating performance test * Make bounds more lenient for small values --- .github/workflows/CI_test_suite.yml | 50 ++++++++- test_suite/compare_performance.py | 128 ++++++++++++++++++++++ test_suite/system_tests.sh | 2 +- test_suite/visualize_performance_tests.py | 6 +- 4 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 test_suite/compare_performance.py diff --git a/.github/workflows/CI_test_suite.yml b/.github/workflows/CI_test_suite.yml index ddfd317..cef228e 100644 --- a/.github/workflows/CI_test_suite.yml +++ b/.github/workflows/CI_test_suite.yml @@ -82,7 +82,13 @@ jobs: uses: actions/upload-artifact@v4 with: name: performance-test-graphs - path: test_suite/system_test_logs/performance/1_No-NAT_No-NAT/*.png + path: test_suite/system_test_logs/performance/*/*.png + + - name: Upload performance test data + uses: actions/upload-artifact@v4 + with: + name: performance-test-data + path: test_suite/system_test_logs/performance/*/performance_test_data.json - name: Upload full performance test logs uses: actions/upload-artifact@v4 @@ -94,6 +100,48 @@ jobs: if: ${{ steps.system-test.outcome == 'failure' }} run: exit 1 + - name: Download artifact from target branch head + id: download_target_branch + if: ${{ github.event_name == 'pull_request' }} + uses: dawidd6/action-download-artifact@v9 + with: + branch: ${{ github.base_ref }} + name: performance-test-data + path: ./test_suite + skip_unpack: true + workflow_conclusion: "" + continue-on-error: true + + - name: Download artifact from previous commit + id: download_previous_commit + if: ${{ github.event_name == 'push' }} + uses: dawidd6/action-download-artifact@v9 + with: + commit: ${{ github.event.before }} + name: performance-test-data + path: ./test_suite + skip_unpack: true + workflow_conclusion: "" + continue-on-error: true + + - name: Stop job and give warning if downloading previous artifact failed + if: ${{ steps.download_target_branch.outcome == 'failure' || steps.download_previous_commit.outcome == 'failure' }} + run: | + echo "# ⚠️ Could not make performance comparison" >> $GITHUB_STEP_SUMMARY + echo "Downloading performance test data of target branch head/previous commit failed. See the corresponding PerformanceTests job step for details" >> $GITHUB_STEP_SUMMARY + + - name: Unzip artifact + working-directory: test_suite + if: ${{ steps.download_target_branch.outcome == 'success' || steps.download_previous_commit.outcome == 'success' }} + run: unzip performance-test-data.zip -d previous_performance + + - name: Compare current and artifact performance, and redirect results to job step summary + id: performance-comparison + working-directory: test_suite + if: ${{ steps.download_target_branch.outcome == 'success' || steps.download_previous_commit.outcome == 'success' }} + run: python compare_performance.py previous_performance/ system_test_logs/performance/ > $GITHUB_STEP_SUMMARY + + IntegrationTests: runs-on: ubuntu-latest steps: diff --git a/test_suite/compare_performance.py b/test_suite/compare_performance.py new file mode 100644 index 0000000..b8c7ec4 --- /dev/null +++ b/test_suite/compare_performance.py @@ -0,0 +1,128 @@ +import json +import os +import sys +from pathlib import Path + +# Exit codes +EXIT_PERFORMANCE_SIMILAR = 0 +EXIT_COMPARISON_FAILED = 1 +EXIT_PERFORMANCE_WORSE = 1 +EXIT_PERFORMANCE_BETTER = 0 + +# Ensure both parameters are provided +if len(sys.argv) - 1 != 2: + print(f""" +Usage: python {sys.argv[0]} + +The two parameters should be either system test logs containing only performance tests, or extracted performance-test-data artifacts from GitHub Actions + +The output of this script is formatted as markdown such that it can be used in GitHub job step summaries""") + exit(EXIT_COMPARISON_FAILED) + +baseline=sys.argv[1] +new=sys.argv[2] + +# This dictionary defines which measurements to compare, and when to consider two measurements worse/better/similar +COMPARISON_CONFIG = { + "Target bitrate": { + "packet_loss": { + "better": lambda new, baseline: new < 0.8 * baseline and new < baseline - 5, + "worse": lambda new, baseline: new > 1.2 * baseline and new > baseline + 5 + } + } +} + +# Keep track of whether performance is worse or better +performance_worse = False +performance_better = False + +def failure(reason: str): + print("# ❌ Performance comparison failed") + print(reason) + exit(EXIT_COMPARISON_FAILED) + +def check_same_performance_test(new_data: dict, baseline_data: dict, rel_path: str): + """Check whether the two data files contain the same performance test, otherwise comparison is not possible""" + + same_test_var = new_data["test_var"] == baseline_data["test_var"] + + if not same_test_var: + failure(f"mismatch in test variable or its values for {rel_path}") + + baseline_metrics = set(baseline_data["measurements"].keys()) + new_metrics = set(new_data["measurements"].keys()) + all_required_metrics = baseline_metrics <= new_metrics + + if not all_required_metrics: + failure(f"for {rel_path}, some of the measurements in the baseline data are not present in the new data") + +def report_performance_change(better: bool, metric: str, idx: int, test_var: str, test_var_values: list[float], baseline: float, new: float): + change = "improved" if better else "degraded" + print(f"- For {test_var} = {test_var_values[idx]}, {metric} {change}: {baseline:.1f} -> {new:.1f}") + +def compare_measurements(new_data: dict, baseline_data: dict): + test_var = baseline_data["test_var"]["label"] + test_var_values = baseline_data["test_var"]["values"] + + if not(test_var in COMPARISON_CONFIG.keys()): + return + + # Keep track of performance difference by modifying the global variables + global performance_worse, performance_better + + metrics_to_compare = COMPARISON_CONFIG[test_var].keys() + new_measurements = new_data["measurements"] + baseline_measurements = baseline_data["measurements"] + + for metric in metrics_to_compare: + metric_label = baseline_measurements[metric]["label"] + new_values = new_measurements[metric]["values"]["average"]["eduP2P"] + baseline_values = baseline_measurements[metric]["values"]["average"]["eduP2P"] + is_worse = COMPARISON_CONFIG[test_var][metric]["worse"] + is_better = COMPARISON_CONFIG[test_var][metric]["better"] + + for i, (new_val, baseline_val) in enumerate(zip(new_values, baseline_values)): + if is_worse(new_val, baseline_val): + report_performance_change(False, metric_label, i, test_var, test_var_values, baseline_val, new_val) + performance_worse = True + + if is_better(new_val, baseline_val): + report_performance_change(True, metric_label, i, test_var, test_var_values, baseline_val, new_val) + performance_better = True and not performance_worse # Worse performance has higher priority than better performance + +# Iterate over all data files from baseline performance test data +cwd = os.getcwd() +baseline_files = Path(f"{cwd}/{baseline}").rglob("performance_test_data.json*") +print("# Comparison details") + +for path in baseline_files: + path = str(path) + + # Get relative path by removing current working directory + baseline directory prefix + rel_path = path[len(cwd) + len(baseline) + 1:] + print(f"### Comparing {rel_path}...") + + # Attempt to open same performance test file in new data + try: + with open(f"{cwd}/{new}/{rel_path}") as f_new: + new_data = json.load(f_new) + except FileNotFoundError: + failure(f"{rel_path} is present in {baseline}, but not in {new}") + + with open(path) as f_baseline: + baseline_data = json.load(f_baseline) + + check_same_performance_test(new_data, baseline_data, rel_path) + compare_measurements(new_data, baseline_data) + +# Print final conclusion about performance +if performance_worse: + print(f"# 📉 Total performance has degraded") + exit(EXIT_PERFORMANCE_WORSE) +elif performance_better: + print(f"# 📈 Total performance has improved!") + exit(EXIT_PERFORMANCE_BETTER) + +print(f"# ✅ No significant performance change") +exit(EXIT_PERFORMANCE_SIMILAR) + diff --git a/test_suite/system_tests.sh b/test_suite/system_tests.sh index 8088da4..b220f47 100755 --- a/test_suite/system_tests.sh +++ b/test_suite/system_tests.sh @@ -279,7 +279,7 @@ fi if [[ $performance == true ]]; then echo -e "\nPerformance tests (without NAT)" - run_system_test -k bitrate -v 100,200,300,400,500 -d 3 -b both TS_PASS_DIRECT router1-router2 : wg0:wg0 + run_system_test -k bitrate -v 100,200,300,400,500 -d 3 -b both -r 3 TS_PASS_DIRECT router1-router2 : wg0:wg0 elif [[ -n $file ]]; then echo -e "\nTests from file: $file" diff --git a/test_suite/visualize_performance_tests.py b/test_suite/visualize_performance_tests.py index ac0e2b3..0622756 100644 --- a/test_suite/visualize_performance_tests.py +++ b/test_suite/visualize_performance_tests.py @@ -60,13 +60,17 @@ def test_iteration(): # Delete transform key from bitrate metric, since it is not JSON serializable del extracted_data["bitrate"]["transform"] + # Delete json_key key from all metrics, since they are no longer needed + for k in extracted_data.keys(): + del extracted_data[k]["json_key"] + # Merge test variable info and measurements into one dictionary data_dict = { "test_var": test_var_dict, "measurements": extracted_data } - json.dump(data_dict, file) + json.dump(data_dict, file, indent=4) for metric in extracted_data.keys(): create_performance_graph(test_var, test_var_values, metric, extracted_data, parent_path) From d9ec31be552daddefcbda3049c200f6a284712bb Mon Sep 17 00:00:00 2001 From: Jonathan de Jong Date: Tue, 1 Apr 2025 11:02:42 +0200 Subject: [PATCH 81/82] Update bug.md: set Type: Bug --- .github/ISSUE_TEMPLATE/bug.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index cf0bfb2..b506414 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -2,7 +2,7 @@ name: Bug about: Template for a problem with the library's behaviour title: "" -labels: Bug +type: Bug assignees: '' --- From 4449afeb0215dc3fefd068c28fbee80961376bb6 Mon Sep 17 00:00:00 2001 From: Henk Berendsen <61596108+hb140502@users.noreply.github.com> Date: Mon, 14 Apr 2025 19:00:34 +0200 Subject: [PATCH 82/82] Parallel system tests (#146) * Update log levels (remove trace, add warn/error) * Add trace log level to test client, undo removal in system test scripts * Dockerfile for image to run system tests * Fix error occurring in Docker container Python version * Optimize system tests speed - Replace sleep 1s by sleep 0.1s - Instead of logging and checking both IPv4 and IPv6 HTTP server connections to make sure the peer is finished, only check the IPv6 server (since the peers connect to this one last) - Add a sleep period to desynchronize the peers. This avoids two handshakes potentially being initiated at the same time, causing a 5 second backoff period - In tail commands, add flag -s 0.1 to specify a polling rate of 100ms. Leaving this unspecified drastically slows down the commands when running in Docker containers * Only build the eduP2P client and server binaries when new -b flag is specified (can be omitted for speed if binaries have already been built), and use the flag in CI * Fix wrong variable in -b flag check * Add -t option to system_tests.sh to specify how many threads should run in parallel * Monitor progress of each thread and add script to display progress to the user * Delete conntrack entries before each test to ensure only connections made in the current test are present in the logs * Add sleep to both peers before they start connecting. This sleep prevents failures with the NAT hairpinning tests, which seem to be caused by the nftables rules not having enough time to be added before the peers try to connect * Refactor Dockerfile to optimize caching by installing requirements before cloning whole repository, and add build argument to specifiy which branch to clone * Use parallel system tests in CI, speed up by caching docker build and not installing Python dependencies * Document parallel system tests * Move new features funded by NLnet to separate changelog, and add parallel system tests feature * Add -b flag to sequential system tests to ensure eduP2P binaries exist before running the tests * Remove unused variable * Add peer http servers output to test logs --- .github/workflows/CI_test_suite.yml | 44 +++- test_suite/CHANGELOG.md | 37 +++ test_suite/Dockerfile | 37 +++ test_suite/README.md | 110 ++++---- test_suite/system_test.sh | 14 +- test_suite/system_tests.sh | 304 +++++++++++++++++----- test_suite/test_client/main.go | 3 + test_suite/test_client/setup_client.sh | 43 +-- test_suite/visualize_performance_tests.py | 2 +- 9 files changed, 439 insertions(+), 155 deletions(-) create mode 100644 test_suite/CHANGELOG.md create mode 100644 test_suite/Dockerfile diff --git a/.github/workflows/CI_test_suite.yml b/.github/workflows/CI_test_suite.yml index cef228e..2a0f718 100644 --- a/.github/workflows/CI_test_suite.yml +++ b/.github/workflows/CI_test_suite.yml @@ -10,8 +10,9 @@ on: jobs: - SystemTests: + SystemTestsSequential: runs-on: ubuntu-latest + if: ${{ github.event_name == 'pull_request' }} steps: - uses: actions/checkout@v4 @@ -33,9 +34,44 @@ jobs: run: pip install -r python_requirements.txt working-directory: test_suite - - name: Run system tests + - name: Run system tests sequentially id: system-test - run: ./system_tests.sh -c 0 + run: ./system_tests.sh -b + working-directory: test_suite + continue-on-error: true + + - name: Upload system test logs + uses: actions/upload-artifact@v4 + with: + name: system-test-logs + path: test_suite/system_test_logs/ + + - name: Fail job if system test failed (for clarity in GitHub UI) + if: ${{ steps.system-test.outcome == 'failure' }} + run: exit 1 + + SystemTestsParallel: + runs-on: ubuntu-latest + if: ${{ github.event_name == 'push' }} + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Dockerfile to run system tests in parallel + uses: docker/build-push-action@v6 + with: + file: test_suite/Dockerfile + load: true + tags: system_tests:latest + build-args: BRANCH=${{ github.ref_name }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run system tests in parallel + id: system-test + run: GITHUB_ACTION=true ./system_tests.sh -t 4 working-directory: test_suite continue-on-error: true @@ -74,7 +110,7 @@ jobs: - name: Run performance test with varying bitrate id: system-test - run: ./system_tests.sh -p -L performance + run: ./system_tests.sh -b -p -L performance working-directory: test_suite continue-on-error: true diff --git a/test_suite/CHANGELOG.md b/test_suite/CHANGELOG.md new file mode 100644 index 0000000..45a528e --- /dev/null +++ b/test_suite/CHANGELOG.md @@ -0,0 +1,37 @@ +# Changelog + +In this file, the test suite features that have been made possible thanks to [funding from NLnet](./README.md#funding) are documented. + + +## Parallel system tests (April 11, 2025) + +### Added +- [Dockerfile](Dockerfile) that installs all requirements to run the test suite's system tests, clones the repository state at the head of a specified branch, and build the eduP2P client, control server and relay server test binaries. +- Docker Engine as new requirement in [system test requirements](README.md#system-test-specific-requirements). +- `-t` flag in [system_tests.sh](system_tests.sh) to run the system test in parallel with the specified amount of threads. Each thread is a Docker container in which a portion of the system tests is executed. +- SystemTestsParallel job in [test suite CI workflow](../.github/workflows/CI_test_suite.yml) that builds the Dockerfile with caching and runs the system tests with the `-t` flag. +- Explanation of the `-t` flag, motivation for using Docker and reason why parallel system tests currently do not speed up CI runs in the [system test documentation](README.md#system-tests). +- Log level `trace` (most detailed) in [test_client/main.go](test_client/main.go). + +### Changed +- Building of eduP2P binaries in [system_tests.sh](system_tests.sh) now only happens when explicitly providing the new `-b` flag to save time when the binaries have already been built (e.g. in Dockerfile or on local machine). +- Optimizations in [test_client/setup_client.sh](test_client/setup_client.sh), such as smaller sleep durations to make the system tests run faster. + +### Fixed +- Bug in [visualize_performance_tests.py](visualize_performance_tests.py): quotation marks inside a format string caused error in some Python versions. +- Handshake error between eduP2P peers in the system tests caused by both peers initializing a handshake simultaneously. Fixed by desynchronizing the peers with a conditional sleep in [test_client/setup_client.sh](test_client/setup_client.sh). + +## Repeated performance tests (March 7, 2025) + +### Added +- `-r` flag in [performance_tests.sh](performance_tests.sh) to repeat the same performance test multiple times and aggregate the results of each repetition by taking their average. +- Explanation of the `-r` flag in the [performance test documentation](./README.md#performance-tests). +- Report on how aggregating the performance test results can improve their reliability in the [performance test results](./README.md#consistency-of-results). + +## Simulating network delay (March 4, 2025) + +### Added +- `-d` flag in [system_tests.sh](system_tests.sh) to add artificial network delay in the system tests. +- New value `delay` for the `-k` flag in [performance_tests.sh](performance_tests.sh) to add variable artificial network delay during the performance tests. +- Explanation of the delay variable in the [performance test documentation](./README.md#performance-tests). +- Report on how the delay affects eduP2P network performance in the [performance test results](./README.md#results-with-varying-one-way-delay). \ No newline at end of file diff --git a/test_suite/Dockerfile b/test_suite/Dockerfile new file mode 100644 index 0000000..d6c6447 --- /dev/null +++ b/test_suite/Dockerfile @@ -0,0 +1,37 @@ +# Git branch +ARG BRANCH="main" + +# Stage that clones repository, to be copied by next stage for cache optimization +FROM alpine AS get-requirements +ARG BRANCH + +# Clone repository, prevent cache from using old version by adding current version to Dockerfile +RUN apk add --no-cache git +ADD https://api.github.com/repos/eduP2P/common/git/refs/heads/${BRANCH} version.json +RUN git clone -b ${BRANCH} --single-branch https://github.com/eduP2P/common + +# Stage that makes optimal use of cache by first copying and installing requirements from previous stage, and only cloning repository afterwards +FROM golang:1.22 AS run-system-tests +ARG BRANCH + +# Command-line requirements +COPY --from=get-requirements /common/test_suite/system_test_requirements.txt /go/system_test_requirements.txt +RUN apt-get update &&\ + apt-get -y install sudo &&\ + xargs -a system_test_requirements.txt sudo apt-get -y install + +# Go packages +COPY --from=get-requirements /common/go.mod /go/src/go.mod +WORKDIR /go/src +RUN go mod download + +# Clone repository, prevent cache from using old version by adding current version to Dockerfile +WORKDIR /go +ADD https://api.github.com/repos/eduP2P/common/git/refs/heads/${BRANCH} version.json +RUN git clone -b ${BRANCH} --single-branch https://github.com/eduP2P/common + +# Build eduP2P binaries +WORKDIR /go/common/test_suite +RUN for binary in test_client control_server relay_server; do go build -o $binary/$binary $binary/*.go; done + +ENTRYPOINT ["/go/common/test_suite/system_tests.sh"] \ No newline at end of file diff --git a/test_suite/README.md b/test_suite/README.md index 9e3bb97..31f6017 100644 --- a/test_suite/README.md +++ b/test_suite/README.md @@ -63,6 +63,10 @@ sudo and xargs packages): xargs -a system_test_requirements.txt sudo apt-get install +Optionally, the system tests can be run in parallel. In this mode the +system tests are distributed over Docker containers, so it requires +installing [Docker Engine](https://docs.docker.com/engine/install/). + ### Performance test-specific requirements The performance tests are run as a part of the system tests, and require @@ -106,6 +110,28 @@ physical setup for two reasons: network congestion than a physical network. The simulated network setup is described in detail in the next section. +As mentioned in the [system test +requirements](#system-test-specific-requirements), the tests may be run +in parallel using Docker. The user can specify the amount of “threads” +with the `-t` flag, which determines over how many Docker containers the +tests will be distributed. The reason for using Docker is that it allows +the concurrent tests to be executed in isolated networks. Naturally, +running the tests in parallel allows the tests to run much faster. One +disadvantage of the parallel tests is that a Docker image must be built +before running the tests. This takes quite a while the first time the +image is built, but by making use of Docker’s caching, building the +image again after revisions to the code is significantly faster. + +The parallel system tests are also being used in the CI GitHub workflow +when new code is pushed to a branch. For pull requests, the sequential +version of the tests is still used because we cannot simply clone the +branch head inside the Docker image in this case. Currently, running the +system tests in parallel in CI does not the system test job to run +faster. The reason for this is that loading and storing the Docker image +from the GitHub Actions cache takes too long for the speed-up in +actually running the tests to matter. This problem could potentially be +solved by deploying a self-hosted runner with persistent memory to +perform the CI workflow. ### Network Simulation Setup @@ -562,8 +588,8 @@ establish a connection between peers are well-established This test suite uses the RFC 4787 [\[4\]](#ref-rfc4787) terminology, which does not categorize NAT into these four types. However, each of these four types of NAT described in RFC 3489 uses a different -combination of the NAT mapping and filtering behaviour described in RFC 4787. -Below, the four types of NAT from RFC 3489 are listed, while +combination of the NAT mapping and filtering behaviour described in RFC +4787. Below, the four types of NAT from RFC 3489 are listed, while noting the RFC 4787 mapping and filtering behaviour they are equivalent to: @@ -582,12 +608,12 @@ an ‘X’ if UDP hole punching is successful in the scenario where one peer is behind the NAT indicated by the cell’s row header, and the other peer is behind the NAT indicated by the cell’s column header. -| NAT Type | Full Cone | Restricted Cone | Port Restricted Cone | Symmetric | -|:-------------------------|:----------|:----------------|:---------------------|:----------| -| **Full Cone** | X | X | X | X | -| **Restricted Cone** | X | X | X | X | -| **Port Restricted Cone** | X | X | X | | -| **Symmetric** | X | X | | | +| NAT Type | Full Cone | Restricted Cone | Port Restricted Cone | Symmetric | +|:---|:---|:---|:---|:---| +| **Full Cone** | X | X | X | X | +| **Restricted Cone** | X | X | X | X | +| **Port Restricted Cone** | X | X | X | | +| **Symmetric** | X | X | | | As seen in the table, UDP hole punching succeeds unless one peer is behind a Port Restricted Cone NAT or Symmetric NAT, and the other peer @@ -696,12 +722,12 @@ of RFC 4787 NAT mapping and filtering behaviour. The results of repeating the UDP hole punching experiment with eduP2P are shown in the table below: -| NAT Type | Full Cone | Restricted Cone | Port Restricted Cone | Symmetric | -|:-------------------------|:----------|:----------------|:---------------------|:----------| -| **Full Cone** | X | X | X | X | -| **Restricted Cone** | X | X | X | | -| **Port Restricted Cone** | X | X | X | | -| **Symmetric** | X | | | | +| NAT Type | Full Cone | Restricted Cone | Port Restricted Cone | Symmetric | +|:---|:---|:---|:---|:---| +| **Full Cone** | X | X | X | X | +| **Restricted Cone** | X | X | X | | +| **Port Restricted Cone** | X | X | X | | +| **Symmetric** | X | | | | Comparing this table with the one in the previous section, we see that eduP2P is not able to establish a direct connection when one peer is @@ -994,17 +1020,17 @@ The results of extending the UDP hole punching experiment to all combinations of RFC 4787 mapping (EIM, ADM, ADPM) and filtering (EIF, ADF, ADPF) behaviours are shown in the table below: -| NAT Type | EIM-EIF | EIM-ADF | EIM-ADPF | ADM-EIF | ADM-ADF | ADM-ADPF | ADPM-EIF | ADPM-ADF | ADPM-ADPF | -|:--------------|:--------|:--------|:---------|:--------|:--------|:---------|:---------|:---------|:----------| -| **EIM-EIF** | X | X | X | X | X | X | X | X | X | -| **EIM-ADF** | X | X | X | X | X | X | X | X | X | -| **EIM-ADPF** | X | X | X | X | | | X | | | -| **ADM-EIF** | X | X | X | X | X | X | X | X | X | -| **ADM-ADF** | X | X | | X | | | X | | | -| **ADM-ADPF** | X | X | | X | | | X | | | -| **ADPM-EIF** | X | X | X | X | X | X | X | X | X | -| **ADPM-ADF** | X | X | | X | | | X | | | -| **ADPM-ADPF** | X | X | | X | | | X | | | +| NAT Type | EIM-EIF | EIM-ADF | EIM-ADPF | ADM-EIF | ADM-ADF | ADM-ADPF | ADPM-EIF | ADPM-ADF | ADPM-ADPF | +|:---|:---|:---|:---|:---|:---|:---|:---|:---|:---| +| **EIM-EIF** | X | X | X | X | X | X | X | X | X | +| **EIM-ADF** | X | X | X | X | X | X | X | X | X | +| **EIM-ADPF** | X | X | X | X | | | X | | | +| **ADM-EIF** | X | X | X | X | X | X | X | X | X | +| **ADM-ADF** | X | X | | X | | | X | | | +| **ADM-ADPF** | X | X | | X | | | X | | | +| **ADPM-EIF** | X | X | X | X | X | X | X | X | X | +| **ADPM-ADF** | X | X | | X | | | X | | | +| **ADPM-ADPF** | X | X | | X | | | X | | | Based on these results, we can conclude that there are three (overlapping) types of NAT scenarios where the UDP hole punching process @@ -1390,8 +1416,8 @@ Network Emulator.” Available: J. Rosenberg, C. Huitema, R. Mahy, and J. Weinberger, “STUN - Simple Traversal of User Datagram Protocol (UDP) Through Network Address Translators -(NATs).” in Request for comments. RFC 3489; RFC Editor, Mar. 2003. -doi: [10.17487/RFC3489](https://doi.org/10.17487/RFC3489). +(NATs).” in Request for comments. RFC 3489; RFC Editor, Mar. +2003. doi: [10.17487/RFC3489](https://doi.org/10.17487/RFC3489). @@ -1428,33 +1454,3 @@ Conservancy](https://commonsconservancy.org/). The test suite features that have been made possible thanks to this funding are described below. - -### Simulating network delay (finished March 4, 2025) - -This feature makes it possible to add artificial network delay in the -system and performance tests. - -The feature can be used with the system tests by calling -`system_tests.sh` with the option `-d `. In the performance -tests, this artificial delay can be configured as the independent test -variable. More details are given in the [performance test -documentation](./README.md#performance-tests). - -Furthermore, the effect of the artificial delay on the eduP2P network -performance is reported in the [performance test -results](./README.md#results-with-varying-one-way-delay). - -### Repeated performance tests (finished March 7, 2025) - -This feature adds the option to repeat the same performance test -multiple times and aggregate the results of each repetition by taking -their average. - -The option is configured with the `-r` flag of the performance tests. -See the [performance test documentation](./README.md#performance-tests) -for details on how to configure and run a performance test. - -In the [performance test results](./README.md#consistency-of-results), -the variance between different repetitions of the same performance test -is analysed. This analysis shows that aggregating the results can -improve their reliability. diff --git a/test_suite/system_test.sh b/test_suite/system_test.sh index d84eead..43d0a82 100755 --- a/test_suite/system_test.sh +++ b/test_suite/system_test.sh @@ -37,7 +37,7 @@ If [WIREGUARD INTERFACE 1] or [WIREGUARD INTERFACE 2] is not provided, the corre is a string of IP addresses separated by a space that may be the destination IP of packets crossing this NAT device, and is necessary to simulate an Address-Dependent Mapping - should be one of {trace|debug|info} (in order of most to least log messages), but can NOT be info if one if the peers is using userspace WireGuard (then IP of the other peer is not logged)""" + should be one of {trace|debug|info|warn|error}, and MUST be trace/debug if one of the peers uses userspace WireGuard (the other peer's IP address is not logged otherwise)""" # Use functions and constants from util.sh . ./util.sh @@ -162,6 +162,11 @@ wg_interface_regex="^([^:]*):([^:]*)$" validate_str $wg_interface_str $wg_interface_regex wg_interfaces=(${BASH_REMATCH[1]} ${BASH_REMATCH[2]}) +# Remove conntrack entries from potential previous tests +for router_ns in ${router_ns_list[@]}; do + sudo ip netns exec $router_ns conntrack -D &> /dev/null +done + # Prepare a string describing the NAT setup NAT_TYPES=("EI" "AD" "APD") @@ -234,6 +239,9 @@ function clean_exit() { # Kill background processes, such as the setup_client.sh scripts sudo kill $(jobs -p) &> /dev/null + # Remove restrictive permissions on certain log files + sudo chmod --recursive 777 $log_dir + exit $exit_code } @@ -266,7 +274,7 @@ for i in {0..1}; do touch $peer_logfile # Make sure file already exists so tail command later in script does not fail sudo ip netns exec $peer_ns ./setup_client.sh `# Run script in peer's network namespace` \ - $peer_id $peer_ns $test_target $control_pub_key $control_ip $control_port $log_lvl ${wg_interfaces[$i]} `# Positional parameters` \ + $peer_id $peer_ns $test_target $control_pub_key $control_ip $control_port $log_lvl $log_dir ${wg_interfaces[$i]} `# Positional parameters` \ 2>&1 | tee $peer_logfile &> /dev/null & # Combination of tee and redirect to /dev/null is necessary to avoid weird behaviour caused by redirecting a script run with sudo done @@ -274,7 +282,7 @@ done for i in {0..1}; do peer_id="peer$((i+1))" export LOG_FILE=${log_dir}/$peer_id.txt # Export to use in bash -c - timeout ${SYSTEM_TEST_TIMEOUT}s bash -c 'tail -n +1 -f $LOG_FILE | sed -n "/TS_PASS/q2; /TS_FAIL/q3"' # bash -c is necessary to use timeout with | and still get the right exit codes + timeout ${SYSTEM_TEST_TIMEOUT}s bash -c 'tail -f -n +1 -s0.1 $LOG_FILE | sed -n "/TS_PASS/q2; /TS_FAIL/q3"' # bash -c is necessary to use timeout with | and still get the right exit codes # Branch on exit code of previous command case $? in diff --git a/test_suite/system_tests.sh b/test_suite/system_tests.sh index b220f47..719fc18 100755 --- a/test_suite/system_tests.sh +++ b/test_suite/system_tests.sh @@ -22,12 +22,18 @@ The following options can be used to configure additional parameters during the -d Add delay to packets transmitted by the eduP2P clients, control server and relay server The delay should be provided as an integer that represents the one-way delay in milliseconds - -l + -l Specifies the log level used in the eduP2P client of the two peers - The log level 'info' should not be used if a system test is run where one of the peers uses userspace WireGuard (the other peer's IP address is not logged in this case) + If one of the peers uses userspace WireGuard, the log level trace/debug must be used, since the other peer's IP address is not logged otherwise -L Specifies the alphanumeric name of the directory inside system_test_logs/ where the test logs will be stored - If this argument is not provided, the directory name is the current timestamp""" + If this argument is not provided, the directory name is the current timestamp + -t + Run the system tests in parallel with the specified number of threads. + It is not recommended to combine this flag with -p, as multithreading will likely degrade the performance and the graphs will not be created automatically + -b + Build the client, control server and relay server binaries before running the tests""" + # Use functions and constants from util.sh . ./util.sh @@ -35,7 +41,7 @@ The following options can be used to configure additional parameters during the log_lvl="debug" # Validate optional arguments -while getopts ":c:d:ef:l:L:ph" opt; do +while getopts ":c:d:ef:l:L:t:bph" opt; do case $opt in c) connectivity=true @@ -73,21 +79,23 @@ while getopts ":c:d:ef:l:L:ph" opt; do l) log_lvl=$OPTARG - # Log level should be info/debug/trace - log_lvl_regex="^info|debug|trace?$" + log_lvl_regex="^trace|debug|info|warn|error?$" validate_str $log_lvl $log_lvl_regex ;; L) alphanum_regex="^[a-zA-Z0-9]+$" validate_str $OPTARG $alphanum_regex log_dir_rel=system_test_logs/$OPTARG + ;; + t) + n_threads=$OPTARG - # Ensure log dir does not exist yet - ls $log_dir_rel &> /dev/null - - if [[ $? -eq 0 ]]; then - exit_with_error "$log_dir_rel already exists" - fi + # Make sure n_threads is an integer between 2 and 8 + threads_regex="^[2-8]$" + validate_str $n_threads $int_regex + ;; + b) + build=true ;; p) performance=true @@ -102,12 +110,23 @@ while getopts ":c:d:ef:l:L:ph" opt; do esac done -# Shift positional parameters indexing by accounting for the optional arguments -shift $((OPTIND-1)) - # Store repository's root directory for later use repo_dir=$(cd ..; pwd) +function create_log_dir() { + if [[ -z $log_dir_rel ]]; then + timestamp=$(date +"%Y-%m-%dT%H_%M_%S") + log_dir_rel=system_test_logs/${timestamp} # Relative path for pretty printing + fi + + log_dir=${repo_dir}/test_suite/${log_dir_rel} # Absolute path for use in scripts running from different directories + mkdir -p ${log_dir} + echo "Logging to ${log_dir_rel}" +} + +create_log_dir + +# ================================ FUNCTIONS FOR SEQUENTIAL SYSTEM TESTS ================================ function cleanup () { # Kill the two servers if they have already been started by the script sudo pkill control_server @@ -117,9 +136,6 @@ function cleanup () { sudo kill $test_pid &> /dev/null } -# Run cleanup when script exits -trap cleanup EXIT - function build_go() { for binary in test_client control_server relay_server; do binary_dir="${repo_dir}/test_suite/$binary" @@ -127,28 +143,11 @@ function build_go() { done } -build_go - -function create_log_dir() { - if [[ -z $log_dir_rel ]]; then - timestamp=$(date +"%Y-%m-%dT%H_%M_%S") - log_dir_rel=system_test_logs/${timestamp} # Relative path for pretty printing - fi - - log_dir=${repo_dir}/test_suite/${log_dir_rel} # Absolute path for use in scripts running from different directories - mkdir -p ${log_dir} - echo "Logging to ${log_dir_rel}" -} - -create_log_dir - function setup_networks() { cd nat_simulation/ adm_ips=$(sudo ./setup_networks.sh) # setup_networks.sh returns an array of IPs used by hosts in the network simulation setup, this list is needed to simulate a NAT device with an Address-Dependent Mapping } -setup_networks - function extract_server_pub_key() { server_type=$1 # control_server or relay_server ip=$2 @@ -206,27 +205,133 @@ function setup_servers() { start_server "relay_server" $relay_ip $relay_port } -# Choose ports for the control and relay servers, then start them -control_port=9999 -relay_port=3340 -echo "Setting up servers" -setup_servers +function sequential_setup() { + # Run cleanup when script exits + trap cleanup EXIT + + # Go build binaries unless -b flag was specified + if [[ $build == true ]]; then + echo "Building binaries..." + build_go + else + echo "Skipped building binaries" + fi + + setup_networks + + # Choose ports for the control and relay servers, then start them + control_port=9999 + relay_port=3340 + echo "Setting up servers" + setup_servers + + cd $repo_dir/test_suite + + if [[ -n $packet_loss ]]; then + sudo ./set_packet_loss.sh $packet_loss + fi + + if [[ -n $delay ]]; then + sudo ./set_delay.sh $delay + fi + + # Test counters + n_tests=0 + n_failed=0 +} + +# Log messages that should not be printed when the tests are run in parallel +function log_sequential() { + msg=$1 + + if [[ -z $n_threads ]]; then + echo -e $msg + fi +} + +# ================================ FUNCTIONS FOR PARALLEL SYSTEM TESTS ================================ +function parallel_setup() { + echo """ +Dividing the system tests among $n_threads threads. The output of each thread can be found in the logs.""" + + # The current system tests command will be run in parallel docker containers with a few modifications: + system_test_opts=$(echo $@ | sed -r -e "s/-f \S+//" `# Potential -f flag is removed, as each docker container will be assigned a file containing a subset of the current system tests` \ + -e "s/-t [2-8]//") # -t flag is removed, since each docker container will run the tests in parallel` -# Test counters -n_tests=0 -n_failed=0 + # Tests will be assigned to the containers in a round-robin manner, so we keep track of the current thread + current_thread=0 + + # Arrays of length n_threads representing number of assigned and completed tests per thread, initialized to 0 + assigned=() + completed=() + + for i in $(seq 1 $n_threads); do + # Initialize above arrays to 0 + assigned+=(0) + completed+=(0) + + # Create a directory and file for each thread to store the system test logs and commands + mkdir $log_dir/thread$i + touch $log_dir/thread$i/tests.txt + done +} + +function log_parallel() { + thread=$1 + msg=$2 + + let "move_cursor = n_threads - thread" + + # Move cursor up to the line for the current thread + echo -ne "\033[${move_cursor}A\tThread $((thread+1)): $msg\r" + + # Move cursor back to original position + echo -ne "\033[${move_cursor}B\r" + +} + +function monitor_thread_progress() { + current_thread=0 + + while : # while True + do + for i in $(seq 0 $((n_threads-1))); do + if [[ ${completed[$i]} -ne ${assigned[$i]} ]]; then + completed[$i]=$(docker logs ${container_ids[$i]} | grep -Ec "result=\S+TS_(PASS|FAIL)") # \S+ matches special characters that give color to test result + log_parallel $i $(progress_bar ${completed[$i]} ${assigned[$i]}) + fi + done + + sleep 1s + done +} + +# ================================ SYSTEM TESTS LOGIC ================================ +# Check if -t flag was specified +if [[ -n $n_threads ]]; then + parallel_setup +else + sequential_setup +fi # Usage: run_system_test [optional arguments of system_test.sh] function run_system_test() { - let "n_tests++" - - # Run in background and wait for test to finish to allow for interrupting from the terminal - ./system_test.sh $@ $n_tests $control_pub_key $control_ip $control_port "$adm_ips" $log_lvl $log_dir $repo_dir & - test_pid=$! - wait $test_pid - - if [[ $? -ne 0 ]]; then - let "n_failed++" + if [[ -n $n_threads ]]; then # Save the system test to the file corresponding to the current thread + current_thread_dir="thread$((current_thread+1))" + echo "run_system_test $@" >> $log_dir/$current_thread_dir/tests.txt + let "assigned[$current_thread]++" + let "current_thread = (current_thread+1) % n_threads" + else # Run the system test now + let "n_tests++" + + # Run in background and wait for test to finish to allow for interrupting from the terminal + ./system_test.sh $@ $n_tests $control_pub_key $control_ip $control_port "$adm_ips" $log_lvl $log_dir $repo_dir & + test_pid=$! + wait $test_pid + + if [[ $? -ne 0 ]]; then + let "n_failed++" + fi fi } @@ -267,19 +372,9 @@ function connectivity_test_logic() { fi } -cd $repo_dir/test_suite - -if [[ -n $packet_loss ]]; then - sudo ./set_packet_loss.sh $packet_loss -fi - -if [[ -n $delay ]]; then - sudo ./set_delay.sh $delay -fi - if [[ $performance == true ]]; then - echo -e "\nPerformance tests (without NAT)" - run_system_test -k bitrate -v 100,200,300,400,500 -d 3 -b both -r 3 TS_PASS_DIRECT router1-router2 : wg0:wg0 + log_sequential "\nPerformance tests (without NAT)" + run_system_test -k bitrate -v 100,200,300,400,500 -d 3 -b both TS_PASS_DIRECT router1-router2 : wg0:wg0 elif [[ -n $file ]]; then echo -e "\nTests from file: $file" @@ -290,13 +385,13 @@ elif [[ -n $file ]]; then else rfc_3489_nats=("0-0" "0-1" "0-2" "2-2") - echo """ + log_sequential """ Starting connectivity tests between two peers (possibly) behind NATs with various combinations of mapping and filtering behaviour: - Endpoint-Independent Mapping/Filtering (EIM/EIF) - Address-Dependent Mapping/Filtering (ADM/ADF) - Address and Port-Dependent Mapping/Filtering (ADPM/ADPF)""" - echo -e "\nTests with one peer behind a NAT" + log_sequential "\nTests with one peer behind a NAT" for nat_mapping in {0..2}; do for nat_filter in {0..2}; do nat=$nat_mapping-$nat_filter @@ -308,7 +403,7 @@ Starting connectivity tests between two peers (possibly) behind NATs with variou done done - echo -e "\nTests with both peers behind a NAT" + log_sequential "\nTests with both peers behind a NAT" for nat1_mapping in {0..2}; do for nat1_filter in {0..2}; do for nat2_mapping in {0..2}; do @@ -319,7 +414,7 @@ Starting connectivity tests between two peers (possibly) behind NATs with variou done done - echo -e "\nTest hairpinning" + log_sequential "\nTest hairpinning" for nat_mapping in {0..2}; do for nat_filter in {0..2}; do nat=$nat_mapping-$nat_filter @@ -333,6 +428,9 @@ Starting connectivity tests between two peers (possibly) behind NATs with variou fi function print_summary() { + n_failed=$1 + n_tests=$2 + if [[ $n_failed -eq 0 ]]; then echo -e "${GREEN}All tests passed!${NC}" else @@ -341,7 +439,71 @@ function print_summary() { fi } -print_summary +if [[ -n $n_threads ]]; then + # Keep track of docker container IDs + container_ids=() + + docker_log_dir="/go/common/test_suite/system_test_logs" + + for i in $(seq 1 $n_threads); do + thread="thread$i" + + # Run the thread in a docker container, and store its ID + container_id=$(docker run \ + --network=host `# Host driver gives faster curl connectivity check` \ + --cap-add CAP_SYS_ADMIN --cap-add NET_ADMIN --security-opt apparmor=unconfined --device /dev/net/tun:/dev/net/tun `# Permissions required to create network setup` \ + --mount type=bind,src=$log_dir/$thread,dst=$docker_log_dir/$thread `# Bind logs inside docker container to the corresponding thread on the host` \ + -dt system_tests -f $docker_log_dir/$thread/tests.txt -L $thread `# Run tests from this thread's file and store the logs in the mounted directory` \ + $system_test_opts) # Copy the remaining options from the current system tests command + container_ids+=($container_id) + + # Print progress bar for this thread, unless test is run as GitHub Action + if [[ -z $GITHUB_ACTION ]]; then + echo -e "\tThread $i: $(progress_bar 0 ${assigned[$((i-1))]})\r" + fi + done + + # Report on progress of each thread + if [[ -z $GITHUB_ACTION ]]; then + monitor_thread_progress & + progress_pid=$! + fi + + exit_codes=( $(docker wait ${container_ids[@]}) ) # Each exit code represents the amount of failed tests in the corresponding container + + if [[ -z $GITHUB_ACTION ]]; then + kill $progress_pid # Stop monitoring progress after all containers have finished + fi + + # Print summary for each thread individually + for i in $(seq 0 $((n_threads-1))); do + test_summary=$(print_summary ${exit_codes[$i]} ${assigned[$i]}) + + if [[ -z $GITHUB_ACTION ]]; then + progress_bar=$(progress_bar ${assigned[$i]} ${assigned[$i]}) + log_parallel $i "$progress_bar - $test_summary" + else + echo -e "\tThread $((i+1)): $test_summary" + fi + done + + # Replace space delimiters by + and pipe into calculator + n_failed=$(echo ${exit_codes[@]} | tr " " + | bc) + n_tests=$(echo ${assigned[@]} | tr " " + | bc) + + # Log the containers' outputs + for i in $(seq 1 $n_threads); do + thread="thread$i" + id=${container_ids[$((i-1))]} + docker logs $id > $log_dir/$thread/cmd_output.txt + done + + # Containers are only used one time, now that they have finished running they can be removed + docker rm ${container_ids[@]} > /dev/null +else + # Create graphs for performance tests, if any were included + python3 visualize_performance_tests.py $log_dir +fi -# Create graphs for performance tests, if any were included -python3 visualize_performance_tests.py $log_dir \ No newline at end of file +print_summary $n_failed $n_tests +exit $n_failed \ No newline at end of file diff --git a/test_suite/test_client/main.go b/test_suite/test_client/main.go index 8fa9f22..9464724 100644 --- a/test_suite/test_client/main.go +++ b/test_suite/test_client/main.go @@ -16,6 +16,7 @@ import ( "github.com/edup2p/common/extwg" "github.com/edup2p/common/toversok" + "github.com/edup2p/common/types" "github.com/edup2p/common/types/dial" "github.com/edup2p/common/types/key" "github.com/edup2p/common/usrwg" @@ -76,6 +77,8 @@ func main() { level := slog.LevelInfo switch logLevel { + case "trace": + level = types.LevelTrace case "debug": level = slog.LevelDebug case "info": diff --git a/test_suite/test_client/setup_client.sh b/test_suite/test_client/setup_client.sh index c2f320a..9238e23 100755 --- a/test_suite/test_client/setup_client.sh +++ b/test_suite/test_client/setup_client.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash usage_str=""" -Usage: ${0} [WIREGUARD INTERFACE] +Usage: ${0} [WIREGUARD INTERFACE] should be one of {trace|debug|info} (in order of most to least log messages), but can NOT be info if one if the peers is using userspace WireGuard (then IP of the other peer is not logged) @@ -27,8 +27,8 @@ done shift $((OPTIND-1)) # Make sure all required positional parameters have been passed -min_req=7 -max_req=8 +min_req=8 +max_req=9 if [[ $# < $min_req || $# > $max_req ]]; then exit_with_error "expected $min_req or $max_req positional parameters, but received $#" @@ -41,7 +41,8 @@ control_pub_key=$4 control_ip=$5 control_port=$6 log_lvl=$7 -wg_interface=$8 +log_dir=$8 +wg_interface=$9 # Create WireGuard interface if wg_interface is set if [[ -n $wg_interface ]]; then @@ -52,6 +53,7 @@ fi # Create temporary file to store test_client output out="test_client_out_${id}.txt" +touch $out # Run test_client and store its output in the temporary file (sudo ./test_client --control-host=$control_ip --control-port=$control_port --control-key=control:$control_pub_key --ext-wg-device=$wg_interface --log-level=$log_lvl --config=$id.json 2>&1 | tee $out &) @@ -62,10 +64,6 @@ function clean_exit() { # Remove temporary test_client output file sudo rm $out - # Remove http server output files if they exist - rm $http_ipv4_out &> /dev/null - rm $http_ipv6_out &> /dev/null - # Kill http servers if they are running kill $http_ipv4_pid &> /dev/null kill $http_ipv6_pid &> /dev/null @@ -79,7 +77,7 @@ trap "clean_exit 1" SIGTERM # Get own virtual IPs and peer's virtual IPs with external WireGuard if [[ -n $wg_interface ]]; then # Store virtual IPs as " " when they are logged - ips=$(timeout 10s tail -n +1 -f $out | sed -rn "/.*sudo ip address add (\S+) dev ${wg_interface}; sudo ip address add (\S+) dev ${wg_interface}.*/{s//\1 \2/p;q}") + ips=$(timeout 10s tail -n +1 -f -s 0.1 $out | sed -rn "/.*sudo ip address add (\S+) dev ${wg_interface}; sudo ip address add (\S+) dev ${wg_interface}.*/{s//\1 \2/p;q}") if [[ -z $ips ]]; then echo "TS_FAIL: could not find own virtual IPs in logs" @@ -103,7 +101,7 @@ if [[ -n $wg_interface ]]; then peer_ips=$(wg show $wg_interface allowed-ips | cut -d$'\t' -f2) # IPs are shown as "\t " while [[ -z $peer_ips ]]; do - sleep 1s + sleep 0.1s let "timeout--" if [[ $timeout -eq 0 ]]; then @@ -123,7 +121,7 @@ else timeout=10 while ! ip address show ts0 | grep -Eq "inet [0-9.]+"; do - sleep 1s + sleep 0.1s let "timeout--" if [[ $timeout -eq 0 ]]; then @@ -136,8 +134,8 @@ else ipv4=$(extract_ipv4 $peer_ns ts0) ipv6=$(extract_ipv6 $peer_ns ts0) - # Store peer IPs as " "" when they are logged - peer_ips=$(timeout 10s tail -n +1 -f $out | sed -rn "/.*IPv4:(\S+) IPv6:(\S+).*/{s//\1 \2/p;q}") + # Store peer IPs as " " when they are logged + peer_ips=$(timeout 10s tail -f -n +1 -s 0.1 $out | sed -rn "/.*IPv4:(\S+) IPv6:(\S+).*/{s//\1 \2/p;q}") if [[ -z $peer_ips ]]; then echo "TS_FAIL: could not find peer's virtual IPs in logs" @@ -149,15 +147,23 @@ else peer_ipv6=$(echo $peer_ips | cut -d ' ' -f2) fi +# Necessary to avoid failures with hairpinning tests, probably caused by delay in adding nftables rules to simulate hairpinning +sleep 0.5s + # Start HTTP servers on own virtual IPs for peer to access, and save their pids to kill them during cleanup -http_ipv4_out="http_ipv4_output_${id}.txt" +http_ipv4_out="$log_dir/${id}_http_ipv4.txt" python3 -m http.server -b $ipv4 80 &> $http_ipv4_out & http_ipv4_pid=$! -http_ipv6_out="http_ipv6_output_${id}.txt" +http_ipv6_out="$log_dir/${id}_http_ipv6.txt" python3 -m http.server -b $ipv6 80 &> $http_ipv6_out & http_ipv6_pid=$! +# Desynchronize peers to avoid error and subsequent recovery delay caused by handshake initation in both directions at same time +if [[ $id == "peer1" ]]; then + sleep 0.1s +fi + # Try connecting to peer's HTTP server hosted on IP addres function try_connect() { peer_addr=$1 @@ -172,14 +178,13 @@ try_connect "http://${peer_ipv4}" # Peers try to establish a direct connection after initial connection; if expecting a direct connection, give them some time to establish one if [[ $test_target == "TS_PASS_DIRECT" ]]; then - timeout 10s tail -f -n +1 $out | sed -n "/ESTABLISHED direct peer connection/q" + timeout 10s tail -f -n +1 -s 0.1 $out | sed -n "/ESTABLISHED direct peer connection/q" fi try_connect "http://[${peer_ipv6}]" -# Wait until timeout or until peer connected to server (peer's IP will appear in server output) -timeout 10s tail -f -n +1 $http_ipv4_out | sed -n "/${peer_ipv4}/q" -timeout 10s tail -f -n +1 $http_ipv6_out | sed -n "/${peer_ipv6}/q" +# Wait until timeout or until peer connected to second server (peer's IP will appear in server output) +timeout 10s tail -f -n +1 -s 0.1 $http_ipv6_out | sed -n "/${peer_ipv6}/q" echo "TS_PASS" clean_exit 0 \ No newline at end of file diff --git a/test_suite/visualize_performance_tests.py b/test_suite/visualize_performance_tests.py index 0622756..e6b0c5f 100644 --- a/test_suite/visualize_performance_tests.py +++ b/test_suite/visualize_performance_tests.py @@ -211,7 +211,7 @@ def aggregate_repetitions(extracted_data: dict) -> dict: # Given a dictionary containing the label and unit of a metric, returns a string to describe the metric on a graph axis def axis_label(label_unit_dict: dict) -> str: - return f"{label_unit_dict["label"]} ({label_unit_dict["unit"]})" + return f"{label_unit_dict['label']} ({label_unit_dict['unit']})" # Graph to illustrate the performance of eduP2P, possibly by comparing against WireGuard and/or a direct connection def create_performance_graph(test_var: str, test_var_values: list[float], metric: str, extracted_data: dict, save_path: str):

#X5 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